diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index a56139d36..d33ba76a0 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -46,17 +46,7 @@ export async function executeBundle(bundle: Bundle, originEntity?: Entity | null return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext); } catch (e: any) { - const note = await froca.getNote(bundle.noteId); - toastService.showPersistent({ - id: `custom-script-failure-${note?.noteId}`, - title: t("toast.bundle-error.title"), - icon: "bx bx-error-circle", - message: t("toast.bundle-error.message", { - id: note?.noteId, - title: note?.title, - message: e.message - }) - }); + showErrorForScriptNote(bundle.noteId, t("toast.bundle-error.message", { message: e.message })); logError("Widget initialization failed: ", e); } } @@ -151,17 +141,7 @@ async function getWidgetBundlesByParent() { } } catch (e: any) { const noteId = bundle.noteId; - const note = await froca.getNote(noteId); - toastService.showPersistent({ - id: `custom-script-failure-${noteId}`, - title: t("toast.bundle-error.title"), - icon: "bx bx-error-circle", - message: t("toast.bundle-error.message", { - id: noteId, - title: note?.title, - message: e.message - }) - }); + showErrorForScriptNote(noteId, t("toast.bundle-error.message", { message: e.message })); logError("Widget initialization failed: ", e); continue; diff --git a/apps/client/src/services/toast.ts b/apps/client/src/services/toast.ts index 641b3cdce..f31e242cd 100644 --- a/apps/client/src/services/toast.ts +++ b/apps/client/src/services/toast.ts @@ -69,7 +69,7 @@ export async function showErrorForScriptNote(noteId: string, message: string) { showPersistent({ id: `custom-widget-failure-${noteId}`, - title: note?.title ?? "", + title: t("toast.scripting-error", { title: note?.title ?? "" }), icon: note?.getIcon() ?? "bx bx-error-circle", message, timeout: 15_000, diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 75e116753..9cfc8b2e1 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -21,7 +21,7 @@ }, "bundle-error": { "title": "Failed to load a custom script", - "message": "Script from note with ID \"{{id}}\", titled \"{{title}}\" could not be executed due to:\n\n{{message}}" + "message": "Script could not be executed due to:\n\n{{message}}" }, "widget-list-error": { "title": "Failed to obtain the list of widgets from the server" @@ -30,7 +30,8 @@ "title": "Failed to render a custom React widget" }, "widget-missing-parent": "Custom widget does not have mandatory '{{property}}' property defined.", - "open-script-note": "Open script note" + "open-script-note": "Open script note", + "scripting-error": "Custom script error: {{title}}" }, "add_link": { "add_link": "Add link", @@ -1779,7 +1780,8 @@ "note_type_switcher_others": "Other note type", "note_type_switcher_templates": "Template", "note_type_switcher_collection": "Collection", - "edited_notes": "Edited notes" + "edited_notes": "Notes edited on this day", + "promoted_attributes": "Promoted attributes" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx index 07ecf6b66..42b461df0 100644 --- a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx @@ -15,7 +15,7 @@ export default function TabHistoryNavigationButtons() { const legacyBackVisible = useLauncherVisibility("_lbBackInHistory"); const legacyForwardVisible = useLauncherVisibility("_lbForwardInHistory"); - return (isElectron() && + return (
{!legacyBackVisible && .scrolling-container { - background-color: var(--code-background-color); - --scrollbar-background-color: var(--main-background-color); +.note-split.type-code:not(.mime-text-x-sqlite) { + &> .scrolling-container { + background-color: var(--code-background-color); + --scrollbar-background-color: var(--main-background-color); + } + + .inline-title, + .title-actions { + background-color: var(--main-background-color); + } } diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index bc1494d3d..b4030faae 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -32,7 +32,7 @@ import { ParentComponent } from "../react/react_utils"; const COLLAPSE_THRESHOLD = 5; const INITIAL_ITEMS = 2; -const FINAL_ITEMS = 2; +const FINAL_ITEMS = 3; export default function Breadcrumb() { const { note, notePath, notePaths, noteContext } = useNotePaths(); @@ -65,8 +65,7 @@ export default function Breadcrumb() { ? : } - {(index < notePaths.length - 1 || note?.hasChildren()) && - } + )) )} @@ -226,7 +225,7 @@ function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNoteP ; })} - + {childNotes.length > 0 && } note_create.createNote(notePath, { activate: true })} diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 613c845e3..295bbd102 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -75,10 +75,6 @@ } } -.note-split.type-code:not(.mime-text-x-sqlite) .inline-title { - background-color: var(--main-background-color); -} - body.prefers-centered-content .inline-title { margin-inline: auto; } @@ -99,58 +95,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; - } -} - -.edited-notes { - padding: 1.5em 0; - - .collapsible-inner-body { - display: flex; - flex-wrap: wrap; - gap: 0.3em; - - .badge { - margin: 0; - a.tn-link { - color: inherit; - text-transform: none; - text-decoration: none; - display: inline-block; - } - } - } - -} diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index bb043cca2..42df13bc3 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -3,28 +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 SimpleBadge, { Badge, BadgeWithDropdown } from "../react/Badge"; -import Collapsible from "../react/Collapsible"; -import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useNoteBlob, useNoteContext, useNoteLabel, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumOptionBool } from "../react/hooks"; -import NoteLink from "../react/NoteLink"; +import { useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; -import { useEditedNotes } from "../ribbon/EditedNotesTab"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; -import { onWheelHorizontalScroll } from "../widget_utils"; const supportedNoteTypes = new Set([ "text", "code" @@ -76,9 +64,6 @@ export default function InlineTitle() {
- - - ); } @@ -142,205 +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 - -//#region Edited Notes -function EditedNotes() { - const { note } = useNoteContext(); - const [ dateNote ] = useNoteLabel(note, "dateNote"); - const [ editedNotesOpenInRibbon ] = useTriliumOptionBool("editedNotesOpenInRibbon"); - - return (note && dateNote && - - - - ); -} - -function EditedNotesContent({ note }: { note: FNote }) { - const editedNotes = useEditedNotes(note); - - return (editedNotes !== undefined && - (editedNotes.length > 0 ? editedNotes?.map(editedNote => ( - - )} - /> - )) : ( -
{t("edited_notes.no_edited_notes_found")}
- ))); -} -//#endregion diff --git a/apps/client/src/widgets/layout/NoteBadges.css b/apps/client/src/widgets/layout/NoteBadges.css index 39fafd90e..8de12b810 100644 --- a/apps/client/src/widgets/layout/NoteBadges.css +++ b/apps/client/src/widgets/layout/NoteBadges.css @@ -16,6 +16,13 @@ &.share-badge {--color: var(--badge-share-background-color);} &.clipped-note-badge {--color: var(--badge-clipped-note-background-color);} &.execute-badge {--color: var(--badge-execute-background-color);} + min-width: 0; + + .text { + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + } } .dropdown-badge { diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index 7d025ef87..db2810c1e 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -4,8 +4,41 @@ body.experimental-feature-new-layout { } .title-actions { - &.visible:not(:empty) { + display: flex; + flex-direction: column; + gap: 0.5em; + + &:not(:empty) { padding: 0.75em 15px; } + + .edited-notes { + .collapsible-inner-body { + display: flex; + flex-wrap: wrap; + gap: 0.3em; + + .badge { + margin: 0; + color: inherit; + text-transform: none; + text-decoration: none; + display: inline-block; + transition: background-color 250ms ease-in, color 250ms ease-in; + + &:hover { + background-color: var(--link-hover-background); + color: var(--link-hover-color); + } + } + } + } + + .promoted-attributes-widget { + .promoted-attributes-container { + margin: 0; + padding: 0; + } + } } } diff --git a/apps/client/src/widgets/layout/NoteTitleActions.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx index 2e13c5f10..dd1ed2342 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -1,7 +1,7 @@ import "./NoteTitleActions.css"; import clsx from "clsx"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; @@ -9,29 +9,31 @@ import { t } from "../../services/i18n"; import CollectionProperties from "../note_bars/CollectionProperties"; import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail"; import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes"; +import SimpleBadge from "../react/Badge"; import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible"; -import { useNoteContext, useNoteProperty, useTriliumEvent } from "../react/hooks"; +import { useNoteContext, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOptionBool } from "../react/hooks"; +import NoteLink, { NewNoteLink } 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(); const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_"); const noteType = useNoteProperty(note, "type"); - const items = [ - note && , - note && noteType === "search" && , - note && !isHiddenNote && noteType === "book" && - ].filter(Boolean); - return ( -
0 && "visible")}> - {items} +
+ + {noteType === "search" && } + {!isHiddenNote && note && noteType === "book" && } + +
); } -function SearchProperties({ note, ntxId }: { note: FNote, ntxId: string | null | undefined }) { +function SearchProperties({ note, ntxId }: { note: FNote | null | undefined, ntxId: string | null | undefined }) { return (note && )); } + +//#region Edited Notes +function EditedNotes() { + const { note } = useNoteContext(); + const [ dateNote ] = useNoteLabel(note, "dateNote"); + const [ editedNotesOpenInRibbon ] = useTriliumOptionBool("editedNotesOpenInRibbon"); + + return (note && dateNote && + + + + ); +} + +function EditedNotesContent({ note }: { note: FNote }) { + const editedNotes = useEditedNotes(note); + + return (editedNotes !== undefined && + (editedNotes.length > 0 ? editedNotes?.map(editedNote => ( + + )) : ( +
{t("edited_notes.no_edited_notes_found")}
+ ))); +} +//#endregion 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; +} diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 637661095..cdca69cef 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -37,7 +37,7 @@ body.experimental-feature-new-layout { .title-row { container-type: size; transition: border 400ms ease-out; - + &.note-split-title { border-bottom: 1px solid var(--main-border-color); @@ -56,13 +56,13 @@ body.experimental-feature-new-layout { --note-icon-container-padding-size: 6px; margin-inline: 0; } - + .note-title-widget { --note-title-size: 18px; --note-title-padding-inline: 0; } - @container (max-width: 700px) { + @container (max-width: 600px) { .note-title-widget { --note-title-size: 1.25rem; --note-title-padding-inline: 4px; @@ -80,7 +80,7 @@ body.experimental-feature-new-layout { .ext-badge .text { display: none; } - + .ext-badge { --size: 2em; width: var(--size); diff --git a/apps/client/src/widgets/react/Badge.css b/apps/client/src/widgets/react/Badge.css index cf661b4f9..8eac80b7d 100644 --- a/apps/client/src/widgets/react/Badge.css +++ b/apps/client/src/widgets/react/Badge.css @@ -50,16 +50,30 @@ white-space: nowrap; border-radius: var(--badge-radius); + button, + .btn { + min-width: 0; + overflow: hidden; + } + .ext-badge { border-radius: 0; .text { display: inline-flex; align-items: center; + min-width: 0; + + .text-inner { + min-width: 0; + text-overflow: ellipsis; + overflow: hidden; + } .arrow { font-size: 1.3em; - margin-left: 0.25em; + margin-inline-start: 0.25em; + margin-inline-end: 0; } } } diff --git a/apps/client/src/widgets/react/Badge.tsx b/apps/client/src/widgets/react/Badge.tsx index 6e17ed0ce..48fa7fe09 100644 --- a/apps/client/src/widgets/react/Badge.tsx +++ b/apps/client/src/widgets/react/Badge.tsx @@ -60,7 +60,10 @@ export function BadgeWithDropdown({ text, children, tooltip, className, dropdown {text} } + text={<> + {text} + + } className={className} {...props} />} diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index dec0660c0..d6aab922b 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -2,10 +2,11 @@ import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap"; import { ComponentChildren, HTMLAttributes } from "preact"; import { CSSProperties, HTMLProps } from "preact/compat"; import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; + import { useTooltip, useUniqueName } from "./hooks"; type DataAttributes = { - [key: `data-${string}`]: string | number | boolean | undefined; + [key: `data-${string}`]: string | number | boolean | undefined; }; export interface DropdownProps extends Pick, "id" | "className"> { @@ -66,14 +67,14 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi return () => { resizeObserver.disconnect(); dropdown.dispose(); - } + }; }, []); const onShown = useCallback(() => { setShown(true); externalOnShown?.(); hideTooltip(); - }, [ hideTooltip ]) + }, [ hideTooltip ]); const onHidden = useCallback(() => { setShown(false); @@ -122,7 +123,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi {...buttonProps} > {text} - +
    { + // Prevent clicks directly inside the dropdown from closing. + if (e.target === dropdownContainerRef.current) { + e.stopPropagation(); + } + }} > {shown && children}
- ) + ); } diff --git a/apps/client/src/widgets/react/NoteLink.tsx b/apps/client/src/widgets/react/NoteLink.tsx index a415c8b86..7a86be7ef 100644 --- a/apps/client/src/widgets/react/NoteLink.tsx +++ b/apps/client/src/widgets/react/NoteLink.tsx @@ -105,22 +105,17 @@ export function NewNoteLink({ notePath, viewScope, noContextMenu, showNoteIcon, const [ archived ] = useNoteLabelBoolean(note, "archived"); return ( - - - {icon && } - - - {title} - - - + + {icon && <> } + {title} + ); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 10a5983c6..57b830934 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -369,7 +369,8 @@ export function useActiveNoteContext() { useEffect(() => { setNote(noteContext?.note); - }, [ notePath ]); + setNotePath(noteContext?.notePath); + }, [ notePath, noteContext?.note, noteContext?.notePath ]); useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], () => { const noteContext = appContext.tabManager.getActiveContext() ?? undefined; @@ -634,7 +635,8 @@ export function useLegacyWidget(widgetFactory: () => T, { const renderedWidget = widget.render(); return [ widget, renderedWidget ]; - }, [ noteContext, parentComponent, widgetFactory]); + }, [ noteContext, parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps + // widgetFactory() is intentionally left out // Attach the widget to the parent. useEffect(() => { diff --git a/apps/client/src/widgets/shared_info.tsx b/apps/client/src/widgets/shared_info.tsx index 54c6da785..bbcdbe982 100644 --- a/apps/client/src/widgets/shared_info.tsx +++ b/apps/client/src/widgets/shared_info.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "preact/hooks"; import FNote from "../entities/fnote"; import attributes from "../services/attributes"; import { t } from "../services/i18n"; +import { isElectron } from "../services/utils"; import HelpButton from "./react/HelpButton"; import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks"; import InfoBar from "./react/InfoBar"; @@ -68,7 +69,11 @@ export function useShareInfo(note: FNote | null | undefined) { } }); - return { link, linkHref, isSharedExternally: !!syncServerHost }; + return { + link, + linkHref, + isSharedExternally: !isElectron() || !!syncServerHost // on server we can't reliably detect if the note is shared locally or available publicly. + }; } function getShareId(note: FNote) { diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database/Demo Notes.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database/Demo Notes.html index d3f5a7570..4e60303c0 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database/Demo Notes.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database/Demo Notes.html @@ -1,15 +1,15 @@

When you run Trilium for the first time, it will generate a new database containing demo notes. These notes showcase its many features, such as:

Restoring Demo Notes

@@ -21,10 +21,10 @@

You can easily restore the demo notes by using Trilium's built-in import feature by importing them:

    -
  • Download the .zip archive with +
  • Download the .zip archive with the latest version of the demo notes
  • -
  • Right click on any note in your tree under which you would like the demo +
  • Right click on any note in your tree under which you would like the demo notes to be imported
  • -
  • Click "Import into note"
  • -
  • Select the .zip archive to import it
  • +
  • Click "Import into note"
  • +
  • Select the .zip archive to import it
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html index f0f720b87..832156f7a 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Render Note.html @@ -24,7 +24,7 @@ The current date & time is <span class="date"></span>Now we need to add the script. Create another Code, but this time of JavaScript (frontend) language. Make sure the newly created note is a direct child of the HTML - note created previously; with the following content:

const $dateEl = api.$container.find(".date");
+  note created previously; with the following content:

const $dateEl = api.$container.find(".date");
 $dateEl.text(new Date());

Now create a render note at any place and set its ~renderNote relation to point to the HTML note. When the render note is accessed it will display:

diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.html index c6983e5d7..7617ab9a4 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.html @@ -32,41 +32,30 @@

Let's start by creating a widget that shows a message near the content area. Follow the previous section to create a code note, and use the following content.

- - - - - - - - - - - - - -
LegacyPreact (v0.101.0+)
class HelloNoteDetail extends api.BasicWidget {
+

Legacy version (jQuery)

class HelloCenterPane extends api.BasicWidget {
 
-
constructor() {
-    super();
-    this.contentSized();
+    constructor() {
+        super();
+        this.contentSized();
+    }
+
+    get parentWidget() { return "center-pane" }
+
+    doRender() {
+        this.$widget = $("<span>Center pane</span>");
+    }
+    
 }
 
-get parentWidget() { return "center-pane" }
+module.exports = new HelloCenterPane();
+

Refresh the application and the widget + should appear underneath the content area.

+

Preact version

import { defineWidget } from "trilium:preact";
 
-doRender() {
-    this.<!--FORMULA_INLINE_1766526977514_0-->("&lt;span&gt;Center pane&lt;/span&gt;");
-}
-

}

-

module.exports = new HelloNoteDetail();

-
-
-
import { defineWidget } from "trilium:preact";

export default defineWidget({ +export default defineWidget({ parent: "center-pane", render: () => <span>Center pane from Preact.</span> -});

-
-

+});

Refresh the application and the widget should appear underneath the content area.

Widget location (parent widget)

diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.html index 70ab3f1ac..5386d0052 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.html @@ -30,7 +30,7 @@ class NoteTitleWidget extends api.NoteContextAwareWidget { } module.exports = new NoteTitleWidget(); -

Preact widget (v0.101.0+)

import { defineLauncherWidget, useActiveNoteContext } from "trilium:preact";
+

Preact widget (v0.101.0+)

import { defineLauncherWidget, useActiveNoteContext } from "trilium:preact";
 
 export default defineLauncherWidget({
     render: () => {
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact.html
index 82c823548..1a2ce8779 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact.html	
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact.html	
@@ -2,9 +2,9 @@
   support for JSX.

Preact can be used for:

To get started, the first step is to enable JSX in the list of Code languages. @@ -20,22 +20,23 @@

Import/exports

When using Preact with JSX, there is a special syntax which provides ES-like - imports. This import syntax makes way for a more intuitive that - doesn't make use of global objects and paves the way for better auto-completion - support that might be introduced in the future. 

+ imports. This import syntax makes way for + a more intuitive that doesn't make use of global objects and paves the + way for better auto-completion support that might be introduced in the + future. 

API imports

-

Instead of:

api.showMessage("Hello");
-

the JSX version looks like this:

import { showMessage } from "trilium:api";
+

Instead of:

api.showMessage("Hello");
+

the JSX version looks like this:

import { showMessage } from "trilium:api";
 showMessage("hello");

Preact API imports (hooks, components)

There's a new Script API dedicated to Preact, which provides shared components that are also used by Trilium - internally as well as hooks, for example.

import { useState } from "trilium:preact";
+  internally as well as hooks, for example.

import { useState } from "trilium:preact";
 const [ myState, setMyState ] = useState("Hi");

Exporting

JSX notes can export a component for use in Render Note or for Component libraries: 

export default function() {
+  href="#root/_help_Bqde6BvPo05g">Component libraries: 

export default function() {
     return (
         <>
             <p>Hello world.</p>
@@ -43,13 +44,15 @@ const [ myState, setMyState ] = useState("Hi");
); }

Import/export are not required

-

These imports are syntactic sugar meant to replace the usage for the api global - object (see Script API). 

+

These imports are syntactic sugar meant to replace the usage for the + apiglobal object (see Script API). 

Under the hood

Unlike JavaScript, JSX requires pre-processing to turn it into JavaScript diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.html index 27aa57582..209519800 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.html @@ -7,8 +7,8 @@ also available to Custom Widgets and  Render Note.

-

To use these components, simply import them from trilium:preact:

import { ActionButton, Button, LinkButton } from "trilium:preact";
-

and then use them:

export default function MyRenderNote() {
+

To use these components, simply import them from trilium:preact:

import { ActionButton, Button, LinkButton } from "trilium:preact";
+

and then use them:

export default function MyRenderNote() {
     const onClick = () => showMessage("A button was pressed");
     
     return (
@@ -33,12 +33,12 @@
   to custom widgets and JSX render notes.

To use it, simply:

    -
  1. Create a render note.
  2. -
  3. Create a child code note of JSX type with the content of this file:  +
  4. Create a render note.
  5. +
  6. Create a child code note of JSX type with the content of this file:  Widget showcase
  7. -
  8. Set the ~renderNote relation of the parent note to the child - note.
  9. -
  10. Refresh the render note to see the results.
  11. +
  12. Set the ~renderNote relation of the parent + note to the child note.
  13. +
  14. Refresh the render note to see the results.
\ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.html index 2cdec3295..a148c29f2 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.html @@ -1,4 +1,4 @@ -

Inline styles

<div style={{
+

Inline styles

<div style={{
     display: "flex",
     height: "53px",
     width: "fit-content",
@@ -9,4 +9,4 @@
 

Custom CSS file

Simply create a Custom app-wide CSS. Make sure the class names are unique enough to not intersect with other - UI elements, consider adding a prefix (e.g. x-mywidget-).

\ No newline at end of file + UI elements, consider adding a prefix (e.g. x-mywidget-).

\ No newline at end of file diff --git a/docs/Developer Guide/Developer Guide/Documentation.md b/docs/Developer Guide/Developer Guide/Documentation.md index c4c3cfb06..a7dbaff23 100644 --- a/docs/Developer Guide/Developer Guide/Documentation.md +++ b/docs/Developer Guide/Developer Guide/Documentation.md @@ -1,5 +1,5 @@ # Documentation -There are multiple types of documentation for Trilium: +There are multiple types of documentation for Trilium: * The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing F1. * The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers. diff --git a/docs/User Guide/User Guide/Note Types/Render Note.md b/docs/User Guide/User Guide/Note Types/Render Note.md index 828d7dfef..01e2e1c24 100644 --- a/docs/User Guide/User Guide/Note Types/Render Note.md +++ b/docs/User Guide/User Guide/Note Types/Render Note.md @@ -24,7 +24,7 @@ The current date & time is Now we need to add the script. Create another Code, but this time of JavaScript (frontend) language. Make sure the newly created note is a direct child of the HTML note created previously; with the following content: -``` +```javascript const $dateEl = api.$container.find(".date"); $dateEl.text(new Date()); ``` diff --git a/docs/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.md b/docs/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.md index cc011c514..dec4cfd3f 100644 --- a/docs/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.md +++ b/docs/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets.md @@ -23,21 +23,39 @@ Wherever possible, widget examples will be both in the legacy and Preact format. Let's start by creating a widget that shows a message near the content area. Follow the previous section to create a code note, and use the following content. -
LegacyPreact (v0.101.0+)
class HelloNoteDetail extends api.BasicWidget {
+### Legacy version (jQuery)
 
-
constructor() {
-    super();
-    this.contentSized();
+```
+class HelloCenterPane extends api.BasicWidget {
+
+    constructor() {
+        super();
+        this.contentSized();
+    }
+
+    get parentWidget() { return "center-pane" }
+
+    doRender() {
+        this.$widget = $("Center pane");
+    }
+    
 }
 
-get parentWidget() { return "center-pane" }
+module.exports = new HelloCenterPane();
+```
 
-doRender() {
-    this.<!--FORMULA_INLINE_1766526977514_0-->("&lt;span&gt;Center pane&lt;/span&gt;");
-}

}

module.exports = new HelloNoteDetail();

import { defineWidget } from "trilium:preact";

export default defineWidget({ +[Refresh the application](../../Troubleshooting/Refreshing%20the%20application.md) and the widget should appear underneath the content area. + +### Preact version + +``` +import { defineWidget } from "trilium:preact"; + +export default defineWidget({ parent: "center-pane", - render: () => <span>Center pane from Preact.</span> -});

+ render: () => Center pane from Preact. +}); +``` [Refresh the application](../../Troubleshooting/Refreshing%20the%20application.md) and the widget should appear underneath the content area. diff --git a/docs/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.md b/docs/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.md index 78a69eb8a..c892e3365 100644 --- a/docs/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.md +++ b/docs/User Guide/User Guide/Scripting/Frontend Basics/Launch Bar Widgets/Note Title Widget.md @@ -35,7 +35,7 @@ module.exports = new NoteTitleWidget(); ## Preact widget (v0.101.0+) -``` +```jsx import { defineLauncherWidget, useActiveNoteContext } from "trilium:preact"; export default defineLauncherWidget({ diff --git a/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact.md b/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact.md index db3811479..6f05765fc 100644 --- a/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact.md +++ b/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact.md @@ -19,13 +19,13 @@ When using Preact with JSX, there is a special syntax which provides ES-like imp Instead of: -``` +```jsx api.showMessage("Hello"); ``` the JSX version looks like this: -``` +```jsx import { showMessage } from "trilium:api"; showMessage("hello"); ``` @@ -34,7 +34,7 @@ showMessage("hello"); There's a new Script API dedicated to Preact, which provides shared components that are also used by Trilium internally as well as hooks, for example. -``` +```jsx import { useState } from "trilium:preact"; const [ myState, setMyState ] = useState("Hi"); ``` @@ -43,7 +43,7 @@ const [ myState, setMyState ] = useState("Hi"); JSX notes can export a component for use in Render Note or for Component libraries:  -``` +```jsx export default function() { return ( <> diff --git a/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.md b/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.md index 8d3c86e4f..db05c9255 100644 --- a/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.md +++ b/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/Built-in components.md @@ -5,13 +5,13 @@ Trilium comes with its own set of Preact components, some of which are also avai To use these components, simply import them from `trilium:preact`: -``` +```jsx import { ActionButton, Button, LinkButton } from "trilium:preact"; ``` and then use them: -``` +```jsx export default function MyRenderNote() { const onClick = () => showMessage("A button was pressed"); diff --git a/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.md b/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.md index bda9f195a..3b959261f 100644 --- a/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.md +++ b/docs/User Guide/User Guide/Scripting/Frontend Basics/Preact/CSS.md @@ -1,7 +1,7 @@ # CSS ## Inline styles -``` +```jsx