Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 50 additions & 1 deletion apps/roam/src/utils/__tests__/roamToCrossAppConverters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ vi.mock("roamjs-components/queries/getPageViewType", () => ({
}));
vi.mock("~/utils/pageToMarkdown", () => ({ toMarkdown: () => "" }));

import { nodeUidsWithTypeToCrossApp } from "~/utils/roamToCrossAppConverters";
import {
nodeSchemaToCrossApp,
nodeUidsWithTypeToCrossApp,
} from "~/utils/roamToCrossAppConverters";
import type { DiscourseNode } from "~/utils/getDiscourseNodes";

const USER_ROW = { ":db/id": 5, ":user/uid": "user-1" };

Expand Down Expand Up @@ -60,3 +64,48 @@ describe("nodeUidsWithTypeToCrossApp timestamps", () => {
expect(node.modifiedAt).toEqual(new Date(1000));
});
});

const nodeSchema = (): DiscourseNode => ({
text: "Evidence",
type: "_EVD-node",
shortcut: "e",
format: "[[EVD]] - {content}",
specification: [],
backedBy: "user",
canvasSettings: {},
});

// For the timestamp tests: what Roam holds about one node type page.
const convertSchemaPull = (pullResult: Record<string, unknown> | null) => {
(globalThis as { window: unknown }).window = {
roamAlphaAPI: {
pull: () => pullResult,
},
};
return nodeSchemaToCrossApp(nodeSchema());
};

const schemaPull = {
":create/time": 1000,
":create/user": { ":user/uid": "user-1" },
};

describe("nodeSchemaToCrossApp timestamps", () => {
it("takes the page edit time, as written when a block below it changes", () => {
const schema = convertSchemaPull({
...schemaPull,
":edit/time": 2000,
":page/edit-time": 4000,
});
expect(schema?.modifiedAt).toEqual(new Date(4000));
});

it("falls back to the create time when neither exists", () => {
const schema = convertSchemaPull(schemaPull);
expect(schema?.modifiedAt).toEqual(new Date(1000));
});

it("is null without an author, rather than a concept that cannot be inserted", () => {
expect(convertSchemaPull({ ":create/time": 1000 })).toBeNull();
});
});
11 changes: 8 additions & 3 deletions apps/roam/src/utils/roamToCrossAppConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,20 +185,25 @@ export const nodeSchemaToCrossApp = (
s: DiscourseNode,
): CrossAppNodeSchema | null => {
const relData = window.roamAlphaAPI.pull(
"[:create/time :edit/time {:create/user [:user/uid]}]",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does :page/edit-time not track both blocks+page edit time?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does. cc @maparent

"[:create/time :page/edit-time {:create/user [:user/uid]}]",
`[:block/uid "${s.type}"]`,
) as unknown as {
":create/time": number;
":edit/time": number;
":page/edit-time"?: number;
":create/user": { ":user/uid": string };
};
if (!relData) return null;
const userUid = (relData[":create/user"] ?? {})[":user/uid"];
if (!userUid) return null;
const createdTime = relData[":create/time"] || Date.now();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent operator usage creates a bug with timestamp value 0. Line 199 uses || (OR operator) while lines 203-204 use ?? (nullish coalescing). If :create/time is 0 (Unix epoch, a valid timestamp), the || operator treats it as falsy and incorrectly falls back to Date.now(), creating an incorrect creation time. This is inconsistent with the nullish coalescing used elsewhere.

Fix:

const createdTime = relData[":create/time"] ?? Date.now();

This ensures only null or undefined trigger the fallback, not the valid timestamp 0.

Suggested change
const createdTime = relData[":create/time"] || Date.now();
const createdTime = relData[":create/time"] ?? Date.now();

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

// A node type's settings live either in the page's props or in blocks below it,
// but :page/edit-time reflects both.
const pageEditTime = relData[":page/edit-time"] || createdTime;
return {
localId: s.type,
label: s.text,
authorId: userUid,
createdAt: new Date(relData[":create/time"] || Date.now()),
createdAt: new Date(createdTime),
modifiedAt: new Date(Math.max(pageEditTime, createdTime)),
};
};