web: show references & linked notes in properties

This commit is contained in:
Abdullah Atta
2024-01-24 16:22:28 +05:00
parent 63ae8a4a86
commit 04d66576cb
2 changed files with 324 additions and 5 deletions

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { PropsWithChildren } from "react";
import React, { PropsWithChildren, useRef, useState } from "react";
import {
Pin,
StarOutline,
@@ -26,9 +26,11 @@ import {
SyncOff,
ArrowLeft,
Circle,
Checkmark
Checkmark,
ChevronDown,
ChevronRight
} from "../icons";
import { Flex, Text } from "@theme-ui/components";
import { Box, Button, Flex, Text } from "@theme-ui/components";
import {
useEditorStore,
ReadonlyEditorSession,
@@ -40,12 +42,24 @@ import { store as noteStore } from "../../stores/note-store";
import { AnimatedFlex } from "../animated";
import Toggle from "./toggle";
import ScrollContainer from "../scroll-container";
import { ResolvedItem, getFormattedDate, usePromise } from "@notesnook/common";
import {
getFormattedDate,
usePromise,
ResolvedItem,
useResolvedItem
} from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
import { ListItemWrapper } from "../list-container/list-profiles";
import { VirtualizedList } from "../virtualized-list";
import { SessionItem } from "../session-item";
import {
ContentBlock,
Note,
VirtualizedGrouping,
highlightInternalLinks
} from "@notesnook/core";
import { VirtualizedTable } from "../virtualized-table";
import { TextSlice } from "@notesnook/core/dist/utils/content-block";
const tools = [
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
@@ -117,6 +131,7 @@ function EditorProperties(props: EditorPropertiesProps) {
sx={{
display: "flex",
position: "absolute",
top: 0,
right: 0,
zIndex: 1,
height: "100%",
@@ -185,6 +200,7 @@ function EditorProperties(props: EditorPropertiesProps) {
))}
<Colors noteId={id} color={session.color} />
</Section>
<InternalLinks noteId={id} />
<Notebooks noteId={id} />
<Reminders noteId={id} />
<Attachments noteId={id} />
@@ -196,6 +212,308 @@ function EditorProperties(props: EditorPropertiesProps) {
}
export default React.memo(EditorProperties);
enum InternalLinksTabs {
LINKED_NOTES = 0,
REFERENCED_IN = 1
}
function InternalLinks({ noteId }: { noteId: string }) {
const [tabIndex, setTabIndex] = useState(InternalLinksTabs.LINKED_NOTES);
const [expandedId, setExpandedId] = useState<string>();
const result = usePromise(() => {
const links =
tabIndex === InternalLinksTabs.LINKED_NOTES
? db.relations.from({ id: noteId, type: "note" }, "note")
: db.relations.to({ id: noteId, type: "note" }, "note");
return links.selector.sorted(db.settings.getGroupOptions("notes"));
}, [tabIndex]);
return (
<Flex sx={{ flexDirection: "column", mt: 2 }}>
<Flex
sx={{
justifyContent: "stretch",
borderRadius: "default",
overflow: "hidden",
mx: 2,
mb: 1
}}
>
{["Linked notes", "Referenced in"].map((title, index) => (
<Button
key={title}
variant="secondary"
sx={{
flex: 1,
borderRadius: 0,
color: tabIndex === index ? "accent-selected" : "paragraph",
bg:
tabIndex === index
? "background-selected"
: "background-secondary"
}}
onClick={() => {
setTabIndex(index);
setExpandedId(undefined);
}}
>
{title}
</Button>
))}
</Flex>
{result.status === "fulfilled" && (
<VirtualizedList
mode="dynamic"
estimatedSize={25}
getItemKey={(index) => result.value.key(index)}
items={result.value.placeholders}
context={{
items: result.value,
tabIndex,
noteId,
isExpanded: (id) => expandedId === id,
toggleExpand: (id) =>
setExpandedId((s) => (s === id ? undefined : id))
}}
renderItem={InternalLinkItem}
/>
)}
</Flex>
);
}
function InternalLinkItem({
index,
context
}: {
item: boolean;
index: number;
context: {
items: VirtualizedGrouping<Note>;
tabIndex: InternalLinksTabs;
noteId: string;
isExpanded: (id: string) => boolean;
toggleExpand: (id: string) => void;
};
}) {
const { items, tabIndex, noteId, isExpanded, toggleExpand } = context;
const item = useResolvedItem({ items, index });
if (!item || item.item.type !== "note") return null;
if (tabIndex === InternalLinksTabs.LINKED_NOTES)
return (
<LinkedNote
item={item.item}
noteId={noteId}
isExpanded={isExpanded(item.item.id)}
toggleExpand={() => toggleExpand(item.item.id)}
/>
);
return (
<ReferencedIn
item={item.item}
noteId={noteId}
isExpanded={isExpanded(item.item.id)}
toggleExpand={() => toggleExpand(item.item.id)}
/>
);
}
function LinkedNote({
item,
noteId,
isExpanded,
toggleExpand
}: {
item: Note;
noteId: string;
toggleExpand: () => void;
isExpanded: boolean;
}) {
const [blocks, setBlocks] = useState<ContentBlock[]>([]);
return (
<>
<Button
variant="menuitem"
sx={{
p: 1,
width: "100%",
textAlign: "left",
display: "flex",
justifyContent: "start",
alignItems: "center"
}}
onClick={() => useEditorStore.getState().openSession(item)}
>
<Box
onClick={async (e) => {
e.stopPropagation();
if (isExpanded) return toggleExpand();
const blocks = await db.notes.contentBlocks(item.id);
const linkedBlocks = (await db.notes.internalLinks(noteId)).filter(
(l) => l.id === item.id
);
setBlocks(
linkedBlocks.length > 0
? blocks.filter((a) =>
linkedBlocks.some((l) => l.params?.blockId === a.id)
)
: []
);
toggleExpand();
}}
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</Box>
<Text>{item.title}</Text>
</Button>
{isExpanded
? blocks.map((block) => (
<Button
key={block.id}
variant="menuitem"
sx={{
p: 1,
pl: 4,
gap: 1,
width: "100%",
textAlign: "left",
display: "flex",
alignItems: "center"
}}
onClick={() =>
useEditorStore
.getState()
.openSession(item, { activeBlockId: block.id })
}
>
<Text
variant="subBody"
sx={{
bg: "background-secondary",
p: "small",
px: 1,
borderRadius: "default",
alignSelf: "flex-start"
}}
>
{block.type.toUpperCase()}
</Text>
<Text
variant="body"
sx={{
fontFamily: "monospace",
whiteSpace: "pre-wrap"
}}
>
{block.content}
</Text>
</Button>
))
: null}
</>
);
}
function ReferencedIn({
item,
noteId,
isExpanded,
toggleExpand
}: {
item: Note;
noteId: string;
toggleExpand: () => void;
isExpanded: boolean;
}) {
const [blocks, setBlocks] = useState<
{ id: string; links: [TextSlice, TextSlice, TextSlice][] }[]
>([]);
return (
<>
<Button
variant="menuitem"
sx={{
p: 1,
width: "100%",
textAlign: "left",
display: "flex",
justifyContent: "start",
alignItems: "center"
}}
onClick={() => useEditorStore.getState().openSession(item)}
>
<Box
onClick={async (e) => {
e.stopPropagation();
if (isExpanded) return toggleExpand();
const blocks = await db.notes.contentBlocks(item.id);
setBlocks(
blocks
.filter((b) => b.content.includes(`nn://note/${noteId}`))
.map((block) => ({
id: block.id,
links: highlightInternalLinks(block, noteId)
}))
);
toggleExpand();
}}
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</Box>
<Text>{item.title}</Text>
</Button>
{isExpanded
? blocks.map((block) => (
<>
{block.links.map((link, index) => (
<Button
key={index.toString()}
variant="menuitem"
sx={{
p: 1,
pl: 4,
pr: 2,
gap: 1,
width: "100%",
textAlign: "left",
borderBottom: "1px solid var(--border)"
}}
onClick={() =>
useEditorStore
.getState()
.openSession(item, { activeBlockId: block.id })
}
>
{link.map((slice) => (
<Text
key={slice.text}
sx={{
color: slice.highlighted
? "accent-selected"
: "paragraph",
fontWeight: slice.highlighted ? "bold" : "normal",
textDecoration: slice.highlighted
? "underline solid var(--accent-selected)"
: "none"
}}
>
{slice.text}
</Text>
))}
</Button>
))}
</>
))
: null}
</>
);
}
function Colors({ noteId, color }: { noteId: string; color?: string }) {
const result = usePromise(() => db.colors.all.items(), [color]);
return (
@@ -268,6 +586,7 @@ function Notebooks({ noteId }: { noteId: string }) {
</Section>
);
}
function Reminders({ noteId }: { noteId: string }) {
const result = usePromise(() =>
db.relations

View File

@@ -162,7 +162,7 @@ export default function NoteLinkingDialog(props: NoteLinkingDialogProps) {
sx={{ p: 1, width: "100%", textAlign: "left" }}
onClick={async () => {
setSelectedNote(note);
setBlocks(await db.notes.getBlocks(note.id));
setBlocks(await db.notes.contentBlocks(note.id));
}}
>
<Text variant="body">{note.title}</Text>