Skip to content
Draft
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions apps/roam/src/components/LeftSidebarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,15 @@ export const mountLeftSidebar = (
ReactDOM.render(<LeftSidebarView onloadArgs={onloadArgs} />, 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;
39 changes: 39 additions & 0 deletions apps/roam/src/components/settings/BlockPropFlagPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
const { checked } = e.target;
featureFlagEnabled({ key: featureKey, value: checked });
setValue(checked);
};

return (
<Checkbox
checked={value}
onChange={handleChange}
labelElement={
<>
{idToTitle(title)}
<Description description={description} />
</>
}
/>
);
};
8 changes: 3 additions & 5 deletions apps/roam/src/components/settings/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -30,13 +31,10 @@ const DiscourseGraphHome = () => {
value={settings.canvasPageFormat.value}
defaultValue={DEFAULT_CANVAS_PAGE_FORMAT}
/>
<FlagPanel
<BlockPropFlagPanel
title="(BETA) Left Sidebar"
description="Whether or not to enable the left sidebar."
order={2}
uid={settings.leftSidebarEnabled.uid}
parentUid={settings.settingsUid}
value={settings.leftSidebarEnabled.value || false}
featureKey="Enable Left sidebar"
/>
<FlagPanel
title="(BETA) Suggestive Mode Enabled"
Expand Down
3 changes: 3 additions & 0 deletions apps/roam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit
import { DISCOURSE_CONFIG_PAGE_TITLE } from "./utils/renderNodeConfigPage";
import { getSetting } from "./utils/extensionSettings";
import { STREAMLINE_STYLING_KEY } from "./data/userSettings";
import { initSchema } from "./utils/initBlockPropsSettingsConfig";

const initPostHog = () => {
posthog.init("phc_SNMmBqwNfcEpNduQ41dBUjtGNEUEKAy6jTn63Fzsrax", {
Expand Down Expand Up @@ -82,6 +83,8 @@ export default runExtension(async (onloadArgs) => {
});
}

await initSchema();

initFeedbackWidget();

if (window?.roamjs?.loaded?.has("query-builder")) {
Expand Down
29 changes: 29 additions & 0 deletions apps/roam/src/utils/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -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];
};
62 changes: 62 additions & 0 deletions apps/roam/src/utils/initBlockPropsSettingsConfig.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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.");
};
77 changes: 67 additions & 10 deletions apps/roam/src/utils/initializeObserversAndListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, json>;
const afterProps = normalizeProps(
(after?.[":block/props"] || {}) as json,
) as Record<string, json>;

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" &&
Expand Down
94 changes: 94 additions & 0 deletions apps/roam/src/utils/settingsUsingBlockProps.ts
Original file line number Diff line number Diff line change
@@ -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<json | undefined>(
(currentContext, currentKey) => {
if (
currentContext &&
typeof currentContext === "object" &&
!Array.isArray(currentContext)
) {
return (currentContext as Record<string, json>)[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);
};
7 changes: 7 additions & 0 deletions apps/roam/src/utils/zodSchemaForSettings.ts
Original file line number Diff line number Diff line change
@@ -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<typeof FeatureFlagsSchema>;