Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 756b9f5d1c | |||
| 772a68a87b | |||
| b75e4bcdd9 | |||
| b7ecfa5e35 |
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
> 基于原作者v0.29.1代码修改
|
> 基于原作者v0.29.1代码修改
|
||||||
|
|
||||||
|
#### v0.29.1-2
|
||||||
|
1. 置顶内容新增自动折叠;置顶效果及图标改为全局显示
|
||||||
|
2. 归档页面新增(取消)置顶、编辑、复制按钮
|
||||||
|
3. 权限图标改为全局显示
|
||||||
|
4. 修复中文目录锚点无效;优化目录显示效果
|
||||||
|
5. 备忘录被删除时显式删除其分享链接
|
||||||
|
|
||||||
#### v0.29.1-1
|
#### v0.29.1-1
|
||||||
1. 修改默认配置
|
1. 修改默认配置
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare)
|
|||||||
if delete.UID != nil {
|
if delete.UID != nil {
|
||||||
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
|
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
|
||||||
}
|
}
|
||||||
|
if delete.MemoID != nil {
|
||||||
|
where, args = append(where, "`memo_id` = ?"), append(args, *delete.MemoID)
|
||||||
|
}
|
||||||
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
|
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare)
|
|||||||
if delete.UID != nil {
|
if delete.UID != nil {
|
||||||
where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *delete.UID)
|
where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *delete.UID)
|
||||||
}
|
}
|
||||||
|
if delete.MemoID != nil {
|
||||||
|
where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *delete.MemoID)
|
||||||
|
}
|
||||||
_, err := d.db.ExecContext(ctx, "DELETE FROM memo_share WHERE "+strings.Join(where, " AND "), args...)
|
_, err := d.db.ExecContext(ctx, "DELETE FROM memo_share WHERE "+strings.Join(where, " AND "), args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare)
|
|||||||
if delete.UID != nil {
|
if delete.UID != nil {
|
||||||
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
|
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
|
||||||
}
|
}
|
||||||
|
if delete.MemoID != nil {
|
||||||
|
where, args = append(where, "`memo_id` = ?"), append(args, *delete.MemoID)
|
||||||
|
}
|
||||||
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
|
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,11 @@ func (s *Store) DeleteMemo(ctx context.Context, delete *DeleteMemo) error {
|
|||||||
if err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{RelatedMemoID: &delete.ID}); err != nil {
|
if err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{RelatedMemoID: &delete.ID}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Clean up share links for this memo. SQLite runs with foreign keys disabled,
|
||||||
|
// so it cannot rely on ON DELETE CASCADE for memo_share rows.
|
||||||
|
if err := s.driver.DeleteMemoShare(ctx, &DeleteMemoShare{MemoID: &delete.ID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Clean up attachments linked to this memo.
|
// Clean up attachments linked to this memo.
|
||||||
attachments, err := s.ListAttachments(ctx, &FindAttachment{MemoID: &delete.ID})
|
attachments, err := s.ListAttachments(ctx, &FindAttachment{MemoID: &delete.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+3
-2
@@ -22,8 +22,9 @@ type FindMemoShare struct {
|
|||||||
|
|
||||||
// DeleteMemoShare identifies a share grant to remove.
|
// DeleteMemoShare identifies a share grant to remove.
|
||||||
type DeleteMemoShare struct {
|
type DeleteMemoShare struct {
|
||||||
ID *int32
|
ID *int32
|
||||||
UID *string
|
UID *string
|
||||||
|
MemoID *int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMemoShare creates a new share grant.
|
// CreateMemoShare creates a new share grant.
|
||||||
|
|||||||
@@ -112,10 +112,35 @@ func TestDeleteMemoStore(t *testing.T) {
|
|||||||
memo, err := ts.CreateMemo(ctx, memoCreate)
|
memo, err := ts.CreateMemo(ctx, memoCreate)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, memoCreate.Content, memo.Content)
|
require.Equal(t, memoCreate.Content, memo.Content)
|
||||||
|
otherMemo, err := ts.CreateMemo(ctx, &store.Memo{
|
||||||
|
UID: "test-other-memo",
|
||||||
|
CreatorID: user.ID,
|
||||||
|
Content: "other test content",
|
||||||
|
Visibility: store.Public,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
share, err := ts.CreateMemoShare(ctx, &store.MemoShare{
|
||||||
|
UID: "test-delete-memo-share",
|
||||||
|
MemoID: memo.ID,
|
||||||
|
CreatorID: user.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
keptShare, err := ts.CreateMemoShare(ctx, &store.MemoShare{
|
||||||
|
UID: "test-keep-memo-share",
|
||||||
|
MemoID: otherMemo.ID,
|
||||||
|
CreatorID: user.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
err = ts.DeleteMemo(ctx, &store.DeleteMemo{
|
err = ts.DeleteMemo(ctx, &store.DeleteMemo{
|
||||||
ID: memo.ID,
|
ID: memo.ID,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
deletedShare, err := ts.GetMemoShare(ctx, &store.FindMemoShare{ID: &share.ID})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, deletedShare)
|
||||||
|
keptShareAfterDelete, err := ts.GetMemoShare(ctx, &store.FindMemoShare{ID: &keptShare.ID})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, keptShareAfterDelete)
|
||||||
ts.Close()
|
ts.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" sideOffset={2}>
|
<DropdownMenuContent align="end" sideOffset={2}>
|
||||||
{/* Edit actions (non-readonly, non-archived) */}
|
{/* Edit actions (non-readonly) */}
|
||||||
{!readonly && !isArchived && (
|
{!readonly && (
|
||||||
<>
|
<>
|
||||||
{!isComment && (
|
{!isComment && (
|
||||||
<DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>
|
<DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>
|
||||||
@@ -87,25 +87,23 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Copy submenu (non-archived) */}
|
{/* Copy submenu */}
|
||||||
{!isArchived && (
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubTrigger>
|
<CopyIcon className="w-4 h-auto" />
|
||||||
<CopyIcon className="w-4 h-auto" />
|
{t("common.copy")}
|
||||||
{t("common.copy")}
|
</DropdownMenuSubTrigger>
|
||||||
</DropdownMenuSubTrigger>
|
<DropdownMenuSubContent>
|
||||||
<DropdownMenuSubContent>
|
<DropdownMenuItem onClick={handleCopyLink}>
|
||||||
<DropdownMenuItem onClick={handleCopyLink}>
|
<LinkIcon className="w-4 h-auto" />
|
||||||
<LinkIcon className="w-4 h-auto" />
|
{t("memo.copy-link")}
|
||||||
{t("memo.copy-link")}
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleCopyContent}>
|
||||||
<DropdownMenuItem onClick={handleCopyContent}>
|
<FileTextIcon className="w-4 h-auto" />
|
||||||
<FileTextIcon className="w-4 h-auto" />
|
{t("memo.copy-content")}
|
||||||
{t("memo.copy-content")}
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuSubContent>
|
||||||
</DropdownMenuSubContent>
|
</DropdownMenuSub>
|
||||||
</DropdownMenuSub>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Task submenu (writable task memos) */}
|
{/* Task submenu (writable task memos) */}
|
||||||
{canMutateTasks && (
|
{canMutateTasks && (
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const MemoCommentSection = ({ memo, comments, parentPage }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<div className="w-full" key={`${comment.name}-${comment.updateTime}`} id={extractMemoIdFromName(comment.name)}>
|
<div className="w-full" key={`${comment.name}-${comment.updateTime}`} id={extractMemoIdFromName(comment.name)}>
|
||||||
<MemoView memo={comment} parentPage={parentPage} showCreator compact />
|
<MemoView memo={comment} parentPage={parentPage} showCreator showVisibility compact />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { HeadingItem } from "@/utils/markdown-manipulation";
|
import type { HeadingItem } from "@/utils/markdown-manipulation";
|
||||||
|
|
||||||
@@ -12,8 +13,95 @@ const levelIndent: Record<number, string> = {
|
|||||||
4: "ml-8",
|
4: "ml-8",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Outline navigation for memo headings (h1–h4). */
|
const areStringArraysEqual = (a: string[], b: string[]) => {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((item, index) => item === b[index]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Outline navigation for memo headings (h1-h4). */
|
||||||
const MemoOutline = ({ headings }: MemoOutlineProps) => {
|
const MemoOutline = ({ headings }: MemoOutlineProps) => {
|
||||||
|
const outlineRef = useRef<HTMLElement>(null);
|
||||||
|
const itemRefs = useRef(new Map<string, HTMLAnchorElement>());
|
||||||
|
const [activeSlugs, setActiveSlugs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const orderedSlugs = useMemo(() => headings.map((heading) => heading.slug).filter(Boolean), [headings]);
|
||||||
|
const activeSlugSet = useMemo(() => new Set(activeSlugs), [activeSlugs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveSlugs([]);
|
||||||
|
|
||||||
|
const elements = orderedSlugs
|
||||||
|
.map((slug) => document.getElementById(slug))
|
||||||
|
.filter((element): element is HTMLElement => Boolean(element));
|
||||||
|
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationFrame = 0;
|
||||||
|
|
||||||
|
const updateActiveSlugs = () => {
|
||||||
|
animationFrame = 0;
|
||||||
|
const viewportTop = window.scrollY;
|
||||||
|
const viewportBottom = viewportTop + (window.innerHeight || document.documentElement.clientHeight);
|
||||||
|
const documentBottom = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||||
|
const headingSections = elements.map((element, index) => {
|
||||||
|
const sectionTop = element.getBoundingClientRect().top + window.scrollY;
|
||||||
|
const nextElement = elements[index + 1];
|
||||||
|
const sectionBottom = nextElement ? nextElement.getBoundingClientRect().top + window.scrollY : documentBottom;
|
||||||
|
return { slug: element.id, top: sectionTop, bottom: sectionBottom };
|
||||||
|
});
|
||||||
|
const nextActiveSlugs = headingSections
|
||||||
|
.filter((section) => section.bottom > viewportTop && section.top < viewportBottom)
|
||||||
|
.map((section) => section.slug);
|
||||||
|
|
||||||
|
setActiveSlugs((previousSlugs) => (areStringArraysEqual(previousSlugs, nextActiveSlugs) ? previousSlugs : nextActiveSlugs));
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleUpdateActiveSlugs = () => {
|
||||||
|
if (animationFrame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
animationFrame = window.requestAnimationFrame(updateActiveSlugs);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateActiveSlugs();
|
||||||
|
window.addEventListener("scroll", scheduleUpdateActiveSlugs, { passive: true });
|
||||||
|
window.addEventListener("resize", scheduleUpdateActiveSlugs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (animationFrame) {
|
||||||
|
window.cancelAnimationFrame(animationFrame);
|
||||||
|
}
|
||||||
|
window.removeEventListener("scroll", scheduleUpdateActiveSlugs);
|
||||||
|
window.removeEventListener("resize", scheduleUpdateActiveSlugs);
|
||||||
|
};
|
||||||
|
}, [orderedSlugs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const firstActiveSlug = activeSlugs[0];
|
||||||
|
const outline = outlineRef.current;
|
||||||
|
const activeItem = firstActiveSlug ? itemRefs.current.get(firstActiveSlug) : undefined;
|
||||||
|
|
||||||
|
if (!outline || !activeItem || outline.scrollHeight <= outline.clientHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemTop = activeItem.offsetTop;
|
||||||
|
const itemBottom = itemTop + activeItem.offsetHeight;
|
||||||
|
const comfortableTop = outline.scrollTop + 24;
|
||||||
|
const comfortableBottom = outline.scrollTop + outline.clientHeight - 24;
|
||||||
|
|
||||||
|
if (itemTop >= comfortableTop && itemBottom <= comfortableBottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outline.scrollTo({
|
||||||
|
top: Math.max(0, itemTop - outline.clientHeight / 2 + activeItem.offsetHeight / 2),
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}, [activeSlugs]);
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
|
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const el = document.getElementById(slug);
|
const el = document.getElementById(slug);
|
||||||
@@ -24,27 +112,50 @@ const MemoOutline = ({ headings }: MemoOutlineProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="relative flex flex-col">
|
<nav ref={outlineRef} className="relative flex max-h-[45vh] flex-col overflow-y-auto overscroll-contain pr-1">
|
||||||
{headings.map((heading, index) => (
|
{headings.map((heading, index) => {
|
||||||
<a
|
const isActive = activeSlugSet.has(heading.slug);
|
||||||
key={`${heading.slug}-${index}`}
|
|
||||||
href={`#${heading.slug}`}
|
return (
|
||||||
onClick={(e) => handleClick(e, heading.slug)}
|
<a
|
||||||
className={cn(
|
key={`${heading.slug}-${index}`}
|
||||||
"group relative block py-[5px] pr-1 text-[13px] leading-snug truncate",
|
ref={(element) => {
|
||||||
"text-muted-foreground/60 hover:text-foreground/90",
|
if (element) {
|
||||||
"transition-colors duration-200 ease-out",
|
itemRefs.current.set(heading.slug, element);
|
||||||
levelIndent[heading.level],
|
} else {
|
||||||
heading.level === 1 && "font-medium text-muted-foreground/80",
|
itemRefs.current.delete(heading.slug);
|
||||||
)}
|
}
|
||||||
title={heading.text}
|
}}
|
||||||
>
|
href={`#${heading.slug}`}
|
||||||
<span className="relative">
|
aria-current={activeSlugs[0] === heading.slug ? "location" : undefined}
|
||||||
{heading.text}
|
onClick={(e) => handleClick(e, heading.slug)}
|
||||||
<span className="absolute -bottom-px left-0 h-px w-0 bg-foreground/30 transition-all duration-200 group-hover:w-full" />
|
className={cn(
|
||||||
</span>
|
"group relative block shrink-0 rounded-l-sm py-[5px] pr-1 pl-2 text-[13px] leading-snug truncate",
|
||||||
</a>
|
"transition-colors duration-200 ease-out",
|
||||||
))}
|
isActive ? "bg-primary/10 font-medium text-primary" : "text-muted-foreground/60 hover:text-foreground/90",
|
||||||
|
levelIndent[heading.level],
|
||||||
|
heading.level === 1 && !isActive && "font-medium text-muted-foreground/80",
|
||||||
|
)}
|
||||||
|
title={heading.text}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute top-1.5 bottom-1.5 left-0 w-0.5 rounded-full bg-primary transition-opacity duration-200",
|
||||||
|
isActive ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="relative">
|
||||||
|
{heading.text}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute -bottom-px left-0 h-px bg-foreground/30 transition-all duration-200",
|
||||||
|
isActive ? "w-full bg-primary/50" : "w-0 group-hover:w-full",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
|
|||||||
content={memo.content}
|
content={memo.content}
|
||||||
onClick={handleMemoContentClick}
|
onClick={handleMemoContentClick}
|
||||||
onDoubleClick={handleMemoContentDoubleClick}
|
onDoubleClick={handleMemoContentDoubleClick}
|
||||||
compact={memo.pinned ? false : compact} // Always show full content when pinned
|
compact={compact}
|
||||||
/>
|
/>
|
||||||
<AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
|
<AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
|
||||||
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
|
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
|
|||||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { convertVisibilityToString } from "@/utils/memo";
|
import { convertVisibilityToString } from "@/utils/memo";
|
||||||
@@ -67,7 +66,7 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
{showVisibility && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<span className="flex justify-center items-center rounded-md hover:opacity-80">
|
<span className="flex justify-center items-center rounded-md hover:opacity-80">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const Archived = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PagedMemoList
|
<PagedMemoList
|
||||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility compact />}
|
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility showPinned compact />}
|
||||||
listSort={listSort}
|
listSort={listSort}
|
||||||
state={State.ARCHIVED}
|
state={State.ARCHIVED}
|
||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ const Explore = () => {
|
|||||||
|
|
||||||
// Get sorting logic using unified hook (no pinned sorting)
|
// Get sorting logic using unified hook (no pinned sorting)
|
||||||
const { listSort, orderBy } = useMemoSorting({
|
const { listSort, orderBy } = useMemoSorting({
|
||||||
pinnedFirst: false,
|
pinnedFirst: true,
|
||||||
state: State.NORMAL,
|
state: State.NORMAL,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PagedMemoList
|
<PagedMemoList
|
||||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}
|
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility showPinned compact />}
|
||||||
listSort={listSort}
|
listSort={listSort}
|
||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
filter={memoFilter}
|
filter={memoFilter}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function slugify(text: string): string {
|
|||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/[^\w\s-]/g, "")
|
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
|
||||||
.replace(/[\s_]+/g, "-")
|
.replace(/[\s_]+/g, "-")
|
||||||
.replace(/-+/g, "-")
|
.replace(/-+/g, "-")
|
||||||
.replace(/^-|-$/g, "");
|
.replace(/^-|-$/g, "");
|
||||||
@@ -143,6 +143,8 @@ export function extractHeadings(markdown: string): HeadingItem[] {
|
|||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
let slug = slugify(text);
|
let slug = slugify(text);
|
||||||
|
if (!slug) return;
|
||||||
|
|
||||||
const count = slugCounts.get(slug) || 0;
|
const count = slugCounts.get(slug) || 0;
|
||||||
slugCounts.set(slug, count + 1);
|
slugCounts.set(slug, count + 1);
|
||||||
if (count > 0) slug = `${slug}-${count}`;
|
if (count > 0) slug = `${slug}-${count}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user