diff --git a/apps/roam/src/components/GitHubSync.tsx b/apps/roam/src/components/GitHubSync.tsx index f71b5e4a6..36830e84f 100644 --- a/apps/roam/src/components/GitHubSync.tsx +++ b/apps/roam/src/components/GitHubSync.tsx @@ -5,7 +5,7 @@ import React, { useRef, useState, } from "react"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import matchDiscourseNode from "~/utils/matchDiscourseNode"; import { OnloadArgs, PullBlock, RoamBasicNode } from "roamjs-components/types"; import { Button, Card, Classes, Dialog, Tag } from "@blueprintjs/core"; @@ -48,8 +48,6 @@ import { } from "roamjs-components/components/ConfigPanels/types"; import CustomPanel from "roamjs-components/components/ConfigPanels/CustomPanel"; import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid"; -import CommentsQuery from "./GitHubSyncCommentsQuery"; -import getSubTree from "roamjs-components/util/getSubTree"; import isFlagEnabled from "~/utils/isFlagEnabled"; const CommentUidCache = new Set(); @@ -105,27 +103,21 @@ const getPageGitHubPropsDetails = (pageUid: string) => { const getRoamCommentsContainerUid = async ({ pageUid, extensionAPI, + matchingNode, }: { pageUid: string; extensionAPI: OnloadArgs["extensionAPI"]; + matchingNode?: DiscourseNode; }) => { const pageTitle = getPageTitleByPageUid(pageUid); - const configUid = getPageUidByPageTitle(CONFIG_PAGE); - const configTree = getBasicTreeByParentUid(configUid); - const queryNode = getSubTree({ - tree: configTree, - key: "Comments Block", - }); - if (!queryNode) { - renderToast({ - id: "github-issue-comments", - content: `Comments Block query not set. Set it in ${CONFIG_PAGE}`, - }); + + if (!matchingNode?.githubSync?.commentsQueryUid || !matchingNode) { return; } + const results = await runQuery({ extensionAPI, - parentUid: queryNode.uid, + parentUid: matchingNode.githubSync?.commentsQueryUid, inputs: { NODETEXT: pageTitle, NODEUID: pageUid }, }); @@ -134,9 +126,11 @@ const getRoamCommentsContainerUid = async ({ export const insertNewCommentsFromGitHub = async ({ pageUid, extensionAPI, + matchingNode, }: { pageUid: string; extensionAPI: OnloadArgs["extensionAPI"]; + matchingNode?: DiscourseNode; }) => { const getCommentsOnPage = (pageUid: string) => { const query = `[:find @@ -171,6 +165,7 @@ export const insertNewCommentsFromGitHub = async ({ const commentsContainerUid = await getRoamCommentsContainerUid({ pageUid, extensionAPI, + matchingNode, }); const gitHubAccessToken = localStorageGet("github-oauth"); @@ -250,40 +245,33 @@ export const insertNewCommentsFromGitHub = async ({ }); } }; + export const isGitHubSyncPage = (pageTitle: string) => { - if (!enabled) return; - const gitHubNodeResult = window.roamAlphaAPI.data.fast.q(`[:find - (pull ?node [:block/string]) - :where - [?roamjsgithub-sync :node/title "roam/js/github-sync"] - [?node :block/page ?roamjsgithub-sync] - [?p :block/children ?node] - (or [?p :block/string ?p-String] - [?p :node/title ?p-String]) - [(clojure.string/includes? ?p-String "Node Select")] - ]`) as [PullBlock][]; - const nodeText = gitHubNodeResult[0]?.[0]?.[":block/string"] || ""; - if (!nodeText) return; + if (!enabled) return null; const discourseNodes = getDiscourseNodes(); - const selectedNode = discourseNodes.find((node) => node.text === nodeText); - const isPageTypeOfNode = matchDiscourseNode({ - format: selectedNode?.format || "", - specification: selectedNode?.specification || [], - text: selectedNode?.text || "", - title: pageTitle, - }); - return isPageTypeOfNode; + return discourseNodes.find( + (node) => + node.githubSync?.enabled && + matchDiscourseNode({ + format: node.format || "", + specification: node.specification || [], + text: node.text || "", + title: pageTitle, + }), + ); }; export const renderGitHubSyncPage = async ({ title, h1, onloadArgs, + matchingNode, }: { title: string; h1: HTMLHeadingElement; onloadArgs: OnloadArgs; + matchingNode: DiscourseNode; }) => { const extensionAPI = onloadArgs.extensionAPI; const pageUid = getPageUidByPageTitle(title); @@ -291,6 +279,7 @@ export const renderGitHubSyncPage = async ({ const commentsContainerUid = await getRoamCommentsContainerUid({ pageUid, extensionAPI, + matchingNode, }); const commentHeaderEl = document.querySelector( `.rm-block__input[id$="${commentsContainerUid}"]`, @@ -913,73 +902,6 @@ const initializeGitHubSync = async (onloadArgs: OnloadArgs) => { const unloads = new Set<() => void>(); const toggle = async (flag: boolean) => { if (flag && !enabled) { - const { observer: configObserver } = await createConfigObserver({ - title: "roam/js/github-sync", - config: { - tabs: [ - { - id: "home", - fields: [ - // @ts-ignore - { - title: "Docs", - description: `More information about the GitHub Sync Feature.`, - Panel: CustomPanel, - options: { - component: () => { - return ( -
-

- For more information about the GitHub Sync feature, - visit the GitHub page: -

- - GitHub Sync Documentation - -
- ); - }, - }, - } as Field, - { - // @ts-ignore - Panel: SelectPanel, - title: "Node Select", - description: - "Select the node type to sync with GitHub Issues", - options: { - items: [ - "None", - ...getDiscourseNodes() - .map((node) => node.text) - .filter((text) => text !== "Block"), - ], - }, - defaultValue: "None", - }, - // @ts-ignore - { - Panel: CustomPanel, - title: "Comments Block", - description: - "Where comments are synced to. This will fire when the node is loaded. You have access to ':in NODETEXT' and ':in NODEUID' as variables for the current node.", - options: { - component: ({ uid }) => - React.createElement(CommentsQuery, { - parentUid: uid, - onloadArgs, - }), - }, - } as Field, - ], - }, - ], - }, - }); - const commentObserver = createBlockObserver({ onBlockLoad: (b) => { const { blockUid } = getUids(b); @@ -1013,7 +935,6 @@ const initializeGitHubSync = async (onloadArgs: OnloadArgs) => { }, }); - unloads.add(() => configObserver?.disconnect()); unloads.add(() => commentObserver.forEach((o) => o.disconnect())); unloads.add(() => CommentUidCache.clear()); unloads.add(() => CommentContainerUidCache.clear()); @@ -1026,7 +947,6 @@ const initializeGitHubSync = async (onloadArgs: OnloadArgs) => { await toggle(isFlagEnabled(SETTING)); return toggle; }; - export const toggleGitHubSync = async ( flag: boolean, onloadArgs: OnloadArgs, diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 91112aae4..1546ebcfd 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -1,17 +1,26 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import SelectPanel from "roamjs-components/components/ConfigPanels/SelectPanel"; import BlocksPanel from "roamjs-components/components/ConfigPanels/BlocksPanel"; import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; -import { getSubTree } from "roamjs-components/util"; +import { getSubTree, setInputSetting } from "roamjs-components/util"; import Description from "roamjs-components/components/Description"; -import { Label, Tabs, Tab, TabId } from "@blueprintjs/core"; +import { + Label, + Tabs, + Tab, + TabId, + Checkbox, + Icon, + Tooltip, +} from "@blueprintjs/core"; import DiscourseNodeSpecification from "./DiscourseNodeSpecification"; import DiscourseNodeAttributes from "./DiscourseNodeAttributes"; import DiscourseNodeCanvasSettings from "./DiscourseNodeCanvasSettings"; import DiscourseNodeIndex from "./DiscourseNodeIndex"; import { OnloadArgs } from "roamjs-components/types"; +import CommentsQuery from "~/components/GitHubSyncCommentsQuery"; const NodeConfig = ({ node, @@ -32,12 +41,33 @@ const NodeConfig = ({ const overlayUid = getUid("Overlay"); const canvasUid = getUid("Canvas"); const graphOverviewUid = getUid("Graph Overview"); + const githubSyncUid = getUid("GitHub Sync"); + + const [githubCommentsFormatUid, setGithubCommentsFormatUid] = + useState(""); + + // TEMP FIX: This is a workaround to ensure the github comments format uid is set after the github sync node is created + useEffect(() => { + const timeout = setTimeout(() => { + const commentsUid = getSubTree({ + parentUid: githubSyncUid, + key: "Comments Block", + }).uid; + setGithubCommentsFormatUid(commentsUid); + }, 250); + + return () => clearTimeout(timeout); + }, [node.type]); + const attributeNode = getSubTree({ parentUid: node.type, key: "Attributes", }); const [selectedTabId, setSelectedTabId] = useState("main"); + const [isGithubSyncEnabled, setIsGithubSyncEnabled] = useState( + node.githubSync?.enabled || false, + ); return ( <> @@ -45,12 +75,13 @@ const NodeConfig = ({ onChange={(id) => setSelectedTabId(id)} selectedTabId={selectedTabId} renderActiveTabPanelOnly={true} + className="discourse-node-tabs overflow-x-auto" > +
+
+
+
+
+
} /> + +
+

+ GitHub integration allows you to sync {node.text} pages with + GitHub Issues. When enabled, you can: +

+
    +
  • Send pages to GitHub as issues
  • +
  • Import and sync comments from GitHub
  • +
  • Configure where comments appear in the page
  • +
+
+ { + const target = e.target as HTMLInputElement; + setIsGithubSyncEnabled(target.checked); + if (target.checked) { + setInputSetting({ + blockUid: githubSyncUid, + key: "Enabled", + value: "true", + }); + } else { + setInputSetting({ + blockUid: githubSyncUid, + key: "Enabled", + value: "false", + }); + } + }} + > + GitHub Sync Enabled + + + + + + {isGithubSyncEnabled && ( + <> + + + + )} +
+ } + /> ); diff --git a/apps/roam/src/utils/getDiscourseNodes.ts b/apps/roam/src/utils/getDiscourseNodes.ts index 2879ee76a..73bc7fae3 100644 --- a/apps/roam/src/utils/getDiscourseNodes.ts +++ b/apps/roam/src/utils/getDiscourseNodes.ts @@ -10,7 +10,11 @@ export const excludeDefaultNodes = (node: DiscourseNode) => { return node.backedBy !== "default"; }; -// TODO - only text and type should be required +type GitHubSyncConfig = { + enabled: boolean; + commentsQueryUid: string; +}; + export type DiscourseNode = { text: string; type: string; @@ -25,6 +29,7 @@ export type DiscourseNode = { graphOverview?: boolean; description?: string; template?: InputTextNode[]; + githubSync?: GitHubSyncConfig; }; const DEFAULT_NODES: DiscourseNode[] = [ @@ -81,6 +86,19 @@ const getSpecification = (children: RoamBasicNode[] | undefined) => { const getDiscourseNodes = (relations = getDiscourseRelations()) => { const configuredNodes = Object.entries(discourseConfigRef.nodes) .map(([type, { text, children }]): DiscourseNode => { + const githubSyncNode = getSubTree({ tree: children, key: "GitHub Sync" }); + + const githubSync: GitHubSyncConfig = { + enabled: + getSettingValueFromTree({ + tree: githubSyncNode.children, + key: "Enabled", + }) === "true", + commentsQueryUid: getSubTree({ + parentUid: githubSyncNode.uid, + key: "Comments Block", + }).uid, + }; return { format: getSettingValueFromTree({ tree: children, key: "format" }), text, @@ -95,6 +113,7 @@ const getDiscourseNodes = (relations = getDiscourseRelations()) => { ), graphOverview: children.filter((c) => c.text === "Graph Overview").length > 0, + githubSync, description: getSettingValueFromTree({ tree: children, key: "description", diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 3c01fa505..52e642c89 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -56,7 +56,10 @@ export const initObservers = async ({ if (isNodeConfigPage(title)) renderNodeConfigPage(props); else if (isQueryPage(props)) renderQueryPage(props); else if (isCanvasPage(props)) renderTldrawCanvas(props); - else if (isGitHubSyncPage(title)) renderGitHubSyncPage(props); + else { + const matchingNode = isGitHubSyncPage(title); + if (matchingNode) renderGitHubSyncPage({ ...props, matchingNode }); + } }, });