From 0934bbe6b1a66f39159e17cf138e3e1e8bc4e779 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 8 Dec 2025 21:37:35 +0530 Subject: [PATCH] dry run for full flow --- apps/roam/src/components/LeftSidebarView.tsx | 11 +++ .../settings/BlockPropFlagPanel.tsx | 39 ++++++++ .../components/settings/GeneralSettings.tsx | 8 +- apps/roam/src/index.ts | 3 + apps/roam/src/utils/featureFlags.ts | 29 ++++++ .../src/utils/initBlockPropsSettingsConfig.ts | 62 ++++++++++++ .../utils/initializeObserversAndListeners.ts | 77 +++++++++++++-- .../roam/src/utils/settingsUsingBlockProps.ts | 94 +++++++++++++++++++ apps/roam/src/utils/zodSchemaForSettings.ts | 7 ++ 9 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 apps/roam/src/components/settings/BlockPropFlagPanel.tsx create mode 100644 apps/roam/src/utils/featureFlags.ts create mode 100644 apps/roam/src/utils/initBlockPropsSettingsConfig.ts create mode 100644 apps/roam/src/utils/settingsUsingBlockProps.ts create mode 100644 apps/roam/src/utils/zodSchemaForSettings.ts 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