Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e7114b4
ENG-1465: Add "Use new settings store" feature flag (#811)
sid597 Feb 25, 2026
c324bb6
ENG-1455: Dual-read from old-system settings and from blockprops (#812)
sid597 Mar 1, 2026
700b590
ENG-1472: Refactor BlockPropSettingPanels to add accessor-backed defa…
sid597 Mar 1, 2026
b88f56a
Eng-1473 port non boolean personal setting consumers (#823)
sid597 Mar 1, 2026
4c908aa
ENG-1467: Port global setting consumer reads (→ getGlobalSetting) (#824)
sid597 Mar 1, 2026
47411c2
ENG-1456: Migrate personal boolean flag consumers (getSetting → getPe…
sid597 Mar 2, 2026
0bc1d22
ENG-1479: Port suggestive mode settings to dual-read (#845)
sid597 Mar 3, 2026
3b4c80a
ENG-1468: Port node and relations tree based consumers (#825)
sid597 Mar 3, 2026
f6cc8c2
ENG-1478: Port left sidebar to dual-readers + reactivity
sid597 Mar 2, 2026
5155a2c
ENG-1478: DRY merge helpers, real dual-read in settings panels, emitt…
sid597 Mar 2, 2026
686fd05
ENG-1478: refresh config tree before emitter-driven rebuild
sid597 Mar 3, 2026
eea529a
ENG-1478: extract emitter key constants, fix stale TODO
sid597 Mar 3, 2026
5925be2
ENG-1478: fix legacy adapter child.uid → child.text, fix settingKeys …
sid597 Mar 3, 2026
ee8bf56
ENG-1478: skip reload prompt when new settings store is active
sid597 Mar 3, 2026
ade239b
ENG-1478: guard getGlobalSetting/getPersonalSetting against empty keys
sid597 Mar 3, 2026
b7d26df
ENG-1469: Refactor getDiscourseRelations and getDiscourseNodes to rea…
sid597 Mar 3, 2026
a1ce922
Fix: use backedBy 'user' for initial discourse nodes in block props
sid597 Mar 4, 2026
915c0eb
Hardcode backedBy 'user' in toDiscourseNode to match legacy behavior
sid597 Mar 4, 2026
d682a86
ENG-1484: reactive settings for triggers and suggestive mode overlay
sid597 Mar 5, 2026
d280bfd
ENG-1484: use nullish coalescing for trigger fallbacks to match init …
sid597 Mar 6, 2026
2fdb3de
ENG-1484: reactive suggestive mode flag, gate reload prompts on new s…
sid597 Mar 6, 2026
bc261e0
ENG-1484: broad selector for suggestive overlay cleanup to match legacy
sid597 Mar 6, 2026
0ee94fa
ENG-1484: fix prettier formatting (root config)
sid597 Mar 6, 2026
dc1d46a
ENG-1484: drop reload prompts — reactivity is unconditional via dual-…
sid597 Mar 6, 2026
46fa9bc
Merge pull request #844 from DiscourseGraphs/eng-1478-port-both-globa…
sid597 Mar 9, 2026
6606ffa
Merge pull request #853 from DiscourseGraphs/eng-1469-refactor-getdis…
sid597 Mar 9, 2026
b5257d7
ENG-1499: Replace raw string accessor paths with shared constants fro…
sid597 Mar 9, 2026
5a462ce
Fix missed keyImageOption getter to use shared constants
sid597 Mar 9, 2026
fc58bf3
Merge pull request #875 from DiscourseGraphs/eng-1499-convert-legacy-…
sid597 Mar 10, 2026
cd16a66
ENG-1519: Add legacy-to-blockprops migration, remove backedBy from sc…
sid597 Mar 10, 2026
b97def3
Fix shouldWrite skipping after reconciliation, warn on missing person…
sid597 Mar 10, 2026
14e72c9
Add migration telemetry for legacy parse failures
sid597 Mar 10, 2026
def877f
Use backend query for node migration
sid597 Mar 10, 2026
cd2fa4c
ENG-735: Remove inline settings UI from config pages
sid597 Mar 10, 2026
f3cae70
Use Object.values instead of unused destructured key
sid597 Mar 11, 2026
991a7c9
ENG-1503: Replace getFormattedConfigTree consumers with direct helper…
sid597 Mar 5, 2026
3b67eec
ENG-1484: extract constants, remove dead defaults per review
sid597 Mar 11, 2026
bdbbbc4
Fix useConfig to call buildConfig() for dual-read merge
sid597 Mar 11, 2026
7f51464
merge migration-block-init-staging-branch
sid597 Mar 11, 2026
c44b338
prettier
sid597 Mar 11, 2026
cbf7345
Merge pull request #860 from DiscourseGraphs/eng-1503-remove-remnants…
sid597 Mar 11, 2026
028bec6
Merge migration-block-init-staging-branch into eng-735
sid597 Mar 11, 2026
6e28926
Fix import path for DISCOURSE_CONFIG_PAGE_TITLE
sid597 Mar 11, 2026
29f99fc
Merge branch 'migration-block-init-staging-branch' into eng-1519-lega…
sid597 Mar 11, 2026
f62b3f8
fix review
sid597 Mar 13, 2026
a1f1dc0
Merge pull request #876 from DiscourseGraphs/eng-1519-legacy-to-block…
sid597 Mar 13, 2026
8b23c58
Merge pull request #878 from DiscourseGraphs/eng-735-remove-ui-from-s…
sid597 Mar 13, 2026
fd332e9
Merge remote-tracking branch 'origin/migration-block-init-staging-bra…
sid597 Mar 13, 2026
4877527
Merge pull request #861 from DiscourseGraphs/eng-1484-reactive-settin…
sid597 Mar 13, 2026
19f90c0
Merge origin/main into migration-block-init-staging-branch
sid597 Mar 16, 2026
26fef42
Merge origin/main (ENG-1403, ENG-1508) into migration-block-init-stag…
sid597 Mar 16, 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
35 changes: 13 additions & 22 deletions apps/roam/src/components/DiscourseContextOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import ReactDOM from "react-dom";
import { ContextContent } from "./DiscourseContext";
import useInViewport from "react-in-viewport/dist/es/lib/useInViewport";
import normalizePageTitle from "roamjs-components/queries/normalizePageTitle";
import { USE_STORED_RELATIONS } from "~/data/userSettings";
import { getSetting } from "~/utils/extensionSettings";
import deriveDiscourseNodeAttribute from "~/utils/deriveDiscourseNodeAttribute";
import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree";
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";
import getSubTree from "roamjs-components/util/getSubTree";
import { getDiscourseNodeSetting } from "~/components/settings/utils/accessors";
import { DISCOURSE_NODE_KEYS } from "~/components/settings/utils/settingKeys";
import { getStoredRelationsEnabled } from "~/utils/storedRelations";
import nanoid from "nanoid";
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import getDiscourseContextResults from "~/utils/getDiscourseContextResults";
Expand Down Expand Up @@ -190,11 +188,10 @@ const useDiscourseContext = (uid: string, tag: string) => {
).length,
)
.reduce((acc, cur) => acc + cur, 0);
const attribute = getSettingValueFromTree({
tree: getBasicTreeByParentUid(discourseNode.type),
key: "Overlay",
defaultValue: "Overlay",
});
const attribute =
getDiscourseNodeSetting<string>(discourseNode.type, [
DISCOURSE_NODE_KEYS.overlay,
]) || "Overlay";
return deriveDiscourseNodeAttribute({
uid: uid,
attribute,
Expand All @@ -203,17 +200,11 @@ const useDiscourseContext = (uid: string, tag: string) => {
setRefs(refs);
setScore(score);

const nodeType = discourseNode.type;
const attributeNode = getSubTree({
tree: getBasicTreeByParentUid(nodeType || ""),
key: "Attributes",
});
const scoreFormula = attributeNode?.children
? getSettingValueFromTree({
tree: attributeNode.children,
key: attribute,
})
: "";
const scoreFormula =
getDiscourseNodeSetting<string>(discourseNode.type, [
DISCOURSE_NODE_KEYS.attributes,
attribute,
]) ?? "";
if (scoreFormula === "" && score !== numResults) {
internalError({
error: "DiscourseContext: Score does not match Num relations",
Expand All @@ -222,7 +213,7 @@ const useDiscourseContext = (uid: string, tag: string) => {
score,
numResults,
ignoreCache,
reified: getSetting<boolean>(USE_STORED_RELATIONS),
reified: getStoredRelationsEnabled(),
},
});
}
Expand Down
4 changes: 3 additions & 1 deletion apps/roam/src/components/DiscourseFloatingMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import { FeedbackWidget } from "./BirdEatsBugs";
import { render as renderSettings } from "~/components/settings/Settings";
import posthog from "posthog-js";
import { getPersonalSetting } from "./settings/utils/accessors";
import { PERSONAL_KEYS } from "./settings/utils/settingKeys";

type DiscourseFloatingMenuProps = {
// CSS placement class
Expand Down Expand Up @@ -128,7 +130,7 @@ export const installDiscourseFloatingMenu = (
floatingMenuAnchor.id = ANCHOR_ID;
document.getElementById("app")?.appendChild(floatingMenuAnchor);
}
if (onLoadArgs.extensionAPI.settings.get("hide-feedback-button") as boolean) {
if (getPersonalSetting<boolean>([PERSONAL_KEYS.hideFeedbackButton])) {
floatingMenuAnchor.classList.add("hidden");
}
ReactDOM.render(
Expand Down
19 changes: 13 additions & 6 deletions apps/roam/src/components/DiscourseNodeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils";
import { OnloadArgs } from "roamjs-components/types";
import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings";
import posthog from "posthog-js";
import { setPersonalSetting } from "~/components/settings/utils/accessors";
import {
getPersonalSetting,
setPersonalSetting,
} from "~/components/settings/utils/accessors";
import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys";

type Props = {
textarea?: HTMLTextAreaElement;
Expand Down Expand Up @@ -423,9 +427,12 @@ export const NodeMenuTriggerComponent = ({
const [isActive, setIsActive] = useState(false);
const [comboKey, setComboKey] = useState<IKeyCombo>(
() =>
(extensionAPI.settings.get(
"personal-node-menu-trigger",
) as IKeyCombo) || { modifiers: 0, key: "" },
getPersonalSetting<IKeyCombo>([
PERSONAL_KEYS.personalNodeMenuTrigger,
]) || {
modifiers: 0,
key: "",
},
);

const handleKeyDown = useCallback(
Expand All @@ -438,7 +445,7 @@ export const NodeMenuTriggerComponent = ({
const combo = { key: comboObj.key, modifiers: comboObj.modifiers };
setComboKey(combo);
void extensionAPI.settings.set("personal-node-menu-trigger", combo);
setPersonalSetting(["Personal node menu trigger"], combo);
setPersonalSetting([PERSONAL_KEYS.personalNodeMenuTrigger], combo);
},
[extensionAPI],
);
Expand All @@ -460,7 +467,7 @@ export const NodeMenuTriggerComponent = ({
onClick={() => {
setComboKey({ modifiers: 0, key: "" });
void extensionAPI.settings.set("personal-node-menu-trigger", "");
setPersonalSetting(["Personal node menu trigger"], "");
setPersonalSetting([PERSONAL_KEYS.personalNodeMenuTrigger], "");
}}
minimal
/>
Expand Down
11 changes: 7 additions & 4 deletions apps/roam/src/components/DiscourseNodeSearchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ import { OnloadArgs } from "roamjs-components/types";
import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes";
import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression";
import { Result } from "~/utils/types";
import { getSetting } from "~/utils/extensionSettings";
import MiniSearch from "minisearch";
import { setPersonalSetting } from "~/components/settings/utils/accessors";
import {
getPersonalSetting,
setPersonalSetting,
} from "~/components/settings/utils/accessors";
import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys";

type Props = {
textarea: HTMLTextAreaElement;
Expand Down Expand Up @@ -711,7 +714,7 @@ export const NodeSearchMenuTriggerSetting = ({
}) => {
const extensionAPI = onloadArgs.extensionAPI;
const [nodeSearchTrigger, setNodeSearchTrigger] = useState<string>(
getSetting("node-search-trigger", "@"),
getPersonalSetting<string>([PERSONAL_KEYS.nodeSearchMenuTrigger]) ?? "@",
);

const handleNodeSearchTriggerChange = (
Expand All @@ -726,7 +729,7 @@ export const NodeSearchMenuTriggerSetting = ({

setNodeSearchTrigger(trigger);
void extensionAPI.settings.set("node-search-trigger", trigger);
setPersonalSetting(["Node search menu trigger"], trigger);
setPersonalSetting([PERSONAL_KEYS.nodeSearchMenuTrigger], trigger);
};
return (
<InputGroup
Expand Down
123 changes: 106 additions & 17 deletions apps/roam/src/components/LeftSidebarView.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Inverted fold/unfold logic: isOpen state and block CRUD are backwards

In toggleFoldedState, when isOpen is true (section is currently open/expanded), the code calls setIsOpen(false) and deletes the "Folded" block — meaning it unfolds by removing the folded marker. But when isOpen is false, it calls setIsOpen(true) and creates a "Folded" block. This means the isOpen React state tracks whether the section is collapsed (folded), not whether it is open. The newFolded value at LeftSidebarView.tsx:130 is computed as !isOpen, so when isOpen=true (collapsed/folded), newFolded=false — and this is the value written to the new settings store. However, the old-system block CRUD on lines 132-148 does the opposite: when isOpen=true it deletes the Folded block (setting folded=false), and when isOpen=false it creates the Folded block (setting folded=true). So both systems agree on the final folded value.

The actual bug is that the newFolded value written to the new settings store at lines 150-166 is !isOpen, but the old-system block operations establish that isOpen=true means "currently folded" (since the Folded block exists). When the section is currently folded (isOpen=true), newFolded = !true = false → correct (we're unfolding). When the section is currently expanded (isOpen=false), newFolded = !false = true → correct (we're folding). So the value is actually correct.

However, looking at PersonalSectionItem at line 223, isOpen is initialized from !!section.settings?.folded.value, meaning isOpen=true when folded=true. This means isOpen actually represents "is folded", not "is open". The Collapse isOpen={isOpen} at line 262 will then show content when folded=true and hide content when folded=false — which is the opposite of expected behavior. The section is shown expanded when the folded flag is true, and collapsed when it's false.

(Refers to lines 223-224)

Open in Devin Review

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

Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,33 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit
import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar";
import extractRef from "roamjs-components/util/extractRef";
import {
getFormattedConfigTree,
notify,
subscribe,
} from "~/utils/discourseConfigRef";
import type {
LeftSidebarConfig,
LeftSidebarPersonalSectionConfig,
onSettingChange,
settingKeys,
} from "~/components/settings/utils/settingsEmitter";
import {
type LeftSidebarConfig,
type LeftSidebarPersonalSectionConfig,
mergeGlobalSectionWithAccessor,
mergePersonalSectionsWithAccessor,
} from "~/utils/getLeftSidebarSettings";
import discourseConfigRef, { notify } from "~/utils/discourseConfigRef";
import { getLeftSidebarSettings } from "~/utils/getLeftSidebarSettings";
import {
getGlobalSetting,
getPersonalSetting,
setGlobalSetting,
setPersonalSetting,
} from "~/components/settings/utils/accessors";
import {
PERSONAL_KEYS,
GLOBAL_KEYS,
LEFT_SIDEBAR_KEYS,
LEFT_SIDEBAR_SETTINGS_KEYS,
} from "~/components/settings/utils/settingKeys";
import type {
LeftSidebarGlobalSettings,
PersonalSection,
} from "~/components/settings/utils/zodSchema";
import { createBlock } from "roamjs-components/writes";
import deleteBlock from "roamjs-components/writes/deleteBlock";
import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid";
Expand All @@ -39,11 +58,14 @@ import { SettingsDialog } from "./settings/Settings";
import { OnloadArgs } from "roamjs-components/types";
import renderOverlay from "roamjs-components/util/renderOverlay";
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";
import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage";
import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/data/constants";
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings";
import posthog from "posthog-js";

const getCurrentLeftSidebarConfig = (): LeftSidebarConfig =>
getLeftSidebarSettings(discourseConfigRef.tree);

const parseReference = (text: string) => {
const extracted = extractRef(text);
if (text.startsWith("((") && text.endsWith("))")) {
Expand Down Expand Up @@ -95,12 +117,18 @@ const toggleFoldedState = ({
setIsOpen,
folded,
parentUid,
isGlobal,
sectionIndex,
}: {
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
folded: { uid?: string; value: boolean };
parentUid: string;
isGlobal?: boolean;
sectionIndex?: number;
}) => {
const newFolded = !isOpen;

if (isOpen) {
setIsOpen(false);
if (folded.uid) {
Expand All @@ -118,6 +146,24 @@ const toggleFoldedState = ({
folded.uid = newUid;
folded.value = true;
}

if (isGlobal) {
setGlobalSetting(
[
GLOBAL_KEYS.leftSidebar,
LEFT_SIDEBAR_KEYS.settings,
LEFT_SIDEBAR_SETTINGS_KEYS.folded,
],
newFolded,
);
} else if (sectionIndex !== undefined) {
const sections =
getPersonalSetting<PersonalSection[]>([PERSONAL_KEYS.leftSidebar]) || [];
if (sections[sectionIndex]) {
sections[sectionIndex].Settings.Folded = newFolded;
setPersonalSetting([PERSONAL_KEYS.leftSidebar], sections);
}
}
};

const SectionChildren = ({
Expand Down Expand Up @@ -160,8 +206,10 @@ const SectionChildren = ({

const PersonalSectionItem = ({
section,
sectionIndex,
}: {
section: LeftSidebarPersonalSectionConfig;
sectionIndex: number;
}) => {
const titleRef = parseReference(section.text);
const blockText = useMemo(
Expand All @@ -182,6 +230,7 @@ const PersonalSectionItem = ({
setIsOpen,
folded: section.settings.folded,
parentUid: section.settings.uid || "",
sectionIndex,
});
};

Expand Down Expand Up @@ -226,9 +275,9 @@ const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => {

return (
<div className="personal-left-sidebar-sections">
{sections.map((section) => (
{sections.map((section, index) => (
<div key={section.uid}>
<PersonalSectionItem section={section} />
<PersonalSectionItem section={section} sectionIndex={index} />
</div>
))}
</div>
Expand All @@ -253,6 +302,7 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
setIsOpen,
folded: config.settings.folded,
parentUid: config.settings.uid,
isGlobal: true,
});
}}
>
Expand All @@ -276,22 +326,61 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
);
};

// TODO(ENG-1471): Remove old-system merge when migration complete — just use accessor values directly.
// See mergeGlobalSectionWithAccessor/mergePersonalSectionsWithAccessor for why the merge exists.
const buildConfig = (): LeftSidebarConfig => {
// Read VALUES from accessor (handles flag routing + mismatch detection)
const globalValues = getGlobalSetting<LeftSidebarGlobalSettings>([
GLOBAL_KEYS.leftSidebar,
]);
const personalValues = getPersonalSetting<PersonalSection[]>([
PERSONAL_KEYS.leftSidebar,
]);

// Read UIDs from old system (needed for fold CRUD during dual-write)
const oldConfig = getCurrentLeftSidebarConfig();

return {
uid: oldConfig.uid,
favoritesMigrated: oldConfig.favoritesMigrated,
sidebarMigrated: oldConfig.sidebarMigrated,
global: mergeGlobalSectionWithAccessor(oldConfig.global, globalValues),
personal: {
uid: oldConfig.personal.uid,
sections: mergePersonalSectionsWithAccessor(
oldConfig.personal.sections,
personalValues,
),
},
allPersonalSections: oldConfig.allPersonalSections,
};
};

export const useConfig = () => {
const [config, setConfig] = useState(
() => getFormattedConfigTree().leftSidebar,
);
const [config, setConfig] = useState(() => buildConfig());
useEffect(() => {
const handleUpdate = () => {
setConfig(getFormattedConfigTree().leftSidebar);
setConfig(buildConfig());
};
const unsubscribe = subscribe(handleUpdate);
const unsubGlobal = onSettingChange(
settingKeys.globalLeftSidebar,
handleUpdate,
);
const unsubPersonal = onSettingChange(
settingKeys.personalLeftSidebar,
handleUpdate,
);
return () => {
unsubscribe();
unsubGlobal();
unsubPersonal();
};
}, []);
return { config, setConfig };
};

// TODO(ENG-1471): refreshAndNotify still needed by settings panels
// (LeftSidebarGlobalSettings, LeftSidebarPersonalSettings) for old-system CRUD.
// Remove when settings panels also read via accessors + emitter.
export const refreshAndNotify = () => {
refreshConfigTree();
notify();
Expand Down Expand Up @@ -420,7 +509,7 @@ const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
};

const migrateFavorites = async () => {
const config = getFormattedConfigTree().leftSidebar;
const config = getCurrentLeftSidebarConfig();

if (config.favoritesMigrated.value) return;

Expand Down
5 changes: 1 addition & 4 deletions apps/roam/src/components/ModifyNodeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,7 @@ const ModifyNodeDialog = ({
}

if (keyImageOption === "query-builder") {
const parentUid = resolveQueryBuilderRef({
queryRef: qbAlias,
extensionAPI,
});
const parentUid = resolveQueryBuilderRef({ queryRef: qbAlias });
const results = await runQuery({
extensionAPI,
parentUid,
Expand Down
Loading
Loading