Skip to content
Merged
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
106 changes: 106 additions & 0 deletions src/components/QuickSwitcherSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FormGroup,
InputGroup,
Tag,
TextArea,
} from "@blueprintjs/core";
import React, { useMemo, useState } from "react";
import PageInput from "roamjs-components/components/PageInput";
Expand All @@ -19,6 +20,7 @@ import {
keyboardEventToShortcut,
moveBookmarkByOffset,
normalizeShortcut,
parsePageUidFromUrl,
shortcutHasModifier,
} from "~/utils/quickSwitcher";

Expand All @@ -45,6 +47,11 @@ const showToast = ({
});
};

const isPageUrlInput = ({ entry }: { entry: string }): boolean =>
/^https?:\/\//i.test(entry) ||
entry.startsWith("#/") ||
entry.startsWith("/#/");

export const createQuickSwitcherSettingsComponent = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Component state initialized from closure-captured initialBookmarks won't reflect external updates

The QuickSwitcherSettings component initializes bookmarks via useState(initialBookmarks) where initialBookmarks is captured by the createQuickSwitcherSettingsComponent closure (src/components/QuickSwitcherSettings.tsx:55). If bookmarks are modified externally (e.g. via keyboard shortcut registration in src/quickSwitcher.tsx:242-247), this component won't see those changes until re-mounted. This is a pre-existing pattern not introduced by this PR, but the bulk import feature makes it more likely that users open the settings dialog multiple times in one session, increasing the chance of stale state.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

initialBookmarks,
isMac,
Expand All @@ -56,6 +63,7 @@ export const createQuickSwitcherSettingsComponent = ({
const [isManageDialogOpen, setIsManageDialogOpen] = useState(false);
const [pageTitle, setPageTitle] = useState("");
const [shortcut, setShortcut] = useState("");
const [bulkPages, setBulkPages] = useState("");

const shortcutLabel = useMemo(
() =>
Expand All @@ -82,9 +90,14 @@ export const createQuickSwitcherSettingsComponent = ({
setShortcut("");
};

const clearBulkPages = (): void => {
setBulkPages("");
};

const closeManageDialog = (): void => {
setIsManageDialogOpen(false);
clearForm();
clearBulkPages();
};

const onShortcutKeyDown = (
Expand Down Expand Up @@ -200,6 +213,73 @@ export const createQuickSwitcherSettingsComponent = ({
});
};

const addBulkPages = (): void => {
const entries = bulkPages
.split(/\r?\n/)
.map((entry) => entry.trim())
.filter(Boolean);
if (!entries.length) {
showToast({
content: "Add at least one page title or URL",
intent: "warning",
});
return;
}

const existingPageUids = new Set(
bookmarks
.map((bookmark) => bookmark.pageUid)
.filter((uid): uid is string => Boolean(uid)),
);
const nextBookmarks = [...bookmarks];
let addedCount = 0;
let skippedCount = 0;

entries.forEach((entry) => {
const pageUid = isPageUrlInput({ entry })
? parsePageUidFromUrl({ url: entry })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

parsePageUidFromUrl can throw a URIError on malformed percent-encoded URLs (for example https://roamresearch.com/#/app/graph/page/%). Because this call is unguarded, one bad line aborts addBulkPages entirely instead of skipping that entry.

Suggested fix: make URL parsing fail-safe here (or in parsePageUidFromUrl) by catching decode/parsing errors and treating invalid lines as skipped entries.

: getPageUidByPageTitle(entry);
const title = pageUid ? getPageTitleByPageUid(pageUid) : "";
if (!pageUid || !title || existingPageUids.has(pageUid)) {
skippedCount += 1;
return;
}

const url = buildRoamPageUrl({ pageUid });
if (!url) {
skippedCount += 1;
return;
}

existingPageUids.add(pageUid);
addedCount += 1;
nextBookmarks.push({
id: createBookmarkId(),
title,
pageUid,
url,
shortcut: null,
});
});

if (!addedCount) {
showToast({
content: "No new pages were added",
intent: "warning",
});
return;
}

setAndPersistBookmarks({ nextBookmarks });
clearBulkPages();
showToast({
content: `Added ${addedCount} ${addedCount === 1 ? "page" : "pages"}${
skippedCount ? `, skipped ${skippedCount}` : ""
}`,
intent: "success",
});
};

return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-end">
Expand Down Expand Up @@ -281,6 +361,32 @@ export const createQuickSwitcherSettingsComponent = ({
<Button minimal onClick={clearForm} text="Clear" />
</div>

<FormGroup
helperText="One existing page title or Roam page URL per line."
label="Bulk Add Pages"
>
<TextArea
fill
growVertically
onChange={(
event: React.ChangeEvent<HTMLTextAreaElement>,
): void => setBulkPages(event.target.value)}
placeholder="Project Home&#10;https://roamresearch.com/#/app/graph/page/abc123"
rows={4}
value={bulkPages}
/>
</FormGroup>

<div className="flex flex-wrap gap-2">
<Button
disabled={!bulkPages.trim()}
icon="multi-select"
onClick={addBulkPages}
text="Add Pages"
/>
<Button minimal onClick={clearBulkPages} text="Clear Bulk" />
</div>

<div className="flex flex-col gap-2">
{bookmarks.length ? (
bookmarks.map((bookmark, index) => (
Expand Down
Loading