Skip to content

Commit 82eec26

Browse files
Fix subpage title renaming
Preserve nested page paths when renaming visible page titles, allow the Tauri filesystem rename command, suppress rename-only page transition animation, and surface filesystem rename errors in the page header.
1 parent 1a159a7 commit 82eec26

4 files changed

Lines changed: 106 additions & 25 deletions

File tree

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
{ "path": "$HOME/Library/Application Support/com.philo.dev/**" }
4343
]
4444
},
45+
{
46+
"identifier": "fs:allow-rename",
47+
"allow": [
48+
{ "path": "$APPDATA/**" },
49+
{ "path": "$HOME/Library/Application Support/com.philo.dev/**" }
50+
]
51+
},
4552
{
4653
"identifier": "fs:allow-exists",
4754
"allow": [

apps/desktop/src/components/layout/AppLayout.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ import {
6363
buildPageLinkTarget,
6464
getJournalDir,
6565
getNotePath,
66+
getPageDisplayTitle,
6667
getPagePath,
6768
getPagesDir,
6869
initJournalScope,
6970
parseDateFromNoteLinkTarget,
71+
parsePageTitleFromLinkTarget,
7072
parsePageTitleFromPath,
7173
sanitizePageTitle,
7274
} from "../../services/paths";
@@ -1357,10 +1359,11 @@ function PageView({
13571359
|| resolvedPage.linkKind === "github_issue"
13581360
|| resolvedPage.linkKind === "github_commit"
13591361
);
1362+
const pageEditableTitle = resolvedPage ? getPageDisplayTitle(resolvedPage.title,) : getPageDisplayTitle(title,);
13601363
const pageHeading = resolvedPage
1361-
? (pageIsUrlSummary ? resolvedPage.linkTitle ?? resolvedPage.title : resolvedPage.title)
1362-
: title;
1363-
const canEditTitle = !!resolvedPage && pageHeading === resolvedPage.title && !!onRenameTitle;
1364+
? (pageIsUrlSummary ? resolvedPage.linkTitle ?? pageEditableTitle : pageEditableTitle)
1365+
: pageEditableTitle;
1366+
const canEditTitle = !!resolvedPage && pageHeading === pageEditableTitle && !!onRenameTitle;
13641367
const titleInputWidthCh = Math.max(draftTitle.length, 1,) + 1;
13651368
const meetingLocation = resolvedPage?.type === "meeting" ? resolvedPage.location?.trim() ?? "" : "";
13661369
const summaryUpdatedAt = formatSummaryUpdatedAt(resolvedPage?.summaryUpdatedAt ?? null,);
@@ -1390,25 +1393,27 @@ function PageView({
13901393
setIsEditingTitle(false,);
13911394

13921395
if (!nextTitle) {
1393-
setDraftTitle(resolvedPage.title,);
1396+
setDraftTitle(pageEditableTitle,);
13941397
return;
13951398
}
13961399

1397-
if (nextTitle === resolvedPage.title) {
1398-
setDraftTitle(nextTitle,);
1400+
if (nextTitle === pageEditableTitle || nextTitle === resolvedPage.title) {
1401+
setDraftTitle(pageEditableTitle,);
13991402
setTitleEditError(null,);
14001403
return;
14011404
}
14021405

14031406
try {
14041407
setTitleEditError(null,);
14051408
const renamedPage = await onRenameTitle(resolvedPage, nextTitle,);
1406-
setDraftTitle(renamedPage?.title ?? nextTitle,);
1409+
setDraftTitle(getPageDisplayTitle(renamedPage?.title ?? nextTitle,),);
14071410
} catch (error) {
1408-
setDraftTitle(resolvedPage.title,);
1409-
setTitleEditError(error instanceof Error ? error.message : "Could not rename page.",);
1411+
setDraftTitle(pageEditableTitle,);
1412+
setTitleEditError(
1413+
error instanceof Error ? error.message : typeof error === "string" ? error : "Could not rename page.",
1414+
);
14101415
}
1411-
}, [canEditTitle, draftTitle, onRenameTitle, resolvedPage,],);
1416+
}, [canEditTitle, draftTitle, onRenameTitle, pageEditableTitle, resolvedPage,],);
14121417

14131418
if (!resolvedPage) {
14141419
return (
@@ -1456,7 +1461,7 @@ function PageView({
14561461
onClick={() => {
14571462
if (!canEditTitle) return;
14581463
setTitleEditError(null,);
1459-
setDraftTitle(resolvedPage.title,);
1464+
setDraftTitle(pageEditableTitle,);
14601465
setIsEditingTitle(true,);
14611466
}}
14621467
onContextMenu={(event,) => {
@@ -1639,6 +1644,7 @@ export default function AppLayout() {
16391644
const searchNavigationModeRef = useRef<"mouse" | "keyboard">("mouse",);
16401645
const hasMountedViewRef = useRef(false,);
16411646
const nextViewAnimationDirectionRef = useRef<"forward" | "backward" | null>(null,);
1647+
const skipNextViewAnimationRef = useRef(false,);
16421648
const viewAnimationFrameRef = useRef<number | null>(null,);
16431649
const viewAnimationResetRef = useRef<number | null>(null,);
16441650
const currentView = viewState.history[viewState.index] ?? { kind: "home", };
@@ -1750,6 +1756,17 @@ export default function AppLayout() {
17501756
return;
17511757
}
17521758

1759+
if (skipNextViewAnimationRef.current) {
1760+
skipNextViewAnimationRef.current = false;
1761+
nextViewAnimationDirectionRef.current = null;
1762+
setViewTransitionStyle({
1763+
opacity: 1,
1764+
transform: "translateX(0px)",
1765+
transition: "none",
1766+
},);
1767+
return;
1768+
}
1769+
17531770
if (typeof window === "undefined" || window.matchMedia("(prefers-reduced-motion: reduce)",).matches) {
17541771
setViewTransitionStyle({
17551772
opacity: 1,
@@ -2603,7 +2620,7 @@ export default function AppLayout() {
26032620
closeGlobalSearch();
26042621

26052622
if (result.kind === "page") {
2606-
const title = parsePageTitleFromPath(result.path,);
2623+
const title = parsePageTitleFromLinkTarget(result.relativePath,) ?? parsePageTitleFromPath(result.path,);
26072624
if (title) {
26082625
trackEvent("search_result_opened", {
26092626
kind: result.kind,
@@ -3096,6 +3113,7 @@ export default function AppLayout() {
30963113
currentPageRef.current = renamedPage;
30973114
setActivePage(renamedPage,);
30983115
setPagesRevision((value,) => value + 1);
3116+
skipNextViewAnimationRef.current = true;
30993117
setViewState((current,) => ({
31003118
...current,
31013119
history: current.history.map((view,) => (

apps/desktop/src/services/paths.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,40 @@ function decodePathTarget(target: string,): string {
172172
}
173173
}
174174

175-
const INVALID_PAGE_TITLE_RE = /[\/\\\u0000-\u001F]+/g;
175+
const INVALID_PAGE_TITLE_SEGMENT_RE = /[\\\u0000-\u001F]+/g;
176176

177-
export function sanitizePageTitle(title: string,): string {
178-
return title
179-
.replace(INVALID_PAGE_TITLE_RE, " ",)
177+
function sanitizePageTitleSegment(segment: string,): string {
178+
return segment
179+
.replace(INVALID_PAGE_TITLE_SEGMENT_RE, " ",)
180180
.replace(/\s+/g, " ",)
181181
.replace(/^\.+|\.+$/g, "",)
182182
.trim();
183183
}
184184

185+
export function sanitizePageTitle(title: string,): string {
186+
return title
187+
.replace(/\\/g, "/",)
188+
.split("/",)
189+
.map((segment,) => sanitizePageTitleSegment(segment,))
190+
.filter(Boolean,)
191+
.join("/",);
192+
}
193+
194+
export function getPageDisplayTitle(title: string,): string {
195+
const normalizedTitle = sanitizePageTitle(title,);
196+
return normalizedTitle.split("/",).pop() ?? normalizedTitle;
197+
}
198+
199+
export function replacePageTitleBasename(currentTitle: string, nextTitle: string,): string {
200+
const normalizedNextTitle = sanitizePageTitle(nextTitle,);
201+
if (!normalizedNextTitle) return "";
202+
if (normalizedNextTitle.includes("/",)) return normalizedNextTitle;
203+
204+
const currentSegments = sanitizePageTitle(currentTitle,).split("/",).filter(Boolean,);
205+
const parentTitle = currentSegments.slice(0, -1,).join("/",);
206+
return parentTitle ? `${parentTitle}/${normalizedNextTitle}` : normalizedNextTitle;
207+
}
208+
185209
function joinNoteLinkSegments(parts: string[],): string {
186210
return parts
187211
.map((part,) => part.trim().replace(/^\/+|\/+$/g, "",))
@@ -291,7 +315,12 @@ export async function getPagePath(title: string,): Promise<string> {
291315
}
292316

293317
const pagesDir = await getPagesDir();
294-
return await join(pagesDir, `${normalizedTitle}.md`,);
318+
const segments = normalizedTitle.split("/",);
319+
const filename = segments.pop();
320+
if (!filename) {
321+
throw new Error("Page title is required.",);
322+
}
323+
return await join(pagesDir, ...segments, `${filename}.md`,);
295324
}
296325

297326
export function parsePageTitleFromPath(path: string,): string | null {
@@ -315,7 +344,17 @@ export function buildPageMarkdownHref(title: string,): string {
315344
throw new Error("Page title is required.",);
316345
}
317346

318-
return `pages/${encodeURIComponent(normalizedTitle,)}.md`;
347+
const segments = normalizedTitle.split("/",);
348+
const filename = segments.pop();
349+
if (!filename) {
350+
throw new Error("Page title is required.",);
351+
}
352+
353+
return [
354+
"pages",
355+
...segments.map((segment,) => encodeURIComponent(segment,)),
356+
`${encodeURIComponent(filename,)}.md`,
357+
].join("/",);
319358
}
320359

321360
export function isExplicitPageLinkTarget(target: string,): boolean {
@@ -347,10 +386,14 @@ export function parsePageTitleFromLinkTarget(target: string,): string | null {
347386

348387
const withoutExtension = stripMdExtension(decoded,);
349388
const withoutPagesPrefix = withoutExtension.replace(/^pages\//i, "",);
350-
if (withoutPagesPrefix.includes("/",) || withoutPagesPrefix.includes("\\",)) {
389+
const rawSegments = withoutPagesPrefix.split(/[\\/]+/,).map((segment,) => segment.trim()).filter(Boolean,);
390+
if (rawSegments.some((segment,) => segment === "." || segment === "..")) {
351391
return null;
352392
}
353-
const normalizedTitle = sanitizePageTitle(withoutPagesPrefix,);
393+
const normalizedTitle = rawSegments
394+
.map((segment,) => sanitizePageTitleSegment(segment,))
395+
.filter(Boolean,)
396+
.join("/",);
354397
return normalizedTitle || null;
355398
}
356399

apps/desktop/src/services/storage.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { invoke, } from "@tauri-apps/api/core";
2-
import { join, } from "@tauri-apps/api/path";
3-
import { exists, readDir, rename, } from "@tauri-apps/plugin-fs";
2+
import { dirname, join, } from "@tauri-apps/api/path";
3+
import { exists, mkdir, readDir, rename, } from "@tauri-apps/plugin-fs";
44
import { EMPTY_DOC, json2md, md2json, parseJsonContent, } from "../lib/markdown";
55
import {
66
AttachedPage,
@@ -37,6 +37,7 @@ import {
3737
parseDateFromNoteLinkTarget,
3838
parsePageTitleFromLinkTarget,
3939
parsePageTitleFromPath,
40+
replacePageTitleBasename,
4041
sanitizePageTitle,
4142
} from "./paths";
4243
import {
@@ -784,7 +785,15 @@ export async function loadPage(title: string,): Promise<PageNote | null> {
784785
}
785786

786787
export async function loadPageByPath(path: string,): Promise<PageNote | null> {
787-
const title = parsePageTitleFromPath(path,);
788+
const pagesDir = await getPagesDir().catch(() => null);
789+
const normalizedPath = path.replace(/\\/g, "/",);
790+
const normalizedPagesDir = pagesDir?.replace(/\\/g, "/",).replace(/\/+$/, "",);
791+
const relativePath = normalizedPagesDir && normalizedPath.startsWith(`${normalizedPagesDir}/`,)
792+
? normalizedPath.slice(normalizedPagesDir.length + 1,)
793+
: null;
794+
const title = relativePath
795+
? parsePageTitleFromLinkTarget(relativePath,)
796+
: parsePageTitleFromPath(path,);
788797
if (!title) return null;
789798
return await loadPage(title,);
790799
}
@@ -799,7 +808,7 @@ export async function savePage(page: PageNote,): Promise<void> {
799808

800809
export async function renamePage(page: PageNote, nextTitle: string,): Promise<PageNote> {
801810
const currentTitle = sanitizePageTitle(page.title,);
802-
const normalizedNextTitle = sanitizePageTitle(nextTitle,);
811+
const normalizedNextTitle = replacePageTitleBasename(currentTitle, nextTitle,);
803812
if (!normalizedNextTitle) {
804813
throw new Error("Page title is required.",);
805814
}
@@ -817,6 +826,10 @@ export async function renamePage(page: PageNote, nextTitle: string,): Promise<Pa
817826
}
818827

819828
if (currentPath !== nextPath && await exists(currentPath,)) {
829+
const nextDir = await dirname(nextPath,);
830+
if (!await exists(nextDir,)) {
831+
await mkdir(nextDir, { recursive: true, },);
832+
}
820833
await rename(currentPath, nextPath,);
821834
}
822835

@@ -856,7 +869,7 @@ export async function createAttachedPage({
856869
}: {
857870
title: string;
858871
},): Promise<PageNote> {
859-
const normalizedTitle = sanitizePageTitle(title.replace(/\.md$/i, "",),);
872+
const normalizedTitle = parsePageTitleFromLinkTarget(title,) ?? sanitizePageTitle(title.replace(/\.md$/i, "",),);
860873
if (!normalizedTitle) {
861874
throw new Error("Page title is required.",);
862875
}

0 commit comments

Comments
 (0)