-
Notifications
You must be signed in to change notification settings - Fork 4
Roam: ENG-622: Node tag to node conversions #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
958cdee
real time validation
sid597 27bd7e9
use on blur validation, add tag to config page, settings, extract wit…
sid597 3d3558c
conditionally show node tag
sid597 bfa4ea7
use live validation, don't save conflicting state to roam
sid597 a660726
Merge branch 'eng-610-tag-assignment-in-roam-dg-plugin-settings-menu'…
sid597 2fcc70c
press down key to trigger
sid597 6a2966e
coderabbit review
sid597 8f350cb
update block text
sid597 dc214f2
Merge branch 'eng-621-show-node-tag-options-in-inline-node-creation-m…
sid597 3cdac68
node tage to node conversion
sid597 cd46113
Revert "node tage to node conversion"
sid597 fc76dbf
node tage to node conversion
sid597 c9c6d7d
refactor and review '
sid597 72d0016
address coderabbit
sid597 3b2d92b
address review
sid597 ef89ce4
--amend
sid597 031ef17
fix arrow movement while shift, default menu selection or not
sid597 e4aa456
Merge branch 'eng-621-show-node-tag-options-in-inline-node-creation-m…
sid597 b39a1fb
address review
sid597 61e1f87
use popover and hover listeners to fix rendering
sid597 7a0f6e5
moving out
sid597 6281a07
Merge branch 'main' into eng-622-node-tag-to-node-conversions
sid597 f69a04f
use observer
sid597 d38c473
fix style
sid597 95e7b7b
placeholder text
sid597 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import React, { useEffect, useRef, useState } from "react"; | ||
| import { Dialog, Classes, InputGroup, Label, Button } from "@blueprintjs/core"; | ||
| import renderOverlay from "roamjs-components/util/renderOverlay"; | ||
| import createDiscourseNode from "~/utils/createDiscourseNode"; | ||
| import { OnloadArgs } from "roamjs-components/types"; | ||
| import updateBlock from "roamjs-components/writes/updateBlock"; | ||
| import { render as renderToast } from "roamjs-components/components/Toast"; | ||
| import getDiscourseNodes, { | ||
| DiscourseNode, | ||
| excludeDefaultNodes, | ||
| } from "~/utils/getDiscourseNodes"; | ||
| import { getNewDiscourseNodeText } from "~/utils/formatUtils"; | ||
| import MenuItemSelect from "roamjs-components/components/MenuItemSelect"; | ||
|
|
||
| export type CreateNodeDialogProps = { | ||
| onClose: () => void; | ||
| defaultNodeTypeUid: string; | ||
| extensionAPI: OnloadArgs["extensionAPI"]; | ||
| sourceBlockUid?: string; | ||
| initialTitle: string; | ||
| }; | ||
|
|
||
| const CreateNodeDialog = ({ | ||
| onClose, | ||
| defaultNodeTypeUid, | ||
| extensionAPI, | ||
| sourceBlockUid, | ||
| initialTitle, | ||
| }: CreateNodeDialogProps) => { | ||
| const discourseNodes = getDiscourseNodes().filter(excludeDefaultNodes); | ||
| const defaultNodeType = | ||
| discourseNodes.find((n) => n.type === defaultNodeTypeUid) || | ||
| discourseNodes[0]; | ||
|
|
||
| const [title, setTitle] = useState(initialTitle); | ||
| const [selectedType, setSelectedType] = | ||
| useState<DiscourseNode>(defaultNodeType); | ||
| const [loading, setLoading] = useState(false); | ||
| const inputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (inputRef.current) { | ||
| inputRef.current.focus(); | ||
| } | ||
| }, []); | ||
|
|
||
| const onCreate = async () => { | ||
| if (!title.trim()) return; | ||
| setLoading(true); | ||
|
|
||
| const formattedTitle = await getNewDiscourseNodeText({ | ||
| text: title.trim(), | ||
| nodeType: selectedType.type, | ||
| blockUid: sourceBlockUid, | ||
| }); | ||
|
|
||
| if (!formattedTitle) { | ||
| setLoading(false); | ||
| return; | ||
| } | ||
|
|
||
| const newPageUid = await createDiscourseNode({ | ||
| text: formattedTitle, | ||
| configPageUid: selectedType.type, | ||
| extensionAPI, | ||
| }); | ||
|
|
||
| if (sourceBlockUid) { | ||
| // TODO: This assumes the new node is always a page. If the specification | ||
| // defines it as a block (e.g., "is in page with title"), this will not create | ||
| // the correct reference. The reference format should be determined by the | ||
| // node's specification. | ||
| const pageRef = `[[${formattedTitle}]]`; | ||
sid597 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await updateBlock({ uid: sourceBlockUid, text: pageRef }); | ||
|
|
||
| const newCursorPosition = pageRef.length; | ||
| const windowId = | ||
| window.roamAlphaAPI.ui.getFocusedBlock?.()?.["window-id"] || "main"; | ||
|
|
||
| await window.roamAlphaAPI.ui.setBlockFocusAndSelection({ | ||
| location: { "block-uid": sourceBlockUid, "window-id": windowId }, | ||
| selection: { start: newCursorPosition }, | ||
| }); | ||
| } | ||
|
|
||
| renderToast({ | ||
| id: `discourse-node-created-${Date.now()}`, | ||
| intent: "success", | ||
| timeout: 10000, | ||
| content: ( | ||
| <span> | ||
| Created node{" "} | ||
| <a | ||
| className="cursor-pointer font-medium text-blue-500 hover:underline" | ||
| onClick={async (event) => { | ||
| if (event.shiftKey) { | ||
| await window.roamAlphaAPI.ui.rightSidebar.addWindow({ | ||
| window: { | ||
| "block-uid": newPageUid, | ||
| type: "outline", | ||
| }, | ||
| }); | ||
| } else { | ||
| await window.roamAlphaAPI.ui.mainWindow.openPage({ | ||
| page: { uid: newPageUid }, | ||
| }); | ||
| } | ||
| }} | ||
| > | ||
| [[{formattedTitle}]] | ||
| </a> | ||
| </span> | ||
| ), | ||
| }); | ||
| setLoading(false); | ||
| onClose(); | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog | ||
| isOpen={true} | ||
| onClose={onClose} | ||
| title="Create Discourse Node" | ||
| autoFocus={false} | ||
| > | ||
| <div className={Classes.DIALOG_BODY}> | ||
| <div className="flex flex-col gap-4"> | ||
| <div> | ||
| <label className="mb-1 block font-bold">Title</label> | ||
sid597 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <InputGroup | ||
| placeholder={`This is a potential ${selectedType.text.toLowerCase()}`} | ||
| value={title} | ||
| onChange={(e) => setTitle(e.currentTarget.value)} | ||
| inputRef={inputRef} | ||
| /> | ||
| </div> | ||
|
|
||
| <Label> | ||
| Type | ||
| <MenuItemSelect | ||
| items={discourseNodes.map((n) => n.type)} | ||
| transformItem={(t) => | ||
| discourseNodes.find((n) => n.type === t)?.text || t | ||
| } | ||
| activeItem={selectedType.type} | ||
| onItemSelect={(t) => { | ||
| const nt = discourseNodes.find((n) => n.type === t); | ||
| if (nt) setSelectedType(nt); | ||
| }} | ||
| /> | ||
| </Label> | ||
| </div> | ||
| </div> | ||
| <div className={Classes.DIALOG_FOOTER}> | ||
| <div className={Classes.DIALOG_FOOTER_ACTIONS}> | ||
| <Button minimal onClick={onClose} disabled={loading}> | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| intent="primary" | ||
| onClick={onCreate} | ||
| disabled={!title.trim() || loading} | ||
| loading={loading} | ||
| > | ||
| Create | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
|
|
||
| export const renderCreateNodeDialog = (props: CreateNodeDialogProps) => | ||
| renderOverlay({ | ||
| Overlay: CreateNodeDialog, | ||
| props, | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ const ValidatedInputPanel = ({ | |
| error: string; | ||
| placeholder?: string; | ||
| }) => ( | ||
| <> | ||
| <div className="flex flex-col"> | ||
| <Label> | ||
| {label} | ||
| <Description description={description} /> | ||
|
|
@@ -47,7 +47,7 @@ const ValidatedInputPanel = ({ | |
| {error && ( | ||
| <div className="mt-1 text-sm font-medium text-red-600">{error}</div> | ||
| )} | ||
| </> | ||
| </div> | ||
| ); | ||
|
|
||
| const useDebouncedRoamUpdater = ( | ||
|
|
@@ -229,7 +229,7 @@ const NodeConfig = ({ | |
| onChange={handleTagChange} | ||
| onBlur={handleTagBlur} | ||
| error={tagError} | ||
| placeholder={`#${node.text}`} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I liked the I'm going to create a task to handle
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| placeholder={`${node.text}`} | ||
| /> | ||
| </div> | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import React from "react"; | ||
| import ReactDOM from "react-dom"; | ||
| import { Button, Popover, Position } from "@blueprintjs/core"; | ||
| import { renderCreateNodeDialog } from "~/components/CreateNodeDialog"; | ||
| import { OnloadArgs } from "roamjs-components/types"; | ||
| import getUids from "roamjs-components/dom/getUids"; | ||
| import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; | ||
| import getDiscourseNodes from "./getDiscourseNodes"; | ||
|
|
||
| export const renderNodeTagPopupButton = ( | ||
| parent: HTMLSpanElement, | ||
| extensionAPI: OnloadArgs["extensionAPI"], | ||
| ) => { | ||
| if (parent.dataset.attributeButtonRendered === "true") return; | ||
|
|
||
| parent.dataset.attributeButtonRendered = "true"; | ||
| const wrapper = document.createElement("span"); | ||
| wrapper.style.position = "relative"; | ||
| wrapper.style.display = "inline-block"; | ||
| parent.parentNode?.insertBefore(wrapper, parent); | ||
| wrapper.appendChild(parent); | ||
|
|
||
| const reactRoot = document.createElement("span"); | ||
| reactRoot.style.position = "absolute"; | ||
| reactRoot.style.top = "0"; | ||
| reactRoot.style.left = "0"; | ||
| reactRoot.style.width = "100%"; | ||
| reactRoot.style.height = "100%"; | ||
| reactRoot.style.pointerEvents = "auto"; | ||
| reactRoot.style.zIndex = "10"; | ||
|
|
||
| wrapper.appendChild(reactRoot); | ||
|
|
||
| const textContent = parent.textContent?.trim() || ""; | ||
| const tagAttr = parent.getAttribute("data-tag") || textContent; | ||
| const tag = tagAttr.replace(/^#/, "").toLowerCase(); | ||
| const discourseNodes = getDiscourseNodes(); | ||
| const discourseTagSet = new Set( | ||
| discourseNodes.map((n) => n.tag?.toLowerCase()).filter(Boolean), | ||
| ); | ||
| if (!discourseTagSet.has(tag)) return; | ||
|
|
||
| const matchedNode = discourseNodes.find((n) => n.tag?.toLowerCase() === tag); | ||
|
|
||
| if (!matchedNode) return; | ||
|
|
||
| const blockInputElement = parent.closest(".rm-block__input"); | ||
| const blockUid = blockInputElement | ||
| ? getUids(blockInputElement as HTMLDivElement).blockUid | ||
| : undefined; | ||
|
|
||
| const rawBlockText = blockUid ? getTextByBlockUid(blockUid) : ""; | ||
| const cleanedBlockText = rawBlockText.replace(textContent, "").trim(); | ||
|
|
||
| ReactDOM.render( | ||
| <Popover | ||
| content={ | ||
| <Button | ||
| minimal | ||
| outlined | ||
| onClick={() => { | ||
| renderCreateNodeDialog({ | ||
| onClose: () => {}, | ||
| defaultNodeTypeUid: matchedNode.type, | ||
| extensionAPI, | ||
| sourceBlockUid: blockUid, | ||
| initialTitle: cleanedBlockText, | ||
| }); | ||
| }} | ||
| text={`Create ${matchedNode.text}`} | ||
| /> | ||
| } | ||
| target={ | ||
| <span | ||
| style={{ | ||
| display: "block", | ||
| width: "100%", | ||
| height: "100%", | ||
| }} | ||
| /> | ||
| } | ||
| interactionKind="hover" | ||
| position={Position.TOP} | ||
| modifiers={{ | ||
| offset: { | ||
| offset: "0, 10", | ||
| }, | ||
| arrow: { | ||
| enabled: false, | ||
| }, | ||
| }} | ||
| />, | ||
| reactRoot, | ||
| ); | ||
| }; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.