@firedrill-tools/notion 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +402 -0
- package/firedrill/agent.target.json +17 -0
- package/firedrill/baseline.scenario.json +5 -0
- package/firedrill/bounded.scenario.json +19 -0
- package/firedrill/conformance.suite.json +23 -0
- package/firedrill/notion-bounded.drill.json +318 -0
- package/firedrill/notion-byte-budget.drill.json +116 -0
- package/firedrill/notion-mcp-aliases.drill.json +150 -0
- package/firedrill/notion-page-authoring.drill.json +254 -0
- package/firedrill/notion-rate-limited.drill.json +118 -0
- package/firedrill/notion-schema-growth.drill.json +88 -0
- package/firedrill/notion-scope-agent-only.drill.json +131 -0
- package/firedrill/notion-scope-auditor.drill.json +86 -0
- package/firedrill/notion-scope-board-bot.drill.json +128 -0
- package/firedrill/notion-scope-notes-bot.drill.json +303 -0
- package/firedrill/notion-scope-stranger.drill.json +773 -0
- package/firedrill/notion-task-triage.drill.json +277 -0
- package/firedrill/notion-trash-and-restore.drill.json +186 -0
- package/firedrill/notion-update-lost.drill.json +88 -0
- package/firedrill/notion-workspace-read.drill.json +258 -0
- package/firedrill/notion-write-unavailable.drill.json +161 -0
- package/firedrill/rate-limited.scenario.json +11 -0
- package/firedrill/tools/notion/app/assets/ATTRIBUTION.md +35 -0
- package/firedrill/tools/notion/app/assets/fonts/OFL.txt +93 -0
- package/firedrill/tools/notion/app/assets/fonts/inter-latin.woff2 +0 -0
- package/firedrill/tools/notion/app/assets/notion-wordmark.svg +1 -0
- package/firedrill/tools/notion/app/assets/notion.svg +1 -0
- package/firedrill/tools/notion/app/site/app.js +797 -0
- package/firedrill/tools/notion/app/site/assets/fonts/inter-latin.woff2 +0 -0
- package/firedrill/tools/notion/app/site/assets/notion-wordmark.svg +1 -0
- package/firedrill/tools/notion/app/site/assets/notion.svg +1 -0
- package/firedrill/tools/notion/app/site/chrome.js +104 -0
- package/firedrill/tools/notion/app/site/cover-picker.js +83 -0
- package/firedrill/tools/notion/app/site/database.js +648 -0
- package/firedrill/tools/notion/app/site/editors.js +320 -0
- package/firedrill/tools/notion/app/site/format-bar.js +97 -0
- package/firedrill/tools/notion/app/site/icons.js +131 -0
- package/firedrill/tools/notion/app/site/index.html +125 -0
- package/firedrill/tools/notion/app/site/page.js +826 -0
- package/firedrill/tools/notion/app/site/rich.js +159 -0
- package/firedrill/tools/notion/app/site/state.js +170 -0
- package/firedrill/tools/notion/app/site/styles.css +826 -0
- package/firedrill/tools/notion/app/site/ui.js +418 -0
- package/firedrill/tools/notion/behavior.mjs +1123 -0
- package/firedrill/tools/notion/lib/blocks.mjs +371 -0
- package/firedrill/tools/notion/lib/identity.mjs +123 -0
- package/firedrill/tools/notion/lib/ids.mjs +63 -0
- package/firedrill/tools/notion/lib/json-depth.mjs +26 -0
- package/firedrill/tools/notion/lib/markdown.mjs +381 -0
- package/firedrill/tools/notion/lib/properties.mjs +513 -0
- package/firedrill/tools/notion/lib/query.mjs +272 -0
- package/firedrill/tools/notion/lib/render.mjs +137 -0
- package/firedrill/tools/notion/lib/rich-text.mjs +134 -0
- package/firedrill/tools/notion/lib/size.mjs +44 -0
- package/firedrill/tools/notion/lib/state.mjs +192 -0
- package/firedrill/tools/notion/lib/wire.mjs +89 -0
- package/firedrill/tools/notion/notion.tool.json +9837 -0
- package/firedrill/update-lost.scenario.json +11 -0
- package/firedrill/world.json +7039 -0
- package/firedrill/write-unavailable.scenario.json +11 -0
- package/firedrill.json +5 -0
- package/package.json +63 -0
- package/starter.json +6482 -0
- package/test/conformance.mjs +1186 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Rendering of Notion rich text and property values into safe DOM (textContent only; links are anchors with
|
|
2
|
+
// rel="noopener" that never resolve inside the test environment).
|
|
3
|
+
import { icon } from "./icons.js";
|
|
4
|
+
import { avatar, el, propertyDate } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
export const OPTION_COLORS = ["default", "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"];
|
|
7
|
+
|
|
8
|
+
export const EMOJIS = [
|
|
9
|
+
"π", "π", "π", "π", "ποΈ", "ποΈ", "π", "π", "π", "π", "π§", "πΊοΈ", "π ", "π’", "π", "π", "π οΈ", "βοΈ", "π§", "π©",
|
|
10
|
+
"π€", "π¦Ύ", "π", "π", "π‘", "π§ͺ", "π¬", "π‘", "π°οΈ", "π¦", "π", "ποΈ", "π", "π¨", "ποΈ", "π§©", "π―", "β
", "βοΈ", "π",
|
|
11
|
+
"π
", "ποΈ", "β°", "π", "π¬", "π£", "π", "π", "π", "π°", "π·οΈ", "π", "π", "π‘οΈ", "β οΈ", "π§", "π₯", "β", "π", "β€οΈ",
|
|
12
|
+
"π", "π", "π§ ", "π", "π", "π±", "π", "β", "π", "π", "π", "π", "π₯", "π", "βοΈ", "π", "π", "π¦", "π", "π",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/** Rich text array β span with annotated runs, links and mentions. */
|
|
16
|
+
export function richText(items, { className = "rich" } = {}) {
|
|
17
|
+
const host = el("span", { class: className });
|
|
18
|
+
for (const item of Array.isArray(items) ? items : []) host.append(richItem(item));
|
|
19
|
+
return host;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function richItem(item) {
|
|
23
|
+
const annotations = item.annotations ?? {};
|
|
24
|
+
let text = item.plain_text ?? item.text?.content ?? "";
|
|
25
|
+
let node;
|
|
26
|
+
if (item.type === "mention") {
|
|
27
|
+
const mention = item.mention ?? {};
|
|
28
|
+
if (mention.type === "user") node = el("span", { class: "mention mention-user", text: `@${mention.user?.name ?? text.replace(/^@/, "") ?? "user"}` });
|
|
29
|
+
else if (mention.type === "page") node = el("span", { class: "mention mention-page" }, [icon("page", "mention-icon"), el("span", { text: text || "Untitled" })]);
|
|
30
|
+
else if (mention.type === "date") node = el("span", { class: "mention mention-date", text: `@${propertyDate(mention.date) || text}` });
|
|
31
|
+
else node = el("span", { class: "mention", text });
|
|
32
|
+
} else if (item.href || item.text?.link?.url) {
|
|
33
|
+
node = el("a", { class: "rich-link", text, attrs: { href: item.href ?? item.text.link.url, target: "_blank", rel: "noopener noreferrer" } });
|
|
34
|
+
} else {
|
|
35
|
+
node = el("span", { text });
|
|
36
|
+
}
|
|
37
|
+
const classes = [];
|
|
38
|
+
if (annotations.bold) classes.push("b");
|
|
39
|
+
if (annotations.italic) classes.push("i");
|
|
40
|
+
if (annotations.strikethrough) classes.push("s");
|
|
41
|
+
if (annotations.underline) classes.push("u");
|
|
42
|
+
if (annotations.code) classes.push("code");
|
|
43
|
+
if (classes.length > 0) node.classList.add(...classes);
|
|
44
|
+
if (annotations.color && annotations.color !== "default") node.dataset.color = annotations.color;
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function plain(items) {
|
|
49
|
+
return (Array.isArray(items) ? items : []).map((item) => item.plain_text ?? item.text?.content ?? "").join("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Plain-text content β the rich text array the API expects. */
|
|
53
|
+
export function textToRich(content) {
|
|
54
|
+
return content.length === 0 ? [] : [{ type: "text", text: { content } }];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Coloured pill for select / multi-select / status options. */
|
|
58
|
+
export function pill(option, { status = false } = {}) {
|
|
59
|
+
const element = el("span", { class: `pill ${status ? "status" : ""}`.trim(), text: option.name });
|
|
60
|
+
element.dataset.color = option.color ?? "default";
|
|
61
|
+
if (status) element.prepend(el("span", { class: "status-dot" }));
|
|
62
|
+
return element;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function checkbox(checked, { onToggle, label = "Checkbox" } = {}) {
|
|
66
|
+
const box = el("button", { class: `checkbox ${checked ? "checked" : ""}`.trim(), attrs: { type: "button", role: "checkbox", "aria-checked": String(checked), "aria-label": label } });
|
|
67
|
+
if (checked) box.append(icon("check", "checkbox-mark"));
|
|
68
|
+
if (onToggle) box.addEventListener("click", (event) => { event.stopPropagation(); onToggle(!checked); });
|
|
69
|
+
else box.disabled = true;
|
|
70
|
+
return box;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Name for a user object (partial objects have no name). */
|
|
74
|
+
export function userName(user, users) {
|
|
75
|
+
if (!user) return "";
|
|
76
|
+
if (user.name) return user.name;
|
|
77
|
+
const known = users?.get?.(user.id);
|
|
78
|
+
return known?.name ?? "Unknown user";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function person(user, users, { size = "" } = {}) {
|
|
82
|
+
const name = userName(user, users);
|
|
83
|
+
return el("span", { class: "person" }, [avatar({ id: user?.id, name }, size), el("span", { class: "person-name", text: name })]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Render one property value (Notion page property object) for a cell or the property panel.
|
|
88
|
+
* `lookup`: { users: Map, pages: Map<id, {title, icon}> } for people and relation values.
|
|
89
|
+
*/
|
|
90
|
+
export function propertyValue(property, lookup = {}) {
|
|
91
|
+
const type = property.type;
|
|
92
|
+
const value = property[type];
|
|
93
|
+
const host = el("span", { class: `prop-value type-${type}` });
|
|
94
|
+
switch (type) {
|
|
95
|
+
case "title":
|
|
96
|
+
case "rich_text":
|
|
97
|
+
host.append(richText(value));
|
|
98
|
+
break;
|
|
99
|
+
case "number":
|
|
100
|
+
host.textContent = value === null || value === undefined ? "" : String(value);
|
|
101
|
+
break;
|
|
102
|
+
case "select":
|
|
103
|
+
if (value) host.append(pill(value));
|
|
104
|
+
break;
|
|
105
|
+
case "status":
|
|
106
|
+
if (value) host.append(pill(value, { status: true }));
|
|
107
|
+
break;
|
|
108
|
+
case "multi_select":
|
|
109
|
+
for (const option of value ?? []) host.append(pill(option));
|
|
110
|
+
break;
|
|
111
|
+
case "date":
|
|
112
|
+
host.textContent = propertyDate(value);
|
|
113
|
+
break;
|
|
114
|
+
case "checkbox":
|
|
115
|
+
host.append(checkbox(value === true));
|
|
116
|
+
break;
|
|
117
|
+
case "url":
|
|
118
|
+
if (value) host.append(el("a", { class: "cell-link", text: value, attrs: { href: value, target: "_blank", rel: "noopener noreferrer" } }));
|
|
119
|
+
break;
|
|
120
|
+
case "email":
|
|
121
|
+
if (value) host.append(el("a", { class: "cell-link", text: value, attrs: { href: `mailto:${value}` } }));
|
|
122
|
+
break;
|
|
123
|
+
case "phone_number":
|
|
124
|
+
host.textContent = value ?? "";
|
|
125
|
+
break;
|
|
126
|
+
case "people":
|
|
127
|
+
for (const user of value ?? []) host.append(person(user, lookup.users));
|
|
128
|
+
break;
|
|
129
|
+
case "created_by":
|
|
130
|
+
case "last_edited_by":
|
|
131
|
+
if (value) host.append(person(value, lookup.users));
|
|
132
|
+
break;
|
|
133
|
+
case "relation":
|
|
134
|
+
for (const ref of value ?? []) {
|
|
135
|
+
const target = lookup.pages?.get(ref.id);
|
|
136
|
+
host.append(el("span", { class: "relation-chip" }, [target?.icon ? el("span", { class: "relation-icon", text: target.icon }) : icon("page", "relation-icon"), el("span", { text: target?.title ?? "Untitled" })]));
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case "created_time":
|
|
140
|
+
case "last_edited_time":
|
|
141
|
+
host.textContent = value ? propertyDate({ start: value }) : "";
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
host.textContent = typeof value === "string" ? value : "";
|
|
145
|
+
}
|
|
146
|
+
return host;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Emoji string of an icon object, or undefined for none / external images (never loaded here). */
|
|
150
|
+
export function emojiOf(iconObject) {
|
|
151
|
+
return iconObject && iconObject.type === "emoji" ? iconObject.emoji : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Page or database icon node: the emoji, or the product's grey document / database glyph. */
|
|
155
|
+
export function objectIcon(iconObject, kind = "page", className = "") {
|
|
156
|
+
const emoji = emojiOf(iconObject);
|
|
157
|
+
if (emoji !== undefined) return el("span", { class: `obj-icon emoji ${className}`.trim(), text: emoji, attrs: { "aria-hidden": "true" } });
|
|
158
|
+
return icon(kind === "database" ? "database" : "page", `obj-icon glyph ${className}`.trim());
|
|
159
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// App-wide state and the caches every screen shares (workspace context, users, the page/database index used by
|
|
2
|
+
// the sidebar, breadcrumbs and relation chips). Caches are refilled from the Tool on every world revision change.
|
|
3
|
+
import { call, ToolError } from "./ui.js";
|
|
4
|
+
import { emojiOf, plain } from "./rich.js";
|
|
5
|
+
|
|
6
|
+
export const state = {
|
|
7
|
+
context: undefined, // workspace.context value (workspace, user, integration, now, limits)
|
|
8
|
+
nowMs: undefined,
|
|
9
|
+
users: new Map(), // id β user object (persons and bots) when the integration may list users
|
|
10
|
+
usersDenied: false,
|
|
11
|
+
usersTruncated: false,
|
|
12
|
+
indexTruncated: false,
|
|
13
|
+
pages: new Map(), // id β { id, title, icon, parent, kind: "page", lastEdited, inTrash }
|
|
14
|
+
databases: new Map(), // database id β { id, title, icon, parent, kind: "database", dataSourceId, isInline, schema }
|
|
15
|
+
dataSources: new Map(), // data source id β data source object
|
|
16
|
+
indexError: undefined,
|
|
17
|
+
route: { name: "home" },
|
|
18
|
+
favorites: new Set(),
|
|
19
|
+
tick: 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const canWrite = (level = "read_update") => {
|
|
23
|
+
const content = state.context?.integration?.capabilities?.content ?? "none";
|
|
24
|
+
const rank = { none: 0, read: 1, read_update: 2, read_update_insert: 3 };
|
|
25
|
+
return rank[content] >= rank[level];
|
|
26
|
+
};
|
|
27
|
+
export const canComment = (level = "read") => {
|
|
28
|
+
const comments = state.context?.integration?.capabilities?.comments ?? "none";
|
|
29
|
+
const rank = { none: 0, read: 1, read_insert: 2 };
|
|
30
|
+
return rank[comments] >= rank[level];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function pageTitle(page) {
|
|
34
|
+
const property = Object.values(page.properties ?? {}).find((entry) => entry.type === "title");
|
|
35
|
+
const title = property ? plain(property.title) : "";
|
|
36
|
+
return title.length > 0 ? title : "Untitled";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function titleOfRich(items) {
|
|
40
|
+
const title = plain(items);
|
|
41
|
+
return title.length > 0 ? title : "Untitled";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Read the workspace context (virtual time, identity). */
|
|
45
|
+
export async function loadContext() {
|
|
46
|
+
state.context = await call("workspace.context", {});
|
|
47
|
+
state.nowMs = Date.parse(state.context.now);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const USER_PAGES = 20;
|
|
51
|
+
const INDEX_PAGES = 200;
|
|
52
|
+
|
|
53
|
+
/** All users when the integration has the user-information capability; otherwise the caller's own identity only. */
|
|
54
|
+
export async function loadUsers() {
|
|
55
|
+
const users = new Map();
|
|
56
|
+
try {
|
|
57
|
+
let cursor;
|
|
58
|
+
let more = true;
|
|
59
|
+
// Pages end early by bytes as well as by count, so the guard counts requests, and tripping it is surfaced.
|
|
60
|
+
for (let guard = 0; guard < USER_PAGES && more; guard += 1) {
|
|
61
|
+
const page = await call("users.list", cursor ? { start_cursor: cursor, page_size: 100 } : { page_size: 100 });
|
|
62
|
+
for (const user of page.results) users.set(user.id, user);
|
|
63
|
+
more = page.has_more === true;
|
|
64
|
+
cursor = page.next_cursor;
|
|
65
|
+
}
|
|
66
|
+
state.usersTruncated = more;
|
|
67
|
+
state.usersDenied = false;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!(error instanceof ToolError) || !(error.is("RESTRICTED_RESOURCE") || error.denied)) throw error;
|
|
70
|
+
state.usersDenied = true;
|
|
71
|
+
}
|
|
72
|
+
if (state.context?.user) users.set(state.context.user.id, { ...state.context.user, object: "user" });
|
|
73
|
+
state.users = users;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Every page and data source visible to the integration (search without a query, paginated). */
|
|
77
|
+
export async function loadIndex() {
|
|
78
|
+
const pages = new Map();
|
|
79
|
+
const databases = new Map();
|
|
80
|
+
const dataSources = new Map();
|
|
81
|
+
let truncated = false;
|
|
82
|
+
const collect = async (value, sink) => {
|
|
83
|
+
let cursor;
|
|
84
|
+
for (let guard = 0; ; guard += 1) {
|
|
85
|
+
if (guard === INDEX_PAGES) {
|
|
86
|
+
truncated = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const page = await call("search", { filter: { property: "object", value }, sort: { timestamp: "last_edited_time", direction: "descending" }, page_size: 100, ...(cursor ? { start_cursor: cursor } : {}) });
|
|
90
|
+
for (const item of page.results) sink(item);
|
|
91
|
+
if (!page.has_more) break;
|
|
92
|
+
cursor = page.next_cursor;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await collect("page", (page) => pages.set(page.id, { id: page.id, kind: "page", title: pageTitle(page), icon: emojiOf(page.icon), iconObject: page.icon, parent: page.parent, lastEdited: page.last_edited_time, created: page.created_time, inTrash: page.in_trash, raw: page }));
|
|
97
|
+
await collect("data_source", (source) => {
|
|
98
|
+
dataSources.set(source.id, source);
|
|
99
|
+
const databaseId = source.parent?.database_id ?? source.id;
|
|
100
|
+
databases.set(databaseId, { id: databaseId, kind: "database", title: titleOfRich(source.title), icon: emojiOf(source.icon), iconObject: source.icon, parent: source.database_parent ?? { type: "workspace", workspace: true }, dataSourceId: source.id, isInline: source.is_inline === true, lastEdited: source.last_edited_time, schema: source.properties });
|
|
101
|
+
});
|
|
102
|
+
state.indexError = undefined;
|
|
103
|
+
state.indexTruncated = truncated;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
state.indexError = error;
|
|
106
|
+
}
|
|
107
|
+
state.pages = pages;
|
|
108
|
+
state.databases = databases;
|
|
109
|
+
state.dataSources = dataSources;
|
|
110
|
+
state.tick += 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Lookup maps for property rendering (people names, relation titles). */
|
|
114
|
+
export function lookup() {
|
|
115
|
+
const pageLookup = new Map();
|
|
116
|
+
for (const page of state.pages.values()) pageLookup.set(page.id, { title: page.title, icon: page.icon });
|
|
117
|
+
return { users: state.users, pages: pageLookup };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Parent chain (nearest first) of a page or database node from the index, for breadcrumbs. */
|
|
121
|
+
export function ancestors(node) {
|
|
122
|
+
const chain = [];
|
|
123
|
+
let parent = node?.parent;
|
|
124
|
+
for (let depth = 0; depth < 32 && parent; depth += 1) {
|
|
125
|
+
let next;
|
|
126
|
+
if (parent.type === "page_id") next = state.pages.get(parent.page_id);
|
|
127
|
+
else if (parent.type === "data_source_id") {
|
|
128
|
+
const source = state.dataSources.get(parent.data_source_id);
|
|
129
|
+
next = source ? state.databases.get(source.parent?.database_id ?? parent.database_id) : state.databases.get(parent.database_id);
|
|
130
|
+
} else if (parent.type === "database_id") next = state.databases.get(parent.database_id);
|
|
131
|
+
if (!next) break;
|
|
132
|
+
chain.push(next);
|
|
133
|
+
parent = next.parent;
|
|
134
|
+
}
|
|
135
|
+
return chain;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------------------------
|
|
139
|
+
// Routing (hash based, so the app works from any served path)
|
|
140
|
+
// ---------------------------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
export function parseRoute(hash) {
|
|
143
|
+
const raw = (hash || "#/home").replace(/^#\/?/, "");
|
|
144
|
+
const [path, query = ""] = raw.split("?");
|
|
145
|
+
const params = new URLSearchParams(query);
|
|
146
|
+
const parts = path.split("/").filter(Boolean);
|
|
147
|
+
if (parts[0] === "page" && parts[1]) return { name: "page", id: parts[1], peek: params.get("peek") ?? undefined };
|
|
148
|
+
if (parts[0] === "database" && parts[1]) return { name: "database", id: parts[1], view: parts[2] ?? "table", peek: params.get("peek") ?? undefined };
|
|
149
|
+
return { name: "home" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function routeHash(route) {
|
|
153
|
+
let hash;
|
|
154
|
+
if (route.name === "page") hash = `#/page/${route.id}`;
|
|
155
|
+
else if (route.name === "database") hash = `#/database/${route.id}/${route.view ?? "table"}`;
|
|
156
|
+
else hash = "#/home";
|
|
157
|
+
if (route.peek) hash += `?peek=${encodeURIComponent(route.peek)}`;
|
|
158
|
+
return hash;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function navigate(route) {
|
|
162
|
+
const hash = routeHash(route);
|
|
163
|
+
if (location.hash === hash) window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
164
|
+
else location.hash = hash;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Route to open any index node (page β page view, database β its table). */
|
|
168
|
+
export function routeFor(node) {
|
|
169
|
+
return node.kind === "database" ? { name: "database", id: node.id, view: "table" } : { name: "page", id: node.id };
|
|
170
|
+
}
|