性能优化
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "idx_page_parent_order" ON "Page"("parentId", "order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "idx_page_created_at" ON "Page"("createdAt");
|
||||
@@ -22,6 +22,9 @@ model Page {
|
||||
updatedAt DateTime @updatedAt
|
||||
order Int @default(0)
|
||||
isLocked Boolean @default(false)
|
||||
|
||||
@@index([parentId, order], map: "idx_page_parent_order")
|
||||
@@index([createdAt], map: "idx_page_created_at")
|
||||
}
|
||||
|
||||
model GlobalSettings {
|
||||
|
||||
@@ -18,13 +18,28 @@ export async function GET(request: NextRequest) {
|
||||
// 默认行为:返回所有页面(向后兼容)
|
||||
const usePagination = limit > 0 && limit <= 1000 && page > 0;
|
||||
|
||||
const baseSelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
content: !lightweight,
|
||||
tags: true,
|
||||
icon: true,
|
||||
type: true,
|
||||
parentId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
order: true,
|
||||
isLocked: true,
|
||||
} as const;
|
||||
|
||||
if (!usePagination) {
|
||||
const pages = await prisma.page.findMany({
|
||||
select: baseSelect,
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
const parsedPages = pages.map((p) => ({
|
||||
...p,
|
||||
content: lightweight ? '' : p.content,
|
||||
content: lightweight ? '' : (p.content || ''),
|
||||
tags: safeParseTags(p.tags),
|
||||
}));
|
||||
return NextResponse.json(parsedPages);
|
||||
@@ -34,6 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
const skip = (page - 1) * limit;
|
||||
const [pages, total] = await Promise.all([
|
||||
prisma.page.findMany({
|
||||
select: baseSelect,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
@@ -133,4 +149,3 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+6
-1
@@ -360,13 +360,18 @@ ul[data-type="taskList"],
|
||||
The component handles its own layout. */
|
||||
|
||||
/* Override Highlight.js background for a softer look */
|
||||
.ProseMirror pre {
|
||||
.ProseMirror pre {
|
||||
background: hsl(var(--code-bg)) !important;
|
||||
border: 1px solid hsl(var(--border) / 0.5);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin: 0.95rem 0;
|
||||
overflow-x: auto;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.ProseMirror pre code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
|
||||
+62
-7
@@ -25,7 +25,16 @@ const ICONS = [
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const { activePageId, pages, pageSync, updatePage, setActivePageId, addPage, retryPageSync, retryPendingSyncs, flushPendingSyncs } = useEditorStore();
|
||||
const activePageId = useEditorStore((s) => s.activePageId);
|
||||
const pages = useEditorStore((s) => s.pages);
|
||||
const pageSync = useEditorStore((s) => s.pageSync);
|
||||
const updatePage = useEditorStore((s) => s.updatePage);
|
||||
const setActivePageId = useEditorStore((s) => s.setActivePageId);
|
||||
const addPage = useEditorStore((s) => s.addPage);
|
||||
const retryPageSync = useEditorStore((s) => s.retryPageSync);
|
||||
const retryPendingSyncs = useEditorStore((s) => s.retryPendingSyncs);
|
||||
const flushPendingSyncs = useEditorStore((s) => s.flushPendingSyncs);
|
||||
const fetchPageContent = useEditorStore((s) => s.fetchPageContent);
|
||||
const { openSearchWithTag } = useSearchStore();
|
||||
const { timezone } = useSettingsStore();
|
||||
|
||||
@@ -36,6 +45,7 @@ export default function Home() {
|
||||
const [isAddingTag, setIsAddingTag] = useState(false);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [isIconPickerOpen, setIsIconPickerOpen] = useState(false);
|
||||
const [isLinksOpen, setIsLinksOpen] = useState(false);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(() => !activePageId);
|
||||
@@ -57,19 +67,22 @@ export default function Home() {
|
||||
}, [activePage, pages]);
|
||||
|
||||
const outgoingLinks = useMemo(() => {
|
||||
if (!isLinksOpen) return [];
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getOutgoingLinks(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
}, [activePage, pages, isLinksOpen]);
|
||||
|
||||
const backlinks = useMemo(() => {
|
||||
if (!isLinksOpen) return [];
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getBacklinks(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
}, [activePage, pages, isLinksOpen]);
|
||||
|
||||
const unresolvedLinks = useMemo(() => {
|
||||
if (!isLinksOpen) return [];
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
return getUnresolvedLinkTitles(activePage, pages);
|
||||
}, [activePage, pages]);
|
||||
}, [activePage, pages, isLinksOpen]);
|
||||
|
||||
const localHistory = useMemo(() => {
|
||||
if (!activePage || activePage.type !== "file") return [];
|
||||
@@ -125,6 +138,27 @@ export default function Home() {
|
||||
};
|
||||
}, [flushPendingSyncs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePageId) return;
|
||||
const current = pages.find((p) => p.id === activePageId);
|
||||
if (!current || current.type === "folder") return;
|
||||
if (current.contentLoaded) return;
|
||||
fetchPageContent(activePageId);
|
||||
}, [activePageId, pages, fetchPageContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLinksOpen) return;
|
||||
const batchIds = pages
|
||||
.filter((p) => p.type === "file" && !p.contentLoaded)
|
||||
.slice(0, 8)
|
||||
.map((p) => p.id);
|
||||
if (batchIds.length === 0) return;
|
||||
|
||||
Promise.all(batchIds.map((id) => fetchPageContent(id))).catch((e) => {
|
||||
console.error("Failed to batch load pages for link analysis", e);
|
||||
});
|
||||
}, [isLinksOpen, pages, fetchPageContent]);
|
||||
|
||||
const handleEdgeSwipeStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (isMobileSidebarOpen) return;
|
||||
const touch = e.touches[0];
|
||||
@@ -411,8 +445,27 @@ export default function Home() {
|
||||
/>
|
||||
|
||||
<section className="ui-enter mt-6 space-y-3 md:mt-7">
|
||||
<div className="grid gap-2 md:grid-cols-3">
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<button
|
||||
onClick={() => setIsLinksOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between rounded-md px-1 py-1 text-left transition-colors hover:bg-muted/45"
|
||||
aria-expanded={isLinksOpen}
|
||||
aria-label="切换链接分析面板"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">链接分析</h3>
|
||||
{isLinksOpen && (
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||
{outgoingLinks.length + backlinks.length + unresolvedLinks.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown size={15} className={cn("text-muted-foreground transition-transform", isLinksOpen && "rotate-180")} />
|
||||
</button>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">展开后计算关联页面、反向链接和未解析链接。</p>
|
||||
{isLinksOpen && (
|
||||
<div className="mt-2 grid gap-2 md:grid-cols-3">
|
||||
<div className="rounded-md border border-border/60 bg-muted/10 p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">关联页面</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{outgoingLinks.length}</span>
|
||||
@@ -436,7 +489,7 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<div className="rounded-md border border-border/60 bg-muted/10 p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">反向链接</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{backlinks.length}</span>
|
||||
@@ -460,7 +513,7 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<div className="rounded-md border border-border/60 bg-muted/10 p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">未解析链接</h3>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{unresolvedLinks.length}</span>
|
||||
@@ -486,6 +539,8 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ui-card p-2.5 md:p-3">
|
||||
<button
|
||||
|
||||
@@ -55,7 +55,7 @@ export function CodeBlockComponent({
|
||||
return (
|
||||
<NodeViewWrapper className="group code-block relative my-4 overflow-hidden rounded-lg border border-border/40 shadow-sm" style={{ backgroundColor: codeBg }}>
|
||||
<div
|
||||
className="flex select-none items-center justify-between border-b border-border/50 px-3 py-2 text-xs text-zinc-400"
|
||||
className="flex select-none items-center justify-between border-b border-border/50 px-3 py-2 text-xs text-zinc-300"
|
||||
style={{ backgroundColor: "hsl(var(--code-bg) / 0.9)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -86,7 +86,7 @@ export function CodeBlockComponent({
|
||||
</div>
|
||||
|
||||
<div className="relative grid grid-cols-[auto_1fr] font-mono text-sm leading-6" style={{ backgroundColor: codeBg }}>
|
||||
<div className="select-none border-r border-white/5 px-2 py-4 text-right text-zinc-500" style={{ minWidth: "2.5rem" }} contentEditable={false}>
|
||||
<div className="select-none border-r border-white/5 px-2 py-4 text-right text-zinc-400" style={{ minWidth: "2.5rem" }} contentEditable={false}>
|
||||
{lineNumbers.map((line) => (
|
||||
<div key={line} className="px-1">
|
||||
{line}
|
||||
@@ -94,7 +94,7 @@ export function CodeBlockComponent({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<pre className="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white/10 !my-0 !border-0 !p-0 overflow-x-auto text-zinc-300" style={{ backgroundColor: codeBg }}>
|
||||
<pre className="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white/10 !my-0 !border-0 !p-0 overflow-x-auto text-zinc-100" style={{ backgroundColor: codeBg }}>
|
||||
<NodeViewContent
|
||||
as={"code" as unknown as "div"}
|
||||
className="block min-w-full !whitespace-pre !bg-transparent !p-4 !font-mono !text-sm !leading-6 outline-none"
|
||||
|
||||
@@ -143,7 +143,13 @@ export function SidebarContent({
|
||||
onCloseMobile?: () => void;
|
||||
onToggleSidebarCollapse?: () => void;
|
||||
}) {
|
||||
const { pages, activePageId, fetchPages, addPage, isLoading, movePage, reorderPages } = useEditorStore();
|
||||
const pages = useEditorStore((s) => s.pages);
|
||||
const activePageId = useEditorStore((s) => s.activePageId);
|
||||
const fetchPages = useEditorStore((s) => s.fetchPages);
|
||||
const addPage = useEditorStore((s) => s.addPage);
|
||||
const isLoading = useEditorStore((s) => s.isLoading);
|
||||
const movePage = useEditorStore((s) => s.movePage);
|
||||
const reorderPages = useEditorStore((s) => s.reorderPages);
|
||||
const { setOpen, openSearchWithTag } = useSearchStore();
|
||||
const { triggerImport, isImporting } = useImport();
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -359,18 +365,7 @@ export function SidebarContent({
|
||||
}}
|
||||
>
|
||||
<Upload size={14} />
|
||||
{isImporting ? "导入中..." : "导入到当前层级"}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
disabled={isImporting}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
|
||||
onClick={() => {
|
||||
triggerImport(null);
|
||||
handleItemClick();
|
||||
}}
|
||||
>
|
||||
<Upload size={14} />
|
||||
{isImporting ? "导入中..." : "导入到根目录"}
|
||||
{isImporting ? "导入中..." : "导入文档"}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="my-1 h-px bg-muted" />
|
||||
<DropdownMenu.Item
|
||||
|
||||
@@ -30,6 +30,7 @@ interface TreeViewProps {
|
||||
toggleExpand: (id: string) => void;
|
||||
level?: number;
|
||||
folderFileCount: Record<string, number>;
|
||||
hasChildrenSet?: Set<string>;
|
||||
}
|
||||
|
||||
function buildFolderFileCount(pages: Page[]): Record<string, number> {
|
||||
@@ -65,6 +66,14 @@ function buildFolderFileCount(pages: Page[]): Record<string, number> {
|
||||
return counts;
|
||||
}
|
||||
|
||||
function buildHasChildrenSet(pages: Page[]): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (const page of pages) {
|
||||
if (page.parentId) result.add(page.parentId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function TreeNode({
|
||||
node,
|
||||
pages,
|
||||
@@ -72,6 +81,7 @@ function TreeNode({
|
||||
expanded,
|
||||
toggleExpand,
|
||||
folderFileCount,
|
||||
hasChildrenSet,
|
||||
}: {
|
||||
node: Page;
|
||||
pages: Page[];
|
||||
@@ -79,15 +89,19 @@ function TreeNode({
|
||||
expanded: Record<string, boolean>;
|
||||
toggleExpand: (id: string) => void;
|
||||
folderFileCount: Record<string, number>;
|
||||
hasChildrenSet: Set<string>;
|
||||
}) {
|
||||
const { activePageId, setActivePageId, addPage, deletePage } = useEditorStore();
|
||||
const activePageId = useEditorStore((s) => s.activePageId);
|
||||
const setActivePageId = useEditorStore((s) => s.setActivePageId);
|
||||
const addPage = useEditorStore((s) => s.addPage);
|
||||
const deletePage = useEditorStore((s) => s.deletePage);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { triggerImport } = useImport();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const isFolder = node.type === "folder";
|
||||
const hasChildren = pages.some((p) => p.parentId === node.id);
|
||||
const hasChildren = hasChildrenSet.has(node.id);
|
||||
const isExpanded = expanded[node.id];
|
||||
const isActive = activePageId === node.id;
|
||||
|
||||
@@ -259,14 +273,16 @@ function TreeNode({
|
||||
expanded={expanded}
|
||||
toggleExpand={toggleExpand}
|
||||
folderFileCount={folderFileCount}
|
||||
hasChildrenSet={hasChildrenSet}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TreeView({ pages, parentId, expanded, toggleExpand, level = 0, folderFileCount }: TreeViewProps) {
|
||||
export function TreeView({ pages, parentId, expanded, toggleExpand, level = 0, folderFileCount, hasChildrenSet }: TreeViewProps) {
|
||||
const computedFolderFileCount = level === 0 ? buildFolderFileCount(pages) : folderFileCount;
|
||||
const computedHasChildrenSet = level === 0 ? buildHasChildrenSet(pages) : hasChildrenSet || new Set<string>();
|
||||
const nodes = pages
|
||||
.filter((p) => p.parentId === parentId)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
@@ -285,6 +301,7 @@ export function TreeView({ pages, parentId, expanded, toggleExpand, level = 0, f
|
||||
expanded={expanded}
|
||||
toggleExpand={toggleExpand}
|
||||
folderFileCount={computedFolderFileCount}
|
||||
hasChildrenSet={computedHasChildrenSet}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+74
-8
@@ -15,6 +15,7 @@ export interface Page {
|
||||
tags?: string[];
|
||||
order?: number;
|
||||
isLocked?: boolean;
|
||||
contentLoaded?: boolean;
|
||||
}
|
||||
|
||||
export type PageSyncStatus = "saved" | "saving" | "pending" | "error";
|
||||
@@ -31,7 +32,8 @@ interface EditorState {
|
||||
isLoading: boolean;
|
||||
pageSync: Record<string, PageSyncMeta>;
|
||||
|
||||
fetchPages: () => Promise<void>;
|
||||
fetchPages: (lightweight?: boolean) => Promise<void>;
|
||||
fetchPageContent: (id: string) => Promise<void>;
|
||||
setActivePageId: (id: string | null) => void;
|
||||
addPage: (
|
||||
parentId?: string | null,
|
||||
@@ -61,6 +63,8 @@ const CONTENT_RETRY_MAX_MS = 30000;
|
||||
|
||||
const pendingContentTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const pendingContentPayloads = new Map<string, PendingContentPayload>();
|
||||
const snapshotCheckAtByPage = new Map<string, number>();
|
||||
const SNAPSHOT_CHECK_INTERVAL_MS = 5000;
|
||||
|
||||
function setPageSyncMeta(set: StoreSet, id: string, meta: PageSyncMeta): void {
|
||||
set((state) => ({
|
||||
@@ -153,13 +157,30 @@ export const useEditorStore = create<EditorState>()(
|
||||
isLoading: false,
|
||||
pageSync: {},
|
||||
|
||||
fetchPages: async () => {
|
||||
fetchPages: async (lightweight = true) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const res = await fetch("/api/pages");
|
||||
const query = lightweight ? "?lightweight=true" : "";
|
||||
const res = await fetch(`/api/pages${query}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
set({ pages: data });
|
||||
const prevById = new Map(get().pages.map((p) => [p.id, p]));
|
||||
const merged = (Array.isArray(data) ? data : []).map((page: Page) => {
|
||||
const prev = prevById.get(page.id);
|
||||
const hasLoadedContent = typeof prev?.content === "string" && (prev.content.length > 0 || prev.contentLoaded);
|
||||
if (lightweight && hasLoadedContent) {
|
||||
return {
|
||||
...page,
|
||||
content: prev?.content || "",
|
||||
contentLoaded: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...page,
|
||||
contentLoaded: lightweight ? false : true,
|
||||
};
|
||||
});
|
||||
set({ pages: merged });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch pages", e);
|
||||
@@ -168,6 +189,32 @@ export const useEditorStore = create<EditorState>()(
|
||||
}
|
||||
},
|
||||
|
||||
fetchPageContent: async (id) => {
|
||||
try {
|
||||
const existing = get().pages.find((p) => p.id === id);
|
||||
if (!existing || existing.type === "folder") return;
|
||||
if (existing.contentLoaded) return;
|
||||
|
||||
const res = await fetch(`/api/pages/${id}`);
|
||||
if (!res.ok) return;
|
||||
const page = (await res.json()) as Page;
|
||||
set((state) => ({
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === id
|
||||
? {
|
||||
...p,
|
||||
content: page.content || "",
|
||||
updatedAt: page.updatedAt || p.updatedAt,
|
||||
contentLoaded: true,
|
||||
}
|
||||
: p
|
||||
),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch page content", e);
|
||||
}
|
||||
},
|
||||
|
||||
setActivePageId: (id) => set({ activePageId: id }),
|
||||
|
||||
addPage: async (parentId = null, type = "file", initialData, order) => {
|
||||
@@ -186,12 +233,13 @@ export const useEditorStore = create<EditorState>()(
|
||||
});
|
||||
if (res.ok) {
|
||||
const newPage = await res.json();
|
||||
const hydratedPage = { ...newPage, contentLoaded: true };
|
||||
set((state) => ({
|
||||
pages: [newPage, ...state.pages],
|
||||
activePageId: newPage.id,
|
||||
pages: [hydratedPage, ...state.pages],
|
||||
activePageId: hydratedPage.id,
|
||||
pageSync: {
|
||||
...state.pageSync,
|
||||
[newPage.id]: {
|
||||
[hydratedPage.id]: {
|
||||
status: "saved",
|
||||
message: "已保存",
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
@@ -210,15 +258,33 @@ export const useEditorStore = create<EditorState>()(
|
||||
const isContentOnlyUpdate = data.content !== undefined && Object.keys(data).every((key) => key === "content");
|
||||
|
||||
if (current && (data.content !== undefined || data.title !== undefined)) {
|
||||
const now = Date.now();
|
||||
const titleChanged = data.title !== undefined && data.title !== current.title;
|
||||
const contentChanged = data.content !== undefined && data.content !== current.content;
|
||||
const lastCheckedAt = snapshotCheckAtByPage.get(current.id) ?? 0;
|
||||
const shouldCheckSnapshot = titleChanged || (contentChanged && now - lastCheckedAt >= SNAPSHOT_CHECK_INTERVAL_MS);
|
||||
|
||||
if (shouldCheckSnapshot) {
|
||||
snapshotCheckAtByPage.set(current.id, now);
|
||||
capturePageSnapshot({
|
||||
id: current.id,
|
||||
title: current.title,
|
||||
content: current.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
pages: state.pages.map((p) => (p.id === id ? { ...p, ...data, updatedAt: new Date().toISOString() } : p)),
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === id
|
||||
? {
|
||||
...p,
|
||||
...data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
contentLoaded: data.content !== undefined ? true : p.contentLoaded,
|
||||
}
|
||||
: p
|
||||
),
|
||||
}));
|
||||
|
||||
if (isContentOnlyUpdate) {
|
||||
|
||||
Reference in New Issue
Block a user