diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx
index 8394ddb9b..a5c179142 100644
--- a/apps/roam/src/components/LeftSidebarView.tsx
+++ b/apps/roam/src/components/LeftSidebarView.tsx
@@ -430,4 +430,15 @@ export const mountLeftSidebar = (
ReactDOM.render(, root);
};
+export const unmountLeftSidebar = (wrapper: HTMLElement): void => {
+ if (!wrapper) return;
+ const id = "dg-left-sidebar-root";
+ const root = wrapper.querySelector(`#${id}`) as HTMLDivElement;
+ if (root) {
+ ReactDOM.unmountComponentAtNode(root);
+ root.remove();
+ }
+ wrapper.style.padding = "";
+};
+
export default LeftSidebarView;
diff --git a/apps/roam/src/components/settings/BlockPropFlagPanel.tsx b/apps/roam/src/components/settings/BlockPropFlagPanel.tsx
new file mode 100644
index 000000000..ceddc74e6
--- /dev/null
+++ b/apps/roam/src/components/settings/BlockPropFlagPanel.tsx
@@ -0,0 +1,39 @@
+import { featureFlagEnabled } from "~/utils/featureFlags";
+import { type FeatureFlags } from "~/utils/zodSchemaForSettings";
+import { Checkbox } from "@blueprintjs/core";
+import Description from "roamjs-components/components/Description";
+import idToTitle from "roamjs-components/util/idToTitle";
+import React, { useState } from "react";
+
+export const BlockPropFlagPanel = ({
+ title,
+ description,
+ featureKey,
+}: {
+ title: string;
+ description: string;
+ featureKey: keyof FeatureFlags;
+}) => {
+ const [value, setValue] = useState(() =>
+ featureFlagEnabled({ key: featureKey }),
+ );
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { checked } = e.target;
+ featureFlagEnabled({ key: featureKey, value: checked });
+ setValue(checked);
+ };
+
+ return (
+
+ {idToTitle(title)}
+
+ >
+ }
+ />
+ );
+};
diff --git a/apps/roam/src/components/settings/GeneralSettings.tsx b/apps/roam/src/components/settings/GeneralSettings.tsx
index fbb213cda..c964ff14e 100644
--- a/apps/roam/src/components/settings/GeneralSettings.tsx
+++ b/apps/roam/src/components/settings/GeneralSettings.tsx
@@ -4,6 +4,7 @@ import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel";
import { getFormattedConfigTree } from "~/utils/discourseConfigRef";
import refreshConfigTree from "~/utils/refreshConfigTree";
import { DEFAULT_CANVAS_PAGE_FORMAT } from "~/index";
+import { BlockPropFlagPanel } from "./BlockPropFlagPanel";
const DiscourseGraphHome = () => {
const settings = useMemo(() => {
@@ -30,13 +31,10 @@ const DiscourseGraphHome = () => {
value={settings.canvasPageFormat.value}
defaultValue={DEFAULT_CANVAS_PAGE_FORMAT}
/>
-
{
posthog.init("phc_SNMmBqwNfcEpNduQ41dBUjtGNEUEKAy6jTn63Fzsrax", {
@@ -82,6 +83,8 @@ export default runExtension(async (onloadArgs) => {
});
}
+ await initSchema();
+
initFeedbackWidget();
if (window?.roamjs?.loaded?.has("query-builder")) {
diff --git a/apps/roam/src/utils/featureFlags.ts b/apps/roam/src/utils/featureFlags.ts
new file mode 100644
index 000000000..5c9c2550d
--- /dev/null
+++ b/apps/roam/src/utils/featureFlags.ts
@@ -0,0 +1,29 @@
+import { FeatureFlagsSchema, type FeatureFlags } from "./zodSchemaForSettings";
+import {
+ getBlockPropSettings,
+ setBlockPropSettings,
+ TOP_LEVEL_BLOCK_PROP_KEYS,
+} from "./settingsUsingBlockProps";
+
+export const featureFlagEnabled = ({
+ key,
+ value,
+}: {
+ key: keyof FeatureFlags;
+ value?: boolean;
+}): boolean => {
+ const featureFlagKey = TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags;
+
+ if (value !== undefined) {
+ void setBlockPropSettings({ keys: [featureFlagKey, key], value });
+ return value;
+ }
+
+ const { blockProps } = getBlockPropSettings({
+ keys: [featureFlagKey],
+ });
+
+ const flags = FeatureFlagsSchema.parse(blockProps || {});
+
+ return flags[key];
+};
diff --git a/apps/roam/src/utils/initBlockPropsSettingsConfig.ts b/apps/roam/src/utils/initBlockPropsSettingsConfig.ts
new file mode 100644
index 000000000..851db6cec
--- /dev/null
+++ b/apps/roam/src/utils/initBlockPropsSettingsConfig.ts
@@ -0,0 +1,62 @@
+// utils/initConfigPage.ts
+import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage";
+import {
+ TOP_LEVEL_BLOCK_PROP_KEYS,
+ DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
+} from "./settingsUsingBlockProps";
+import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
+import { createPage, createBlock } from "roamjs-components/writes";
+
+const ensurePageExists = async (pageTitle: string): Promise => {
+ let pageUid = getPageUidByPageTitle(pageTitle);
+
+ if (!pageUid) {
+ pageUid = window.roamAlphaAPI.util.generateUID();
+ await createPage({
+ title: pageTitle,
+ uid: pageUid,
+ });
+ console.log(`[Config] Created Page: "${pageTitle}"`);
+ }
+
+ return pageUid;
+};
+
+const ensureBlockExists = async (blockText: string, pageTitle: string) => {
+ const existingUid = getBlockUidByTextOnPage({
+ text: blockText,
+ title: pageTitle,
+ });
+
+ if (existingUid) return existingUid;
+
+ const pageUid = getPageUidByPageTitle(pageTitle);
+
+ if (!pageUid) {
+ console.warn(
+ `[Config] Page "${pageTitle}" not found. Cannot create block "${blockText}".`,
+ );
+ return null;
+ }
+
+ const newUid = await createBlock({
+ parentUid: pageUid,
+ node: { text: blockText },
+ });
+
+ console.log(`[Config] Created missing block: "${blockText}"`);
+ return newUid;
+};
+
+export const initSchema = async () => {
+ console.log("[Config] Verifying Schema Integrity...");
+ await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE);
+
+ await Promise.all(
+ Object.values(TOP_LEVEL_BLOCK_PROP_KEYS).map((blockName) =>
+ ensureBlockExists(blockName, DG_BLOCK_PROP_SETTINGS_PAGE_TITLE),
+ ),
+ );
+
+ console.log("[Config] Schema Ready.");
+};
diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts
index e10d05adf..96e7f6d5f 100644
--- a/apps/roam/src/utils/initializeObserversAndListeners.ts
+++ b/apps/roam/src/utils/initializeObserversAndListeners.ts
@@ -48,11 +48,22 @@ import {
import { renderNodeTagPopupButton } from "./renderNodeTagPopup";
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
import { getSetting } from "./extensionSettings";
-import { mountLeftSidebar } from "~/components/LeftSidebarView";
-import { getUidAndBooleanSetting } from "./getExportSettings";
+import {
+ mountLeftSidebar,
+ unmountLeftSidebar,
+} from "~/components/LeftSidebarView";
import { getCleanTagText } from "~/components/settings/NodeConfig";
import getPleasingColors from "@repo/utils/getPleasingColors";
import { colord } from "colord";
+import { featureFlagEnabled } from "./featureFlags";
+import {
+ getBlockPropSettings,
+ TOP_LEVEL_BLOCK_PROP_KEYS,
+ DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
+} from "./settingsUsingBlockProps";
+import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
+import { normalizeProps } from "./getBlockProps";
+import type { json } from "./getBlockProps";
const debounce = (fn: () => void, delay = 250) => {
let timeout: number;
@@ -227,23 +238,69 @@ export const initObservers = async ({
const personalTrigger = personalTriggerCombo?.key;
const personalModifiers = getModifiersFromCombo(personalTriggerCombo);
+ // Store reference to the container for reactive updates
+ let leftSidebarContainer: HTMLDivElement | null = null;
+
+ const updateLeftSidebar = (container: HTMLDivElement) => {
+ const isLeftSidebarEnabled = featureFlagEnabled({
+ key: "Enable Left sidebar",
+ });
+ if (isLeftSidebarEnabled) {
+ container.style.padding = "0";
+ mountLeftSidebar(container, onloadArgs);
+ } else {
+ unmountLeftSidebar(container);
+ }
+ };
+
const leftSidebarObserver = createHTMLObserver({
tag: "DIV",
useBody: true,
className: "starred-pages-wrapper",
callback: (el) => {
- const isLeftSidebarEnabled = getUidAndBooleanSetting({
- tree: configTree,
- text: "(BETA) Left Sidebar",
- }).value;
const container = el as HTMLDivElement;
- if (isLeftSidebarEnabled) {
- container.style.padding = "0";
- mountLeftSidebar(container, onloadArgs);
- }
+ leftSidebarContainer = container;
+ updateLeftSidebar(container);
},
});
+ const settingsPageUid = getPageUidByPageTitle(
+ DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
+ );
+ const { blockUid: featureFlagsBlockUid } = getBlockPropSettings({
+ keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags],
+ });
+
+ if (settingsPageUid && featureFlagsBlockUid) {
+ window.roamAlphaAPI.data.addPullWatch(
+ "[:block/props]",
+ `[:block/uid "${featureFlagsBlockUid}"]`,
+ (before, after) => {
+ console.log("feature flags changed", before, after);
+ if (!leftSidebarContainer) return;
+
+ const beforeProps = normalizeProps(
+ (before?.[":block/props"] || {}) as json,
+ ) as Record;
+ const afterProps = normalizeProps(
+ (after?.[":block/props"] || {}) as json,
+ ) as Record;
+
+ const beforeEnabled = beforeProps["Enable Left sidebar"] as
+ | boolean
+ | undefined;
+ const afterEnabled = afterProps["Enable Left sidebar"] as
+ | boolean
+ | undefined;
+
+ // Only update if the flag actually changed
+ if (beforeEnabled !== afterEnabled) {
+ updateLeftSidebar(leftSidebarContainer);
+ }
+ },
+ );
+ }
+
const handleNodeMenuRender = (target: HTMLElement, evt: KeyboardEvent) => {
if (
target.tagName === "TEXTAREA" &&
diff --git a/apps/roam/src/utils/settingsUsingBlockProps.ts b/apps/roam/src/utils/settingsUsingBlockProps.ts
new file mode 100644
index 000000000..985076c8a
--- /dev/null
+++ b/apps/roam/src/utils/settingsUsingBlockProps.ts
@@ -0,0 +1,94 @@
+import getBlockProps from "~/utils/getBlockProps";
+import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage";
+import setBlockProps from "./setBlockProps";
+
+export const DG_BLOCK_PROP_SETTINGS_PAGE_TITLE =
+ "roam/js/discourse-graph/block-prop-settings";
+type json = string | number | boolean | null | json[] | { [key: string]: json };
+
+export const TOP_LEVEL_BLOCK_PROP_KEYS = { featureFlags: "Feature Flags" };
+
+export const getBlockPropSettings = ({
+ keys,
+}: {
+ keys: string[];
+}): { blockProps: json | undefined; blockUid: string } => {
+ const sectionKey = keys[0];
+
+ const blockUid = getBlockUidByTextOnPage({
+ text: sectionKey,
+ title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
+ });
+
+ const allSectionBlockProps = getBlockProps(blockUid);
+
+ if (keys.length > 1) {
+ const propertyPath = keys.slice(1);
+
+ const targetValue = propertyPath.reduce(
+ (currentContext, currentKey) => {
+ if (
+ currentContext &&
+ typeof currentContext === "object" &&
+ !Array.isArray(currentContext)
+ ) {
+ return (currentContext as Record)[currentKey];
+ }
+ return undefined;
+ },
+ allSectionBlockProps,
+ );
+ return { blockProps: targetValue, blockUid };
+ }
+ console.log("all section block props", keys, allSectionBlockProps);
+ return { blockProps: allSectionBlockProps, blockUid };
+};
+
+export const setBlockPropSettings = ({
+ keys,
+ value,
+}: {
+ keys: string[];
+ value: json;
+}) => {
+ console.log("setting block prop settings", keys, value);
+ const { blockProps: currentProps, blockUid } = getBlockPropSettings({
+ keys: [keys[0]],
+ }) || { blockProps: {}, blockUid: "" };
+ console.log("current props", currentProps);
+
+ const newProps = JSON.parse(JSON.stringify(currentProps || {})) as Record<
+ string,
+ json
+ >;
+
+ if (keys && keys.length > 1) {
+ const propertyPath = keys.slice(1);
+ const lastKeyIndex = propertyPath.length - 1;
+
+ propertyPath.reduce((currentContext, currentKey, index) => {
+ const contextRecord = currentContext;
+
+ if (index === lastKeyIndex) {
+ contextRecord[currentKey] = value;
+ return contextRecord;
+ }
+
+ if (
+ !contextRecord[currentKey] ||
+ typeof contextRecord[currentKey] !== "object" ||
+ Array.isArray(contextRecord[currentKey])
+ ) {
+ contextRecord[currentKey] = {};
+ }
+
+ return contextRecord[currentKey];
+ }, newProps);
+ } else {
+ console.log("setting root block prop", value);
+ newProps[keys[1]] = value;
+ }
+ console.log("new props", newProps);
+
+ setBlockProps(blockUid, newProps, true);
+};
diff --git a/apps/roam/src/utils/zodSchemaForSettings.ts b/apps/roam/src/utils/zodSchemaForSettings.ts
new file mode 100644
index 000000000..61a71cfe4
--- /dev/null
+++ b/apps/roam/src/utils/zodSchemaForSettings.ts
@@ -0,0 +1,7 @@
+import { z } from "zod";
+
+export const FeatureFlagsSchema = z.object({
+ "Enable Left sidebar": z.boolean().default(false),
+});
+
+export type FeatureFlags = z.infer;
\ No newline at end of file