Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d20d38e
ENG-2131 Review and accept imported relation types and triples in Roam
sid597 Aug 31, 2026
13352df
ENG-2131 Address pre-PR review findings
sid597 Sep 2, 2026
a09c65b
Fix imported relation matching and refresh the grammar cache
sid597 Sep 6, 2026
e44c698
Read current relation choices when opening the creation dialog
sid597 Sep 6, 2026
f497684
ENG-2131 Align imported relation actions with row content
sid597 Sep 6, 2026
b07daac
ENG-2131 Keep delete confirmation within the actions column
sid597 Sep 6, 2026
f2b15b4
ENG-2131 Refresh mounted relation creation after schema changes
sid597 Sep 6, 2026
3907bca
ENG-2131 Keep getAllRelations for the canvas callers added on main
sid597 Sep 17, 2026
72f9aa6
ENG-2131 Mock internalError in the acceptance test after the main merge
sid597 Sep 17, 2026
51a63f0
ENG-2131 Preserve shared helper imports after rebase
sid597 Sep 18, 2026
2abeb88
ENG-2131 Keep mounted canvases from reloading on schema changes
sid597 Sep 23, 2026
7576d3f
ENG-2288 Retry a failed asset copy by bumping the Roam node's edit time
maparent Sep 22, 2026
a51f9af
ENG-2202 Reconcile Roam node schema metadata between sync and publish
maparent Sep 21, 2026
2494a67
ENG-2296 Simplify the Roam import dialog and import feedback
sid597 Sep 24, 2026
b564f5c
ENG-2296 Show import results before importing relations
sid597 Sep 24, 2026
4c60e7d
ENG-2296 Explain relation import failures and scroll short dialogs
sid597 Sep 24, 2026
8a0ac13
Derive Obsidian import file names from titles Obsidian can create
isaacschaal Sep 24, 2026
44a4d30
Merge remote-tracking branch 'origin/eng-2131-review-and-accept-impor…
sid597 Sep 25, 2026
858b91e
Merge remote-tracking branch 'origin/eng-2202-reconcile-roam-node-sch…
sid597 Sep 25, 2026
ea7db06
Merge remote-tracking branch 'origin/eng-2288-retry-a-failed-asset-co…
sid597 Sep 25, 2026
db7f219
Merge remote-tracking branch 'origin/eng-2296-simplify-the-roam-impor…
sid597 Sep 25, 2026
b3796a7
Merge PR #1482 (fix/import-note-filenames) into push-pull-validation-…
sid597 Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/obsidian/src/utils/__tests__/noteFileName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import { noteFileNameFromTitle } from "~/utils/noteFileName";

describe("noteFileNameFromTitle", () => {
it("keeps a plain title", () => {
expect(noteFileNameFromTitle("CLM - sleep improves memory")).toBe(
"CLM - sleep improves memory",
);
});

it("unwraps Roam page references in the title", () => {
expect(
noteFileNameFromTitle(
"[[EVD]] - REM sleep aids recall - [[@Smith 2020]]",
),
).toBe("EVD - REM sleep aids recall - @Smith 2020");
});

it("drops the characters Obsidian rejects in file names", () => {
expect(noteFileNameFromTitle('a<b>c:d"e/f\\g|h?i*j#k^l[m]n')).toBe(
"abcdefghijklmn",
);
});

it("drops a stray bracket that is not a page reference", () => {
expect(noteFileNameFromTitle("[[unclosed - [tag] - x")).toBe(
"unclosed - tag - x",
);
});

it("drops a slash rather than making a folder", () => {
expect(noteFileNameFromTitle("Projects/Alpha")).toBe("ProjectsAlpha");
});

it("collapses whitespace left behind", () => {
expect(noteFileNameFromTitle(" #tag - x ")).toBe("tag - x");
});

it("returns an empty string when nothing survives", () => {
expect(noteFileNameFromTitle("[[]]")).toBe("");
expect(noteFileNameFromTitle("#")).toBe("");
});

it("unwraps nested and adjacent references", () => {
expect(noteFileNameFromTitle("[[a [[b]] c]]")).toBe("a b c");
expect(noteFileNameFromTitle("[[EVD]][[x]]")).toBe("EVDx");
});

it("does not treat a pipe inside a reference as an alias", () => {
expect(noteFileNameFromTitle("[[Page|alias]]")).toBe("Pagealias");
});

it("passes non-ASCII titles through", () => {
expect(
noteFileNameFromTitle("[[EVD]] - Schlaf verbessert Gedächtnis"),
).toBe("EVD - Schlaf verbessert Gedächtnis");
});
});
5 changes: 4 additions & 1 deletion apps/obsidian/src/utils/importNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from "./importedNodeContent";
import { decorateTitle } from "@repo/database/lib/decorateTitle";
import { buildSchemaRid, findLocalNodeTypeMatch } from "./schemaMatching";
import { noteFileNameFromTitle } from "./noteFileName";

type PublishedNode = {
source_local_id: string;
Expand Down Expand Up @@ -1725,7 +1726,9 @@ const importNodes = async ({
coreTitle !== undefined && localNodeType
? decorateTitle(localNodeType.format, coreTitle)
: null;
const sanitizedFileName = sanitizeFileName(decoratedTitle ?? fileName);
const sanitizedFileName =
noteFileNameFromTitle(decoratedTitle ?? fileName) ||
node.nodeInstanceId;
let finalFilePath: string;

if (existingFile) {
Expand Down
17 changes: 17 additions & 0 deletions apps/obsidian/src/utils/noteFileName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Titles arrive from other apps in their own syntax: Roam node titles carry page
// references ([[EVD]] - x - [[@Smith 2020]]) and tags. Obsidian creates a file with
// those characters in its name but cannot link to it, so references are unwrapped
// to keep the page name and the rest of the link characters (#^[]|, the set
// checkInvalidChars enforces on node type formats) are dropped with the OS set.
// A slash is dropped rather than made a folder: a Roam namespace and an Obsidian
// folder are not the same thing. Returns "" when nothing survives; the caller
// picks the fallback name.
const PAGE_REFERENCE = /\[\[([^\]]*)\]\]/g;
const REJECTED_IN_FILE_NAMES = /[<>:"/\\|?*#^[\]]/g;

export const noteFileNameFromTitle = (title: string): string =>
title
.replace(PAGE_REFERENCE, "$1")
.replace(REJECTED_IN_FILE_NAMES, "")
.replace(/\s+/g, " ")
.trim();
8 changes: 6 additions & 2 deletions apps/roam/src/components/CreateRelationDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRelationSchemaRevision } from "~/utils/relationSchemaChanges";
import React, { useState, useMemo } from "react";
import { Dialog, Classes, Label, Button, Callout } from "@blueprintjs/core";
import renderOverlay from "roamjs-components/util/renderOverlay";
Expand All @@ -8,6 +9,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
import getDiscourseRelations, {
type DiscourseRelation,
} from "~/utils/getDiscourseRelations";
import { excludeProvisionalRelationSchemas } from "~/utils/relationSchemaAcceptance";
import { createReifiedRelation } from "~/utils/createReifiedBlock";
import { getStoredRelationsEnabled } from "~/utils/storedRelations";
import findDiscourseNode from "~/utils/findDiscourseNode";
Expand Down Expand Up @@ -291,7 +293,7 @@ const prepareRelData = (
): RelWithDirection[] => {
nodeTitle = nodeTitle || getPageTitleByPageUid(targetNodeUid).trim();
const discourseNodeSchemas = getDiscourseNodes();
const relations = getDiscourseRelations();
const relations = excludeProvisionalRelationSchemas(getDiscourseRelations());
const nodeSchema = findDiscourseNode({
uid: targetNodeUid,
title: nodeTitle,
Expand Down Expand Up @@ -387,6 +389,7 @@ export const renderCreateRelationDialog = (
export const CreateRelationButton = (
props: CreateRelationDialogProps & { fill?: boolean },
): React.JSX.Element | null => {
useRelationSchemaRevision();
const { fill = false, ...relationProps } = props;
const storedRelationsEnabled = getStoredRelationsEnabled();
if (!storedRelationsEnabled) return null;
Expand All @@ -405,7 +408,8 @@ export const CreateRelationButton = (
text="Add relation"
disabled={extProps === null}
onClick={() => {
renderCreateRelationDialog(extProps);
// A schema may have been accepted since this button last rendered.
renderCreateRelationDialog(relationProps);
}}
/>
);
Expand Down
Loading
Loading