diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index c6de2f3bc..f4ac06e87 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -79,19 +79,6 @@ export default class DesktopLayout { const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); - const titleRow = new FlexContainer("row") - .class("title-row") - .cssBlock(".title-row > * { margin: 5px; }") - .child() - .child() - .optChild(isNewLayout, ) - .optChild(!isNewLayout, ) - .child() - .child() - .child() - .child() - .optChild(isNewLayout, ); - const rootContainer = new RootContainer(true) .setParent(appContext) .class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`) @@ -144,9 +131,19 @@ export default class DesktopLayout { .child( new SplitNoteContainer(() => new NoteWrapperWidget() - .child(titleRow) - .optChild(!isNewLayout, ) - .optChild(isNewLayout, ) + .child(new FlexContainer("row") + .class("title-row") + .cssBlock(".title-row > * { margin: 5px; }") + .child() + .child() + .optChild(isNewLayout, ) + .child() + .optChild(!isNewLayout, ) + .optChild(!isNewLayout, ) + .optChild(!isNewLayout, ) + .optChild(!isNewLayout, ) + .optChild(isNewLayout, )) + .optChild(!isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) .child() .child( diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 0a1971f11..8ecd85b69 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -1315,13 +1315,16 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu { top: 0; inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */ margin-top: -10px; - min-width: max-content; - max-width: 300px; /* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */ max-height: 600px; overflow: auto; } +body.desktop .dropdown-submenu > .dropdown-menu { + min-width: max-content; + max-width: 300px; +} + .dropdown-submenu.dropstart > .dropdown-menu { inset-inline-start: auto; inset-inline-end: calc(100% - 2px); diff --git a/apps/client/src/stylesheets/theme-next/base.css b/apps/client/src/stylesheets/theme-next/base.css index 78d432f19..e210b8d12 100644 --- a/apps/client/src/stylesheets/theme-next/base.css +++ b/apps/client/src/stylesheets/theme-next/base.css @@ -176,7 +176,7 @@ body.desktop .dropdown-submenu .dropdown-menu { cursor: default !important; } -.dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item { +body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item { padding-inline-end: var(--menu-item-start-padding) !important; padding-inline-start: var(--menu-item-end-padding) !important; } @@ -254,7 +254,8 @@ html body .dropdown-item[disabled] { } /* Menu item arrow */ -.dropdown-submenu:not(.dropstart) .dropdown-toggle::after { +body.mobile .dropdown-submenu .dropdown-toggle::after, +body.desktop .dropdown-submenu:not(.dropstart) .dropdown-toggle::after { content: "\ed3b" !important; position: absolute; display: flex !important; @@ -270,7 +271,11 @@ html body .dropdown-item[disabled] { color: var(--menu-item-arrow-color) !important; } -.dropdown-submenu.dropstart .dropdown-toggle::before { +body.mobile .dropdown-submenu.dropstart .dropdown-toggle::before { + content: unset; +} + +body.desktop .dropdown-submenu.dropstart .dropdown-toggle::before { content: "\ea4d" !important; position: absolute; display: flex !important; diff --git a/apps/client/src/stylesheets/theme-next/ribbon.css b/apps/client/src/stylesheets/theme-next/ribbon.css index 556b5cd4e..ffba0fcac 100644 --- a/apps/client/src/stylesheets/theme-next/ribbon.css +++ b/apps/client/src/stylesheets/theme-next/ribbon.css @@ -168,12 +168,6 @@ ul.editability-dropdown li.dropdown-item > div { * Note info */ -:root .note-info-widget-table button.calculate-button { - min-width: 0; - padding: 4px 10px !important; - font-size: 0.8em; -} - /* Narrow width layout */ .note-info-widget { container: info-section / inline-size; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 760804874..9ecdcd640 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -795,7 +795,7 @@ "file_type": "File type", "file_size": "File size", "download": "Download", - "open": "Open", + "open": "Open externally", "upload_new_revision": "Upload new revision", "upload_success": "New file revision has been uploaded.", "upload_failed": "Upload of a new file revision failed.", @@ -826,7 +826,8 @@ "note_size_info": "Note size provides rough estimate of storage requirements for this note. It takes into account note's content and content of its note revisions.", "calculate": "calculate", "subtree_size": "(subtree size: {{size}} in {{count}} notes)", - "title": "Note Info" + "title": "Note Info", + "show_similar_notes": "Show similar notes" }, "note_map": { "open_full": "Expand to full", @@ -889,7 +890,8 @@ "search_parameters": "Search Parameters", "unknown_search_option": "Unknown search option {{searchOptionName}}", "search_note_saved": "Search note has been saved into {{- notePathTitle}}", - "actions_executed": "Actions have been executed." + "actions_executed": "Actions have been executed.", + "view_options": "View options:" }, "similar_notes": { "title": "Similar Notes", @@ -1758,7 +1760,8 @@ "note_type_switcher_label": "Switch from {{type}} to:", "note_type_switcher_others": "More note types", "note_type_switcher_templates": "Templates", - "note_type_switcher_collection": "Collections" + "note_type_switcher_collection": "Collections", + "edited_notes": "Edited notes" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", @@ -2157,16 +2160,22 @@ "execute_sql_description": "This note is a SQL note. Click to execute the SQL query." }, "status_bar": { - "language_title": "Change the language of the entire content", - "note_info_title": "View information about this note such as the creation/modification date or the note size.", - "backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", - "backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.", - "attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.", - "attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab.", + "language_title": "Change content language", + "note_info_title": "View note info (e.g., dates, note size)", + "backlinks_one": "{{count}} backlink", + "backlinks_other": "{{count}} backlinks", + "backlinks_title_one": "View backlink", + "backlinks_title_other": "View backlinks", + "attachments_one": "{{count}} attachment", + "attachments_other": "{{count}} attachments", + "attachments_title_one": "View attachment in a new tab", + "attachments_title_other": "View attachments in a new tab", "attributes_one": "{{count}} attribute", "attributes_other": "{{count}} attributes", - "attributes_title": "Click to open a dedicated pane to edit this note's owned attributes, as well as to see the list of inherited attributes.", - "note_paths_title": "Click to see the paths where this note is placed into the tree.", + "attributes_title": "Owned attributes and inherited attributes", + "note_paths_one": "{{count}} path", + "note_paths_other": "{{count}} paths", + "note_paths_title": "Note paths", "code_note_switcher": "Change language mode" } } diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index 171ea2fa8..5cbb6f10f 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -1,26 +1,27 @@ +import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; import { VNode } from "preact"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; + import appContext, { EventData, EventNames } from "../components/app_context"; import Component from "../components/component"; import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; -import ActionButton, { ActionButtonProps } from "./react/ActionButton"; -import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; -import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; -import server from "../services/server"; -import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; -import toast from "../services/toast"; +import attributes from "../services/attributes"; +import { isExperimentalFeatureEnabled } from "../services/experimental_features"; +import froca from "../services/froca"; import { t } from "../services/i18n"; import { copyImageReferenceToClipboard } from "../services/image"; -import tree from "../services/tree"; import { getHelpUrlForNote } from "../services/in_app_help"; -import froca from "../services/froca"; +import LoadResults from "../services/load_results"; +import server from "../services/server"; +import toast from "../services/toast"; +import tree from "../services/tree"; +import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; +import { ViewTypeOptions } from "./collections/interface"; +import ActionButton, { ActionButtonProps } from "./react/ActionButton"; +import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; import NoteLink from "./react/NoteLink"; import RawHtml from "./react/RawHtml"; -import { ViewTypeOptions } from "./collections/interface"; -import attributes from "../services/attributes"; -import LoadResults from "../services/load_results"; -import { isExperimentalFeatureEnabled } from "../services/experimental_features"; export interface FloatingButtonContext { parentComponent: Component; @@ -38,7 +39,7 @@ function FloatingButton({ className, ...props }: ActionButtonProps) { className={`floating-button ${className ?? ""}`} noIconActionClass {...props} - /> + />; } export type FloatingButtonsList = ((context: FloatingButtonContext) => false | VNode)[]; @@ -85,7 +86,7 @@ function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefault text={t("backend_log.refresh")} icon="bx bx-refresh" onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} - /> + />; } function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) { @@ -97,7 +98,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")} icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"} onClick={() => setSplitEditorOrientation(upcomingOrientation)} - /> + />; } function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) { @@ -109,7 +110,7 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingBut text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")} icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"} onClick={() => setReadOnly(!isReadOnly)} - /> + />; } function EditButton({ note, noteContext }: FloatingButtonContext) { @@ -132,7 +133,7 @@ function EditButton({ note, noteContext }: FloatingButtonContext) { icon="bx bx-pencil" className={animationClass} onClick={() => enableEditing()} - /> + />; } function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { @@ -150,7 +151,7 @@ function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingB appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId }); } }} - /> + />; } function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { @@ -168,7 +169,7 @@ function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode } appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId }); } }} - /> + />; } function RunActiveNoteButton({ note }: FloatingButtonContext) { @@ -177,7 +178,7 @@ function RunActiveNoteButton({ note }: FloatingButtonContext) { icon="bx bx-play" text={t("code_buttons.execute_button_title")} triggerCommand="runActiveNote" - /> + />; } function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { @@ -186,7 +187,7 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { icon="bx bx-help-circle" text={t("code_buttons.trilium_api_docs_button_title")} onClick={() => openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} - /> + />; } function SaveToNoteButton({ note }: FloatingButtonContext) { @@ -204,7 +205,7 @@ function SaveToNoteButton({ note }: FloatingButtonContext) { await appContext.tabManager.getActiveContext()?.setNote(notePath); } }} - /> + />; } function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) { @@ -237,7 +238,7 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB /> - ) + ); } function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) { @@ -253,8 +254,11 @@ function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonCon function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) { const hiddenImageCopyRef = useRef(null); - const isEnabled = ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "") - && note?.isContentAvailable() && isDefaultViewMode; + const isEnabled = ( + !isNewLayout + && ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "") + && note?.isContentAvailable() && isDefaultViewMode + ); return isEnabled && ( <> @@ -275,7 +279,7 @@ function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonCon position: "absolute" // Take out of the the hidden image from flexbox to prevent the layout being affected }} /> - ) + ); } function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) { @@ -295,7 +299,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB onClick={() => triggerEvent("exportPng")} /> - ) + ); } function InAppHelpButton({ note }: FloatingButtonContext) { @@ -308,7 +312,7 @@ function InAppHelpButton({ note }: FloatingButtonContext) { text={t("help-button.title")} onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)} /> - ) + ); } function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) { diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index dd704a0c9..5f0397c7e 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -139,6 +139,8 @@ function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNoteP return (
    {childNotes.map((note) => { + if (note.noteId === "_hidden") return; + const childNotePath = `${notePath}/${note.noteId}`; return
  • + ); @@ -301,3 +305,41 @@ function useBuiltinTemplates() { 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?.map(editedNote => ( + + )} + /> + ))} + + ); +} +//#endregion diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index 52aea7a90..80a7a98d1 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -4,25 +4,8 @@ body.experimental-feature-new-layout { } .title-actions { - padding: 0; - display: flex; - gap: 0.25em; - align-items: center; - width: 100%; - max-width: unset; - padding-inline-start: 15px; - padding-bottom: 0.2em; - font-size: 0.8em; - - .dropdown-menu { - input.form-control { - padding: 2px 8px; - margin-left: 1em; - } - } - - .spacer { - flex-grow: 1; + &.visible { + padding: 0.75em 15px; } } } diff --git a/apps/client/src/widgets/layout/NoteTitleActions.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx index 765eefca1..31323ccbf 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -1,15 +1,38 @@ -import CollectionProperties from "../note_bars/CollectionProperties"; -import { useNoteContext, useNoteProperty } from "../react/hooks"; import "./NoteTitleActions.css"; +import clsx from "clsx"; + +import FNote from "../../entities/fnote"; +import { t } from "../../services/i18n"; +import CollectionProperties from "../note_bars/CollectionProperties"; +import Collapsible from "../react/Collapsible"; +import { useNoteContext, useNoteProperty } from "../react/hooks"; +import SearchDefinitionTab from "../ribbon/SearchDefinitionTab"; + export default function NoteTitleActions() { - const { note } = useNoteContext(); + const { note, ntxId } = useNoteContext(); const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_"); const noteType = useNoteProperty(note, "type"); + const items = [ + note && noteType === "search" && , + note && !isHiddenNote && noteType === "book" && + ].filter(Boolean); + return ( -
    - {note && !isHiddenNote && noteType === "book" && } +
    0 && "visible")}> + {items}
    ); } + +function SearchProperties({ note, ntxId }: { note: FNote, ntxId: string | null | undefined }) { + return (note && + + + ); +} diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 650b4bdd3..a30b4b89e 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -57,10 +57,16 @@ } .dropdown-note-info { + padding: 1em !important; + ul { + --row-block-margin: .2em; + list-style-type: none; - padding: 0.5em; + padding: 0; margin: 0; + margin-top: calc(0px - var(--row-block-margin)); + margin-bottom: 12px; display: table; li { @@ -68,7 +74,8 @@ > strong { display: table-cell; - padding: 0.2em 0; + padding: var(--row-block-margin) 0; + opacity: .5; } > span { @@ -85,9 +92,62 @@ padding: 0.5em; } + .note-path-intro { + color: var(--muted-text-color); + } + .note-path-list { - margin: 1em; + margin: 12px 0; padding: 0; + list-style: none; + + li { + --border-radius: 6px; + + position: relative; + background: var(--card-background-color); + padding: 8px 20px 8px 25px; + + &:first-child { + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + + &:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); + } + + & + li { + margin-top: 2px; + } + + &.path-current::before { + position: absolute; + display: flex; + justify-content: flex-end; + align-items: center; + content: "\ee8f"; + top: 0; + left: 0; + width: 20px; + bottom: 0; + font-family: "boxicons"; + font-size: .85em; + color: var(--menu-item-icon-color); + } + } + + a { + margin-inline: 2px; + padding-inline: 2px; + color: currentColor; + font-weight: normal; + text-decoration: none; + + &.basename { + color: var(--muted-text-color); + } + } + } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 26154e98f..e4724b31c 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -1,6 +1,7 @@ import "./StatusBar.css"; import { Locale } from "@triliumnext/commons"; +import { Dropdown as BootstrapDropdown } from "bootstrap"; import clsx from "clsx"; import { type ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; @@ -18,14 +19,16 @@ import { formatDateTime } from "../../utils/formatters"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks"; +import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteLabel, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks"; import Icon from "../react/Icon"; +import LinkButton from "../react/LinkButton"; import { ParentComponent } from "../react/react_utils"; import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab"; import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor"; import InheritedAttributesTab from "../ribbon/InheritedAttributesTab"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; +import SimilarNotesTab from "../ribbon/SimilarNotesTab"; import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; @@ -40,17 +43,27 @@ interface StatusBarContext { export default function StatusBar() { const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext(); - const [ attributesShown, setAttributesShown ] = useState(false); + const [ activePane, setActivePane ] = useState<"attributes" | "similar-notes" | null>(null); const context: StatusBarContext | undefined | null = note && noteContext && { note, notePath, noteContext, viewScope, hoistedNoteId }; - const attributesContext: AttributesProps | undefined | null = context && { ...context, attributesShown, setAttributesShown }; + const attributesContext: AttributesProps | undefined | null = context && { + ...context, + attributesShown: activePane === "attributes", + setAttributesShown: () => setActivePane("attributes") + }; + const noteInfoContext: NoteInfoContext | undefined | null = context && { + ...context, + similarNotesShown: activePane === "similar-notes", + setSimilarNotesShown: () => setActivePane("similar-notes") + }; const isHiddenNote = note?.isInHiddenSubtree(); return (
    {attributesContext && } + {noteInfoContext && }
    - {context && attributesContext && <> + {context && attributesContext && noteInfoContext && <>
    @@ -60,7 +73,7 @@ export default function StatusBar() { - +
    }
    @@ -81,6 +94,7 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions ...titleOptions?.popperConfig, strategy: "fixed" }, + animation: false, ...titleOptions }} dropdownOptions={{ @@ -92,7 +106,7 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions }} text={<> {icon && (<> )} - {text} + {text} } {...dropdownProps} > @@ -105,7 +119,7 @@ interface StatusBarButtonBaseProps { className?: string; icon: string; title: string; - text?: string | number; + text: string | number; disabled?: boolean; active?: boolean; } @@ -120,6 +134,7 @@ function StatusBarButton({ className, icon, text, title, active, ...restProps }: placement: "top", fallbackPlacements: [ "top" ], popperConfig: { strategy: "fixed" }, + animation: false, title }); @@ -136,7 +151,7 @@ function StatusBarButton({ className, icon, text, title, active, ...restProps }: } }} > -  {text} +  {text} ); } @@ -194,24 +209,41 @@ export function getLocaleName(locale: Locale | null | undefined) { } //#endregion -//#region Note info -export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) { +//#region Note info & Similar +interface NoteInfoContext extends StatusBarContext { + similarNotesShown: boolean; + setSimilarNotesShown: (value: boolean) => void; +} + +export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) { + const dropdownRef = useRef(null); const { metadata, ...sizeProps } = useNoteMetadata(note); + const [ originalFileName ] = useNoteLabel(note, "originalFileName"); return (note &&
      + {originalFileName && } {note.type} {note.mime && ({note.mime})}} /> {note.noteId}} /> } />
    + + { + dropdownRef.current?.hide(); + setSimilarNotesShown(true); + }} + />
    ); } @@ -224,6 +256,14 @@ function NoteInfoValue({ text, title, value }: { text: string; title?: string, v
  • ); } + +function SimilarNotesPane({ note, similarNotesShown }: NoteInfoContext) { + return (similarNotesShown && +
    + +
    + ); +} //#endregion //#region Backlinks @@ -233,7 +273,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) { @@ -252,7 +292,7 @@ function AttachmentCount({ note }: StatusBarContext) { @@ -330,13 +370,14 @@ function AttributesPane({ note, noteContext, attributesShown, setAttributesShown //#region Note paths function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) { const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); + const count = sortedNotePaths?.length ?? 0; - return ( + return (count > 1 && = { grid: "bx bxs-grid", @@ -30,12 +32,12 @@ export default function CollectionProperties({ note }: { note: FNote }) { const [ viewType, setViewType ] = useViewType(note); return ( - <> +
    - +
    ); } @@ -187,9 +189,9 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo {index < property.options.length - 1 && } ); - } else { - return renderItem(option); } + return renderItem(option); + })} ); diff --git a/apps/client/src/widgets/react/Badge.tsx b/apps/client/src/widgets/react/Badge.tsx index e7844368c..a03e6b5fb 100644 --- a/apps/client/src/widgets/react/Badge.tsx +++ b/apps/client/src/widgets/react/Badge.tsx @@ -10,7 +10,7 @@ import Icon from "./Icon"; interface SimpleBadgeProps { className?: string; - title: string; + title: ComponentChildren; } interface BadgeProps { diff --git a/apps/client/src/widgets/react/Collapsible.css b/apps/client/src/widgets/react/Collapsible.css new file mode 100644 index 000000000..aaa61b6e6 --- /dev/null +++ b/apps/client/src/widgets/react/Collapsible.css @@ -0,0 +1,32 @@ +.collapsible { + .collapsible-title { + line-height: 1em; + display: flex; + align-items: center; + appearance: none; + background: transparent; + border: 0; + color: inherit; + + .arrow { + font-size: 1.3em; + transition: transform 250ms ease-in; + } + } + + .collapsible-body { + height: 0; + overflow: hidden; + transition: height 250ms ease-in; + } + + .collapsible-inner-body { + padding-top: 0.5em; + } + + &.expanded { + .collapsible-title .arrow { + transform: rotate(90deg); + } + } +} diff --git a/apps/client/src/widgets/react/Collapsible.tsx b/apps/client/src/widgets/react/Collapsible.tsx new file mode 100644 index 000000000..2bf5be43d --- /dev/null +++ b/apps/client/src/widgets/react/Collapsible.tsx @@ -0,0 +1,52 @@ +import "./Collapsible.css"; + +import clsx from "clsx"; +import { ComponentChildren, HTMLAttributes } from "preact"; +import { useRef, useState } from "preact/hooks"; + +import { useElementSize, useUniqueName } from "./hooks"; +import Icon from "./Icon"; + +interface CollapsibleProps extends Pick, "className"> { + title: string; + children: ComponentChildren; + initiallyExpanded?: boolean; +} + +export default function Collapsible({ title, children, className, initiallyExpanded }: CollapsibleProps) { + const bodyRef = useRef(null); + const innerRef = useRef(null); + const [ expanded, setExpanded ] = useState(initiallyExpanded); + const { height } = useElementSize(innerRef) ?? {}; + const contentId = useUniqueName(); + + return ( +
    + + +
    +
    + {children} +
    +
    +
    + ); + +} diff --git a/apps/client/src/widgets/react/FormFileUpload.tsx b/apps/client/src/widgets/react/FormFileUpload.tsx index 83db15ebb..e97e73184 100644 --- a/apps/client/src/widgets/react/FormFileUpload.tsx +++ b/apps/client/src/widgets/react/FormFileUpload.tsx @@ -1,7 +1,9 @@ import { Ref } from "preact"; -import Button, { ButtonProps } from "./Button"; import { useEffect, useRef } from "preact/hooks"; +import ActionButton, { ActionButtonProps } from "./ActionButton"; +import Button, { ButtonProps } from "./Button"; + interface FormFileUploadProps { name?: string; onChange: (files: FileList | null) => void; @@ -26,7 +28,7 @@ export default function FormFileUpload({ inputRef, name, onChange, multiple, hid multiple={multiple} onChange={e => onChange((e.target as HTMLInputElement).files)} /> - ) + ); } /** @@ -49,5 +51,27 @@ export function FormFileUploadButton({ onChange, ...buttonProps }: Omit - ) + ); +} + +/** + * Similar to {@link FormFileUploadButton}, but uses an {@link ActionButton} instead of a normal {@link Button}. + * @param param the change listener for the file upload and the properties for the button. + */ +export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit & Pick) { + const inputRef = useRef(null); + + return ( + <> + inputRef.current?.click()} + /> +
    @@ -77,3 +65,19 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) { ); } + +export function buildUploadNewFileRevisionListener(note: FNote) { + return (fileToUpload: FileList | null) => { + if (!fileToUpload) { + return; + } + + server.upload(`notes/${note.noteId}/file`, fileToUpload[0]).then((result) => { + if (result.uploaded) { + toast.showMessage(t("file_properties.upload_success")); + } else { + toast.showError(t("file_properties.upload_failed")); + } + }); + }; +} diff --git a/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx b/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx index 9f15c4821..c29a7235c 100644 --- a/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx @@ -1,14 +1,16 @@ -import { t } from "../../services/i18n"; -import { useNoteBlob, useNoteLabel } from "../react/hooks"; -import { TabContext } from "./ribbon-interface"; -import { clearBrowserCache, formatSize } from "../../services/utils"; -import Button from "../react/Button"; -import { downloadFileNote, openNoteExternally } from "../../services/open"; -import { ParentComponent } from "../react/react_utils"; import { useContext } from "preact/hooks"; -import { FormFileUploadButton } from "../react/FormFileUpload"; + +import FNote from "../../entities/fnote"; +import { t } from "../../services/i18n"; +import { downloadFileNote, openNoteExternally } from "../../services/open"; import server from "../../services/server"; import toast from "../../services/toast"; +import { clearBrowserCache, formatSize } from "../../services/utils"; +import Button from "../react/Button"; +import { FormFileUploadButton } from "../react/FormFileUpload"; +import { useNoteBlob, useNoteLabel } from "../react/hooks"; +import { ParentComponent } from "../react/react_utils"; +import { TabContext } from "./ribbon-interface"; export default function ImagePropertiesTab({ note, ntxId }: TabContext) { const [ originalFileName ] = useNoteLabel(note, "originalFileName"); @@ -60,23 +62,27 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) { { - if (!files) return; - const fileToUpload = files[0]; // copy to allow reset below - - const result = await server.upload(`images/${note.noteId}`, fileToUpload); - - if (result.uploaded) { - toast.showMessage(t("image_properties.upload_success")); - await clearBrowserCache(); - } else { - toast.showError(t("image_properties.upload_failed", { message: result.message })); - } - }} + onChange={buildUploadNewImageRevisionListener(note)} /> )} - ) + ); +} + +export function buildUploadNewImageRevisionListener(note: FNote) { + return async (files: FileList | null) => { + if (!files) return; + const fileToUpload = files[0]; // copy to allow reset below + + const result = await server.upload(`images/${note.noteId}`, fileToUpload); + + if (result.uploaded) { + toast.showMessage(t("image_properties.upload_success")); + await clearBrowserCache(); + } else { + toast.showError(t("image_properties.upload_failed", { message: result.message })); + } + }; } diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index c2f388f73..698945d8d 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -1,31 +1,44 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons"; -import { useContext, useState } from "preact/hooks"; +import { useContext } from "preact/hooks"; import appContext, { CommandNames } from "../../components/app_context"; import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; import branches from "../../services/branches"; import dialog from "../../services/dialog"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { t } from "../../services/i18n"; +import protected_session from "../../services/protected_session"; import server from "../../services/server"; import toast from "../../services/toast"; import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils"; import ws from "../../services/ws"; +import ClosePaneButton from "../buttons/close_pane_button"; +import CreatePaneButton from "../buttons/create_pane_button"; +import MovePaneButton from "../buttons/move_pane_button"; import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks"; import { ParentComponent } from "../react/react_utils"; -import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab"; -import protected_session from "../../services/protected_session"; +import NoteActionsCustom from "./NoteActionsCustom"; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); export default function NoteActions() { - const { note, noteContext } = useNoteContext(); + const { note, ntxId, noteContext } = useNoteContext(); return (
    + {isNewLayout && ( + <> + {note && ntxId && } + + + + + + )} {note && !isNewLayout && } {note && note.type !== "launcher" && }
    @@ -79,14 +92,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not {isNewLayout && } - + {isNewLayout && isNormalViewMode && !isHelpPage && <> } - + parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> @@ -98,7 +111,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not })} /> {isElectron && } - + @@ -107,7 +120,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not {canBeConvertedToAttachment && } - {note.type === "render" && } @@ -122,7 +135,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not - + branches.deleteNotes([note.getParentBranches()[0].branchId])} @@ -168,7 +181,7 @@ function NoteBasicProperties({ note }: { note: FNote }) { currentValue={isTemplate} onChange={setIsTemplate} helpPage="KC1HB96bqqHX" disabled={note?.noteId.startsWith("_options")} - /> + /> ; } diff --git a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx new file mode 100644 index 000000000..113d2ed14 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx @@ -0,0 +1,122 @@ +import { NoteType } from "@triliumnext/commons"; +import { useContext } from "preact/hooks"; + +import FNote from "../../entities/fnote"; +import { t } from "../../services/i18n"; +import { downloadFileNote, openNoteExternally } from "../../services/open"; +import ActionButton from "../react/ActionButton"; +import { FormFileUploadActionButton } from "../react/FormFileUpload"; +import { useNoteProperty } from "../react/hooks"; +import { ParentComponent } from "../react/react_utils"; +import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab"; +import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab"; + +interface NoteActionsCustomProps { + note: FNote; + ntxId: string; +} + +interface NoteActionsCustomInnerProps extends NoteActionsCustomProps { + noteType: NoteType; +} + +/** + * Part of {@link NoteActions} on the new layout, but are rendered with a slight spacing + * from the rest of the note items and the buttons differ based on the note type. + */ +export default function NoteActionsCustom(props: NoteActionsCustomProps) { + const noteType = useNoteProperty(props.note, "type"); + const innerProps: NoteActionsCustomInnerProps | undefined = noteType && { + ...props, + noteType + }; + + return (innerProps && +
    + + +
    + ); +} + +//#region Note type mappings +function NoteActionsCustomInner(props: NoteActionsCustomInnerProps) { + switch (props.note.type) { + case "file": + return ; + case "image": + return ; + default: + return null; + } +} + +function FileActions(props: NoteActionsCustomInnerProps) { + return ( + <> + + + + + ); +} + +function ImageActions(props: NoteActionsCustomInnerProps) { + return ( + <> + + + + + ); +} +//#endregion + +//#region Shared buttons +function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps & { + onChange: (files: FileList | null) => void; +}) { + return ( + + ); +} + +function OpenExternallyButton({ note }: NoteActionsCustomInnerProps) { + return ( + openNoteExternally(note.noteId, note.mime)} + /> + ); +} + +function DownloadFileButton({ note }: NoteActionsCustomInnerProps) { + return ( + downloadFileNote(note.noteId)} + /> + ); +} + +function CopyReferenceToClipboardButton({ ntxId, noteType }: NoteActionsCustomInnerProps) { + const parentComponent = useContext(ParentComponent); + + return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) && + parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })} + /> + ); +} +//#endregion diff --git a/apps/client/src/widgets/ribbon/NoteInfoTab.tsx b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx index 8180f74a6..271779ffd 100644 --- a/apps/client/src/widgets/ribbon/NoteInfoTab.tsx +++ b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx @@ -9,6 +9,7 @@ import LoadingSpinner from "../react/LoadingSpinner"; import { useTriliumEvent } from "../react/hooks"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import FNote from "../../entities/fnote"; +import LinkButton from "../react/LinkButton"; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); @@ -53,9 +54,7 @@ export default function NoteInfoTab({ note }: { note: FNote | null | undefined } export function NoteSizeWidget({ isLoading, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }: Omit, "metadata">) { return <> {!isLoading && !noteSizeResponse && !subtreeSizeResponse && ( -
-