diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 94a601c53..c6f8e899e 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -99,38 +99,3 @@ body.prefers-centered-content .inline-title { font-weight: 500; } } - -@keyframes note-type-switcher-intro { - from { - opacity: 0; - } to { - opacity: 1; - } -} - -.note-type-switcher { - --badge-radius: 12px; - - position: relative; - top: 5px; - padding: .25em 0; - display: flex; - align-items: center; - overflow-x: auto; - min-width: 0; - gap: 5px; - min-height: 35px; - - >* { - flex-shrink: 0; - animation: note-type-switcher-intro 200ms ease-in; - } - - .ext-badge { - --color: var(--input-background-color); - color: var(--main-text-color); - font-size: 0.9rem; - flex-shrink: 0; - } -} - diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 95cb2d379..42df13bc3 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -3,25 +3,16 @@ import "./InlineTitle.css"; import { NoteType } from "@triliumnext/commons"; import clsx from "clsx"; import { ComponentChild } from "preact"; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; +import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; -import FNote from "../../entities/fnote"; -import attributes from "../../services/attributes"; -import froca from "../../services/froca"; -import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; -import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types"; -import server from "../../services/server"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; -import { Badge, BadgeWithDropdown } from "../react/Badge"; -import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks"; +import { useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; -import { onWheelHorizontalScroll } from "../widget_utils"; const supportedNoteTypes = new Set([ "text", "code" @@ -73,8 +64,6 @@ export default function InlineTitle() { - - ); } @@ -138,168 +127,4 @@ function TextWithValue({ i18nKey, value, valueTooltip }: { } //#endregion -//#region Note type switcher -const SWITCHER_PINNED_NOTE_TYPES = new Set([ "text", "code", "book", "canvas" ]); -function NoteTypeSwitcher() { - const { note } = useNoteContext(); - const blob = useNoteBlob(note); - const currentNoteType = useNoteProperty(note, "type"); - const { pinnedNoteTypes, restNoteTypes } = useMemo(() => { - const pinnedNoteTypes: NoteTypeMapping[] = []; - const restNoteTypes: NoteTypeMapping[] = []; - for (const noteType of NOTE_TYPES) { - if (noteType.reserved || noteType.static || noteType.type === "book") continue; - if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) { - pinnedNoteTypes.push(noteType); - } else { - restNoteTypes.push(noteType); - } - } - return { pinnedNoteTypes, restNoteTypes }; - }, []); - const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); - const { builtinTemplates, collectionTemplates } = useBuiltinTemplates(); - - return (currentNoteType && supportedNoteTypes.has(currentNoteType) && -
- {note && blob?.contentLength === 0 && ( - <> -
{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
- {pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && ( - switchNoteType(note.noteId, noteType)} - /> - ))} - {collectionTemplates.length > 0 && } - {builtinTemplates.length > 0 && } - {restNoteTypes.length > 0 && } - - )} -
- ); -} - -function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) { - return ( - - {restNoteTypes.map(noteType => ( - switchNoteType(noteId, noteType)} - >{noteType.title} - ))} - - ); -} - -function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) { - return ( - - {collectionTemplates.map(collectionTemplate => ( - setTemplate(noteId, collectionTemplate.noteId)} - >{collectionTemplate.title} - ))} - - ); -} - -function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) { - const [ userTemplates, setUserTemplates ] = useState([]); - - async function refreshTemplates() { - const templateNoteIds = await server.get("search-templates"); - const templateNotes = await froca.getNotes(templateNoteIds); - setUserTemplates(templateNotes); - } - - // First load. - useEffect(() => { - refreshTemplates(); - }, []); - - // React to external changes. - useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) { - refreshTemplates(); - } - }); - - return ( - - {userTemplates.map(template => )} - {userTemplates.length > 0 && } - {builtinTemplates.map(template => )} - - ); -} - -function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) { - return ( - setTemplate(noteId, template.noteId)} - >{template.title} - ); -} - -function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { - return server.put(`notes/${noteId}/type`, { type, mime }); -} - -function setTemplate(noteId: string, templateId: string) { - return attributes.setRelation(noteId, "template", templateId); -} - -function useBuiltinTemplates() { - const [ templates, setTemplates ] = useState<{ - builtinTemplates: FNote[]; - collectionTemplates: FNote[]; - }>({ - builtinTemplates: [], - collectionTemplates: [] - }); - - async function loadBuiltinTemplates() { - const templatesRoot = await froca.getNote("_templates"); - if (!templatesRoot) return; - const childNotes = await templatesRoot.getChildNotes(); - const builtinTemplates: FNote[] = []; - const collectionTemplates: FNote[] = []; - for (const childNote of childNotes) { - if (!childNote.hasLabel("template")) continue; - if (childNote.hasLabel("collection")) { - collectionTemplates.push(childNote); - } else { - builtinTemplates.push(childNote); - } - } - setTemplates({ builtinTemplates, collectionTemplates }); - } - - useEffect(() => { - loadBuiltinTemplates(); - }, []); - - return templates; -} -//#endregion diff --git a/apps/client/src/widgets/layout/NoteTitleActions.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx index 52f70a991..26ec8e553 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -15,6 +15,7 @@ import { useNoteContext, useNoteLabel, useNoteProperty, useTriliumEvent, useTril import NoteLink from "../react/NoteLink"; import { useEditedNotes } from "../ribbon/EditedNotesTab"; import SearchDefinitionTab from "../ribbon/SearchDefinitionTab"; +import NoteTypeSwitcher from "./NoteTypeSwitcher"; export default function NoteTitleActions() { const { note, ntxId, componentId, noteContext } = useNoteContext(); @@ -27,6 +28,7 @@ export default function NoteTitleActions() { {noteType === "search" && } {!isHiddenNote && note && noteType === "book" && } + ); } diff --git a/apps/client/src/widgets/layout/NoteTypeSwitcher.css b/apps/client/src/widgets/layout/NoteTypeSwitcher.css new file mode 100644 index 000000000..574924d03 --- /dev/null +++ b/apps/client/src/widgets/layout/NoteTypeSwitcher.css @@ -0,0 +1,33 @@ +@keyframes note-type-switcher-intro { + from { + opacity: 0; + } to { + opacity: 1; + } +} + +.note-type-switcher { + --badge-radius: 12px; + + position: relative; + top: 5px; + padding: .25em 0; + display: flex; + align-items: center; + overflow-x: auto; + min-width: 0; + gap: 5px; + min-height: 35px; + + >* { + flex-shrink: 0; + animation: note-type-switcher-intro 200ms ease-in; + } + + .ext-badge { + --color: var(--input-background-color); + color: var(--main-text-color); + font-size: 0.9rem; + flex-shrink: 0; + } +} diff --git a/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx b/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx new file mode 100644 index 000000000..60e597bb0 --- /dev/null +++ b/apps/client/src/widgets/layout/NoteTypeSwitcher.tsx @@ -0,0 +1,182 @@ +import "./NoteTypeSwitcher.css"; + +import { NoteType } from "@triliumnext/commons"; +import { useEffect, useMemo, useState } from "preact/hooks"; + +import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; +import froca from "../../services/froca"; +import { t } from "../../services/i18n"; +import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types"; +import server from "../../services/server"; +import { Badge, BadgeWithDropdown } from "../react/Badge"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { useNoteBlob, useNoteContext, useNoteProperty, useTriliumEvent } from "../react/hooks"; +import { onWheelHorizontalScroll } from "../widget_utils"; + +const SWITCHER_PINNED_NOTE_TYPES = new Set([ "text", "code", "book", "canvas" ]); +const supportedNoteTypes = new Set([ + "text", "code" +]); + +export default function NoteTypeSwitcher() { + const { note } = useNoteContext(); + const blob = useNoteBlob(note); + const currentNoteType = useNoteProperty(note, "type"); + const { pinnedNoteTypes, restNoteTypes } = useMemo(() => { + const pinnedNoteTypes: NoteTypeMapping[] = []; + const restNoteTypes: NoteTypeMapping[] = []; + for (const noteType of NOTE_TYPES) { + if (noteType.reserved || noteType.static || noteType.type === "book") continue; + if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) { + pinnedNoteTypes.push(noteType); + } else { + restNoteTypes.push(noteType); + } + } + return { pinnedNoteTypes, restNoteTypes }; + }, []); + const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); + const { builtinTemplates, collectionTemplates } = useBuiltinTemplates(); + + return (currentNoteType && supportedNoteTypes.has(currentNoteType) && +
+ {note && blob?.contentLength === 0 && ( + <> +
{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
+ {pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && ( + switchNoteType(note.noteId, noteType)} + /> + ))} + {collectionTemplates.length > 0 && } + {builtinTemplates.length > 0 && } + {restNoteTypes.length > 0 && } + + )} +
+ ); +} + +function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) { + return ( + + {restNoteTypes.map(noteType => ( + switchNoteType(noteId, noteType)} + >{noteType.title} + ))} + + ); +} + +function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) { + return ( + + {collectionTemplates.map(collectionTemplate => ( + setTemplate(noteId, collectionTemplate.noteId)} + >{collectionTemplate.title} + ))} + + ); +} + +function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) { + const [ userTemplates, setUserTemplates ] = useState([]); + + async function refreshTemplates() { + const templateNoteIds = await server.get("search-templates"); + const templateNotes = await froca.getNotes(templateNoteIds); + setUserTemplates(templateNotes); + } + + // First load. + useEffect(() => { + refreshTemplates(); + }, []); + + // React to external changes. + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) { + refreshTemplates(); + } + }); + + return ( + + {userTemplates.map(template => )} + {userTemplates.length > 0 && } + {builtinTemplates.map(template => )} + + ); +} + +function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) { + return ( + setTemplate(noteId, template.noteId)} + >{template.title} + ); +} + +function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { + return server.put(`notes/${noteId}/type`, { type, mime }); +} + +function setTemplate(noteId: string, templateId: string) { + return attributes.setRelation(noteId, "template", templateId); +} + +function useBuiltinTemplates() { + const [ templates, setTemplates ] = useState<{ + builtinTemplates: FNote[]; + collectionTemplates: FNote[]; + }>({ + builtinTemplates: [], + collectionTemplates: [] + }); + + async function loadBuiltinTemplates() { + const templatesRoot = await froca.getNote("_templates"); + if (!templatesRoot) return; + const childNotes = await templatesRoot.getChildNotes(); + const builtinTemplates: FNote[] = []; + const collectionTemplates: FNote[] = []; + for (const childNote of childNotes) { + if (!childNote.hasLabel("template")) continue; + if (childNote.hasLabel("collection")) { + collectionTemplates.push(childNote); + } else { + builtinTemplates.push(childNote); + } + } + setTemplates({ builtinTemplates, collectionTemplates }); + } + + useEffect(() => { + loadBuiltinTemplates(); + }, []); + + return templates; +}