Skip to content
Open
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
36 changes: 20 additions & 16 deletions apps/roam/src/components/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ import {
type NodeUidWithType,
} from "~/utils/publishNodesToGroups";
import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext";
import { isSyncEnabled } from "~/components/settings/utils/accessors";
import { isNodeSharingEnabled } from "~/components/settings/utils/accessors";

const ExportProgress = ({ id }: { id: string }) => {
const [progress, setProgress] = useState(0);
Expand Down Expand Up @@ -218,12 +218,12 @@ const ExportDialog: ExportDialogComponent = ({
useState<(typeof SEND_TO_DESTINATIONS)[number]>("page");
const isSendToGraph = activeSendToDestination === "graph";
const [livePages, setLivePages] = useState<Result[]>([]);
const syncEnabled = useMemo(() => isSyncEnabled(), []);
const sharingEnabled = useMemo(() => isNodeSharingEnabled(), []);
const [selectedTabId, setSelectedTabId] = useState("sendto");
useEffect(() => {
if (initialPanel === "publish" && !syncEnabled) return;
if (initialPanel === "publish" && !sharingEnabled) return;
if (initialPanel) setSelectedTabId(INITIAL_PANEL_TO_TAB_ID[initialPanel]);
}, [initialPanel, syncEnabled]);
}, [initialPanel, sharingEnabled]);
const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false);
const [gitHubAccessToken, setGitHubAccessToken] = useState<string | null>(
getSetting<string | null>("oauth-github", null),
Expand All @@ -240,7 +240,7 @@ const ExportDialog: ExportDialogComponent = ({

const publishableNodes = useMemo(
() =>
syncEnabled
sharingEnabled
? results
.map((r) => {
const node = findDiscourseNode({ uid: r.uid });
Expand All @@ -250,7 +250,7 @@ const ExportDialog: ExportDialogComponent = ({
})
.filter((n): n is NodeUidWithType => n !== null)
: [],
[results, syncEnabled],
[results, sharingEnabled],
);
const nonDiscourseCount = results.length - publishableNodes.length;

Expand Down Expand Up @@ -808,7 +808,7 @@ const ExportDialog: ExportDialogComponent = ({
};
useEffect(() => {
if (
!syncEnabled ||
!sharingEnabled ||
!isOpen ||
selectedTabId !== "publish" ||
groupsLoaded ||
Expand All @@ -829,7 +829,7 @@ const ExportDialog: ExportDialogComponent = ({
setGroupsLoaded(true);
}
})();
}, [syncEnabled, isOpen, selectedTabId, groupsLoaded, groupsLoading]);
}, [sharingEnabled, isOpen, selectedTabId, groupsLoaded, groupsLoading]);

const handlePublish = async () => {
setPublishError("");
Expand All @@ -840,7 +840,7 @@ const ExportDialog: ExportDialogComponent = ({
if (!client || !context) throw new Error("Could not connect to sync.");
const {
publishedNodeUids,
skippedUnsyncedUids,
failedUpsertUids,
okGroupIds,
failedGroupIds,
} = await publishNodeUidsWithTypeToGroups({
Expand All @@ -849,10 +849,14 @@ const ExportDialog: ExportDialogComponent = ({
groupIds: selectedGroupIds,
nodeUids: publishableNodes,
});
const selectedNodeUids = new Set(publishableNodes.map((n) => n.uid));
const failedNodeCount = failedUpsertUids.filter((uid) =>
selectedNodeUids.has(uid),
).length;
posthog.capture("Export Dialog: Publish", {
groupCount: okGroupIds.length,
publishedNodeCount: publishedNodeUids.length,
skippedUnsyncedCount: skippedUnsyncedUids.length,
failedUpsertCount: failedUpsertUids.length,
nonDiscourseCount,
failedGroupCount: failedGroupIds.length,
});
Expand All @@ -866,10 +870,8 @@ const ExportDialog: ExportDialogComponent = ({
}.`,
]
: ["No nodes were published."];
if (skippedUnsyncedUids.length)
messages.push(
`${skippedUnsyncedUids.length} not synced yet — try again shortly.`,
);
if (failedNodeCount)
messages.push(`${failedNodeCount} failed to publish.`);
if (nonDiscourseCount)
messages.push(`${nonDiscourseCount} skipped (not discourse nodes).`);
if (failedGroupIds.length)
Expand All @@ -881,7 +883,9 @@ const ExportDialog: ExportDialogComponent = ({
renderToast({
content: messages.join(" "),
intent:
failedGroupIds.length || !hasPublishedNodes ? "warning" : "success",
failedGroupIds.length || failedNodeCount || !hasPublishedNodes
? "warning"
: "success",
id: "query-builder-publish-success",
});
if (hasPublishedNodes) onClose();
Expand Down Expand Up @@ -1263,7 +1267,7 @@ const ExportDialog: ExportDialogComponent = ({
>
<Tab id="sendto" title="Send To" panel={SendToPanel} />
<Tab id="export" title="Export" panel={ExportPanel} />
{syncEnabled && (
{sharingEnabled && (
<Tab id="publish" title="Publish" panel={PublishPanel} />
)}
</Tabs>
Expand Down
6 changes: 6 additions & 0 deletions apps/roam/src/components/settings/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,12 @@ const FeatureFlagsTab = (): React.ReactElement => {
onAfterChange={(checked) => setAdvancedNodeSearchValue(checked)}
/>

<FeatureFlagPanel
title="Node sharing"
description="This enables a user to share nodes to other discourse spaces and keeps shared nodes synced in the background. Reload the graph after toggling."
featureKey="Enable node sharing"
/>

<Alert
isOpen={isConsentAlertOpen}
onConfirm={() => {
Expand Down
3 changes: 3 additions & 0 deletions apps/roam/src/components/settings/utils/accessors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,9 @@ export const readAllLegacyDiscourseNodeSettings = (
export const isSyncEnabled = (): boolean =>
getFeatureFlag("Suggestive mode overlay enabled");

export const isNodeSharingEnabled = (): boolean =>
getFeatureFlag("Enable node sharing");

export const setFeatureFlag = (
key: keyof FeatureFlags,
value: boolean,
Expand Down
1 change: 1 addition & 0 deletions apps/roam/src/components/settings/utils/settingKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {

export const FEATURE_FLAG_KEYS = {
enableLeftSidebar: "Enable left sidebar",
enableNodeSharing: "Enable node sharing",
suggestiveModeOverlayEnabled: "Suggestive mode overlay enabled",
useNewSettingsStore: "Use new settings store",
} as const satisfies Record<string, keyof FeatureFlags>;
Expand Down
2 changes: 2 additions & 0 deletions apps/roam/src/components/settings/utils/zodSchema.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ const discourseNodeSettings: DiscourseNodeSettings = {
const featureFlags: FeatureFlags = {
"Advanced node search enabled": true,
"Enable left sidebar": true,
"Enable node sharing": true,
"Suggestive mode overlay enabled": true,
"Use new settings store": false,
};

const defaultFeatureFlags: FeatureFlags = {
"Advanced node search enabled": false,
"Enable left sidebar": false,
"Enable node sharing": false,
"Suggestive mode overlay enabled": false,
"Use new settings store": false,
};
Expand Down
1 change: 1 addition & 0 deletions apps/roam/src/components/settings/utils/zodSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const DiscourseRelationSchema = z.object({
export const FeatureFlagsSchema = z.object({
"Advanced node search enabled": z.boolean().default(false),
"Enable left sidebar": z.boolean().default(false),
"Enable node sharing": z.boolean().default(false),
"Suggestive mode overlay enabled": z.boolean().default(false),
"Use new settings store": z.boolean().default(false),
});
Expand Down
3 changes: 2 additions & 1 deletion apps/roam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { initPostHog } from "./utils/posthog";
import { initSchema } from "./components/settings/utils/init";
import {
bulkReadSettings,
isNodeSharingEnabled,
isSyncEnabled,
} from "./components/settings/utils/accessors";
import { PERSONAL_KEYS } from "./components/settings/utils/settingKeys";
Expand Down Expand Up @@ -134,7 +135,7 @@ export default runExtension(async (onloadArgs) => {
document.addEventListener("input", discourseNodeSearchTriggerListener);
document.addEventListener("selectionchange", nodeCreationPopoverListener);

if (isSyncEnabled()) {
if (isSyncEnabled() || isNodeSharingEnabled()) {
initializeSupabaseSync();
}

Expand Down
49 changes: 49 additions & 0 deletions apps/roam/src/utils/__tests__/nodeSharingFeatureFlag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it, vi } from "vitest";

vi.mock("~/utils/internalError", () => ({ default: vi.fn() }));
vi.mock("~/utils/extensionSettings", () => ({ getSetting: vi.fn() }));
vi.mock("~/utils/parseQuery", () => ({ roamNodeToCondition: vi.fn() }));

import {
isNodeSharingEnabled,
isSyncEnabled,
} from "~/components/settings/utils/accessors";

const seedWindow = (featureFlags: Record<string, boolean>) => {
(globalThis as { window: unknown }).window = {
roamAlphaAPI: {
user: { uid: () => "user-1" },
pull: () => ({
":block/children": [
{
":block/string": "Feature Flags",
":block/props": {
"Use new settings store": true,
...featureFlags,
},
},
],
}),
},
};
};

describe("node sharing feature flag", () => {
it("defaults to disabled alongside suggestive mode", () => {
seedWindow({});
expect(isNodeSharingEnabled()).toBe(false);
expect(isSyncEnabled()).toBe(false);
});

it("enables node sharing without suggestive mode", () => {
seedWindow({ "Enable node sharing": true });
expect(isNodeSharingEnabled()).toBe(true);
expect(isSyncEnabled()).toBe(false);
});

it("does not enable node sharing when only suggestive mode is on", () => {
seedWindow({ "Suggestive mode overlay enabled": true });
expect(isNodeSharingEnabled()).toBe(false);
expect(isSyncEnabled()).toBe(true);
});
});
Loading