-
Notifications
You must be signed in to change notification settings - Fork 6
ENG-1855 Add Roam shared-node import discovery #1214
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
base: main
Are you sure you want to change the base?
Changes from all commits
da4d214
b805ddf
6d2b17a
aba903d
9f7f93e
2fa228c
d70f4d1
f35b7ca
a587350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| import { | ||
| Button, | ||
| Callout, | ||
| Classes, | ||
| Dialog, | ||
| HTMLTable, | ||
| InputGroup, | ||
| Intent, | ||
| NonIdealState, | ||
| Spinner, | ||
| Tag, | ||
| Tooltip, | ||
| } from "@blueprintjs/core"; | ||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | ||
| import createOverlayRender from "roamjs-components/util/createOverlayRender"; | ||
| import { | ||
| discoverSharedNodes, | ||
| type DiscoveredSharedNode, | ||
| } from "~/utils/discoverSharedNodes"; | ||
| import internalError from "~/utils/internalError"; | ||
| import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; | ||
|
|
||
| const formatModifiedAt = (modifiedAt: string): string => | ||
| new Date(modifiedAt).toLocaleString(); | ||
|
|
||
| const SharedNodeRow = ({ node }: { node: DiscoveredSharedNode }) => ( | ||
| <tr> | ||
| <td> | ||
| <Tag minimal>{node.sourceApp}</Tag> | ||
| </td> | ||
| <td> | ||
| <div className="max-w-52 font-medium [overflow-wrap:anywhere]"> | ||
| {node.sourceSpaceName} | ||
| </div> | ||
| <div | ||
| className={[ | ||
| Classes.MONOSPACE_TEXT, | ||
| Classes.TEXT_MUTED, | ||
| "max-w-52 truncate text-xs", | ||
| ].join(" ")} | ||
| title={node.sourceSpaceId} | ||
| > | ||
| {node.sourceSpaceId} | ||
| </div> | ||
| </td> | ||
| <td> | ||
| <div className="max-w-72 font-medium [overflow-wrap:anywhere]"> | ||
| {node.title} | ||
| </div> | ||
| </td> | ||
| <td> | ||
| {node.sourceNodeId ? ( | ||
| <div | ||
| className={[Classes.MONOSPACE_TEXT, "max-w-44 truncate text-xs"].join( | ||
| " ", | ||
| )} | ||
| title={node.sourceNodeRid} | ||
| > | ||
| {node.sourceNodeId} | ||
| </div> | ||
| ) : ( | ||
| <span className={Classes.TEXT_MUTED}>Not provided</span> | ||
| )} | ||
| </td> | ||
| <td className="whitespace-nowrap" title={node.modifiedAt}> | ||
| {formatModifiedAt(node.modifiedAt)} | ||
| </td> | ||
| <td> | ||
| {node.alreadyImported ? ( | ||
| <Tag intent={Intent.SUCCESS} minimal> | ||
| Imported | ||
| </Tag> | ||
| ) : ( | ||
| <Tag minimal>Available</Tag> | ||
| )} | ||
| </td> | ||
| </tr> | ||
| ); | ||
|
|
||
| const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { | ||
| const [nodes, setNodes] = useState<DiscoveredSharedNode[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState(""); | ||
| const [searchTerm, setSearchTerm] = useState(""); | ||
|
|
||
| const loadNodes = useCallback(async (): Promise<void> => { | ||
| setLoading(true); | ||
| setError(""); | ||
| try { | ||
| const context = await getSupabaseContext(); | ||
| if (!context) throw new Error("Could not connect to shared persistence."); | ||
|
sid597 marked this conversation as resolved.
|
||
| const client = await getLoggedInClient(); | ||
| if (!client) throw new Error("Could not connect to shared persistence."); | ||
| setNodes( | ||
| await discoverSharedNodes({ | ||
| client, | ||
| currentSpaceId: context.spaceId, | ||
| }), | ||
| ); | ||
| } catch (loadError) { | ||
| internalError({ | ||
| error: loadError, | ||
| type: "Shared node discovery failed", | ||
| context: { operation: "load-shared-nodes" }, | ||
| sendEmail: false, | ||
| }); | ||
| setError( | ||
| loadError instanceof Error | ||
| ? loadError.message | ||
| : "Could not load shared nodes.", | ||
| ); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| void loadNodes(); | ||
| }, [loadNodes]); | ||
|
|
||
| const visibleNodes = useMemo(() => { | ||
| const normalizedSearch = searchTerm.trim().toLocaleLowerCase(); | ||
| if (!normalizedSearch) return nodes; | ||
| return nodes.filter((node) => | ||
| [ | ||
| node.sourceApp, | ||
| node.sourceSpaceName, | ||
| node.sourceSpaceId, | ||
| node.title, | ||
| node.sourceNodeId, | ||
| ].some((value) => value?.toLocaleLowerCase().includes(normalizedSearch)), | ||
| ); | ||
| }, [nodes, searchTerm]); | ||
|
|
||
| return ( | ||
| <Dialog | ||
| autoFocus={false} | ||
|
Collaborator
Author
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. Roam's command palette dispatches commands on Enter keyup. Without |
||
| canEscapeKeyClose | ||
| canOutsideClickClose | ||
| enforceFocus={false} | ||
| style={{ width: "min(68rem, calc(100vw - 2rem))" }} | ||
| isOpen | ||
| onClose={onClose} | ||
| title="Discover shared nodes" | ||
| > | ||
| <div | ||
| className={[Classes.DIALOG_BODY, "flex min-h-72 flex-col gap-3"].join( | ||
| " ", | ||
| )} | ||
| > | ||
| <div className="flex items-center gap-2"> | ||
| <InputGroup | ||
| className="min-w-0 flex-1" | ||
| leftIcon="search" | ||
| onChange={(event: React.ChangeEvent<HTMLInputElement>) => | ||
| setSearchTerm(event.target.value) | ||
| } | ||
| placeholder="Search shared nodes" | ||
| value={searchTerm} | ||
| /> | ||
| <Tooltip content="Reload shared nodes"> | ||
| <Button | ||
| aria-label="Reload shared nodes" | ||
| disabled={loading} | ||
| icon="refresh" | ||
| minimal | ||
| onClick={() => void loadNodes()} | ||
| /> | ||
| </Tooltip> | ||
| </div> | ||
|
|
||
| {loading ? ( | ||
| <div className="flex min-h-52 items-center justify-center"> | ||
| <Spinner /> | ||
| </div> | ||
| ) : error ? ( | ||
| <Callout intent={Intent.DANGER} title="Could not load shared nodes"> | ||
| <div className="mb-3">{error}</div> | ||
| <Button icon="refresh" onClick={() => void loadNodes()}> | ||
| Try again | ||
| </Button> | ||
| </Callout> | ||
| ) : visibleNodes.length === 0 ? ( | ||
| <div className="flex min-h-52 items-center justify-center"> | ||
| <NonIdealState | ||
| icon="search" | ||
| title={ | ||
| searchTerm ? "No matching shared nodes" : "No shared nodes" | ||
| } | ||
| /> | ||
| </div> | ||
| ) : ( | ||
| <div className="min-h-0 overflow-auto"> | ||
| <HTMLTable striped className="w-full"> | ||
| <thead> | ||
| <tr> | ||
| <th>Source app</th> | ||
|
Collaborator
Author
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. This stays intentionally read-only: these columns match ENG-1855's pre-import metadata, while materialization remains in ENG-1859. |
||
| <th>Source space</th> | ||
| <th>Title</th> | ||
| <th>Source ID</th> | ||
| <th>Modified</th> | ||
| <th>Status</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {visibleNodes.map((node) => ( | ||
| <SharedNodeRow key={node.sourceNodeRid} node={node} /> | ||
| ))} | ||
| </tbody> | ||
| </HTMLTable> | ||
| </div> | ||
| )} | ||
| </div> | ||
| <div className={Classes.DIALOG_FOOTER}> | ||
| <div className="flex items-center justify-between"> | ||
| <span className={[Classes.TEXT_MUTED, "text-xs"].join(" ")}> | ||
| {loading || error | ||
| ? "" | ||
| : `${visibleNodes.length} of ${nodes.length} nodes`} | ||
| </span> | ||
| <Button onClick={onClose}>Close</Button> | ||
| </div> | ||
| </div> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
|
|
||
| type Props = Record<string, never>; | ||
|
|
||
| export const renderDiscoverSharedNodesDialog = createOverlayRender<Props>( | ||
| "discourse-discover-shared-nodes", | ||
| DiscoverSharedNodesDialog, | ||
| ); | ||
|
|
||
| export default DiscoverSharedNodesDialog; | ||
Uh oh!
There was an error while loading. Please reload this page.