New layout refinement (#8053)

This commit is contained in:
Elian Doran
2025-12-15 07:36:58 +02:00
committed by GitHub
36 changed files with 1260 additions and 759 deletions

View File

@@ -79,19 +79,6 @@ export default class DesktopLayout {
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows; const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); const isNewLayout = isExperimentalFeatureEnabled("new-layout");
const titleRow = new FlexContainer("row")
.class("title-row")
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.optChild(isNewLayout, <NoteBadges />)
.optChild(!isNewLayout, <SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)
.child(<CreatePaneButton />)
.optChild(isNewLayout, <NoteActions />);
const rootContainer = new RootContainer(true) const rootContainer = new RootContainer(true)
.setParent(appContext) .setParent(appContext)
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`) .class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
@@ -144,9 +131,19 @@ export default class DesktopLayout {
.child( .child(
new SplitNoteContainer(() => new SplitNoteContainer(() =>
new NoteWrapperWidget() new NoteWrapperWidget()
.child(titleRow) .child(new FlexContainer("row")
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>) .class("title-row")
.optChild(isNewLayout, <Ribbon />) .cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.optChild(isNewLayout, <NoteBadges />)
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.optChild(!isNewLayout, <MovePaneButton direction="left" />)
.optChild(!isNewLayout, <MovePaneButton direction="right" />)
.optChild(!isNewLayout, <ClosePaneButton />)
.optChild(!isNewLayout, <CreatePaneButton />)
.optChild(isNewLayout, <NoteActions />))
.optChild(!isNewLayout, <Ribbon />)
.child(new WatchedFileUpdateStatusWidget()) .child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />) .child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child( .child(

View File

@@ -1315,13 +1315,16 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
top: 0; top: 0;
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */ 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; margin-top: -10px;
min-width: max-content;
max-width: 300px;
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */ /* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
max-height: 600px; max-height: 600px;
overflow: auto; overflow: auto;
} }
body.desktop .dropdown-submenu > .dropdown-menu {
min-width: max-content;
max-width: 300px;
}
.dropdown-submenu.dropstart > .dropdown-menu { .dropdown-submenu.dropstart > .dropdown-menu {
inset-inline-start: auto; inset-inline-start: auto;
inset-inline-end: calc(100% - 2px); inset-inline-end: calc(100% - 2px);

View File

@@ -176,7 +176,7 @@ body.desktop .dropdown-submenu .dropdown-menu {
cursor: default !important; 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-end: var(--menu-item-start-padding) !important;
padding-inline-start: var(--menu-item-end-padding) !important; padding-inline-start: var(--menu-item-end-padding) !important;
} }
@@ -254,7 +254,8 @@ html body .dropdown-item[disabled] {
} }
/* Menu item arrow */ /* 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; content: "\ed3b" !important;
position: absolute; position: absolute;
display: flex !important; display: flex !important;
@@ -270,7 +271,11 @@ html body .dropdown-item[disabled] {
color: var(--menu-item-arrow-color) !important; 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; content: "\ea4d" !important;
position: absolute; position: absolute;
display: flex !important; display: flex !important;

View File

@@ -168,12 +168,6 @@ ul.editability-dropdown li.dropdown-item > div {
* Note info * Note info
*/ */
:root .note-info-widget-table button.calculate-button {
min-width: 0;
padding: 4px 10px !important;
font-size: 0.8em;
}
/* Narrow width layout */ /* Narrow width layout */
.note-info-widget { .note-info-widget {
container: info-section / inline-size; container: info-section / inline-size;

View File

@@ -795,7 +795,7 @@
"file_type": "File type", "file_type": "File type",
"file_size": "File size", "file_size": "File size",
"download": "Download", "download": "Download",
"open": "Open", "open": "Open externally",
"upload_new_revision": "Upload new revision", "upload_new_revision": "Upload new revision",
"upload_success": "New file revision has been uploaded.", "upload_success": "New file revision has been uploaded.",
"upload_failed": "Upload of a new file revision failed.", "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.", "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", "calculate": "calculate",
"subtree_size": "(subtree size: {{size}} in {{count}} notes)", "subtree_size": "(subtree size: {{size}} in {{count}} notes)",
"title": "Note Info" "title": "Note Info",
"show_similar_notes": "Show similar notes"
}, },
"note_map": { "note_map": {
"open_full": "Expand to full", "open_full": "Expand to full",
@@ -889,7 +890,8 @@
"search_parameters": "Search Parameters", "search_parameters": "Search Parameters",
"unknown_search_option": "Unknown search option {{searchOptionName}}", "unknown_search_option": "Unknown search option {{searchOptionName}}",
"search_note_saved": "Search note has been saved into {{- notePathTitle}}", "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": { "similar_notes": {
"title": "Similar Notes", "title": "Similar Notes",
@@ -1758,7 +1760,8 @@
"note_type_switcher_label": "Switch from {{type}} to:", "note_type_switcher_label": "Switch from {{type}} to:",
"note_type_switcher_others": "More note types", "note_type_switcher_others": "More note types",
"note_type_switcher_templates": "Templates", "note_type_switcher_templates": "Templates",
"note_type_switcher_collection": "Collections" "note_type_switcher_collection": "Collections",
"edited_notes": "Edited notes"
}, },
"search_result": { "search_result": {
"no_notes_found": "No notes have been found for given search parameters.", "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." "execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
}, },
"status_bar": { "status_bar": {
"language_title": "Change the language of the entire content", "language_title": "Change content language",
"note_info_title": "View information about this note such as the creation/modification date or the note size.", "note_info_title": "View note info (e.g., dates, note size)",
"backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", "backlinks_one": "{{count}} backlink",
"backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.", "backlinks_other": "{{count}} backlinks",
"attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.", "backlinks_title_one": "View backlink",
"attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab.", "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_one": "{{count}} attribute",
"attributes_other": "{{count}} attributes", "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.", "attributes_title": "Owned attributes and inherited attributes",
"note_paths_title": "Click to see the paths where this note is placed into the tree.", "note_paths_one": "{{count}} path",
"note_paths_other": "{{count}} paths",
"note_paths_title": "Note paths",
"code_note_switcher": "Change language mode" "code_note_switcher": "Change language mode"
} }
} }

View File

@@ -1,26 +1,27 @@
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
import { VNode } from "preact"; import { VNode } from "preact";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import appContext, { EventData, EventNames } from "../components/app_context"; import appContext, { EventData, EventNames } from "../components/app_context";
import Component from "../components/component"; import Component from "../components/component";
import NoteContext from "../components/note_context"; import NoteContext from "../components/note_context";
import FNote from "../entities/fnote"; import FNote from "../entities/fnote";
import ActionButton, { ActionButtonProps } from "./react/ActionButton"; import attributes from "../services/attributes";
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; import { isExperimentalFeatureEnabled } from "../services/experimental_features";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import froca from "../services/froca";
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
import server from "../services/server";
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
import toast from "../services/toast";
import { t } from "../services/i18n"; import { t } from "../services/i18n";
import { copyImageReferenceToClipboard } from "../services/image"; import { copyImageReferenceToClipboard } from "../services/image";
import tree from "../services/tree";
import { getHelpUrlForNote } from "../services/in_app_help"; 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 NoteLink from "./react/NoteLink";
import RawHtml from "./react/RawHtml"; 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 { export interface FloatingButtonContext {
parentComponent: Component; parentComponent: Component;
@@ -38,7 +39,7 @@ function FloatingButton({ className, ...props }: ActionButtonProps) {
className={`floating-button ${className ?? ""}`} className={`floating-button ${className ?? ""}`}
noIconActionClass noIconActionClass
{...props} {...props}
/> />;
} }
export type FloatingButtonsList = ((context: FloatingButtonContext) => false | VNode)[]; export type FloatingButtonsList = ((context: FloatingButtonContext) => false | VNode)[];
@@ -85,7 +86,7 @@ function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefault
text={t("backend_log.refresh")} text={t("backend_log.refresh")}
icon="bx bx-refresh" icon="bx bx-refresh"
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
/> />;
} }
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) { 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")} 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"} icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
onClick={() => setSplitEditorOrientation(upcomingOrientation)} onClick={() => setSplitEditorOrientation(upcomingOrientation)}
/> />;
} }
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) { 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")} 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"} icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
onClick={() => setReadOnly(!isReadOnly)} onClick={() => setReadOnly(!isReadOnly)}
/> />;
} }
function EditButton({ note, noteContext }: FloatingButtonContext) { function EditButton({ note, noteContext }: FloatingButtonContext) {
@@ -132,7 +133,7 @@ function EditButton({ note, noteContext }: FloatingButtonContext) {
icon="bx bx-pencil" icon="bx bx-pencil"
className={animationClass} className={animationClass}
onClick={() => enableEditing()} onClick={() => enableEditing()}
/> />;
} }
function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
@@ -150,7 +151,7 @@ function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingB
appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId }); appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId });
} }
}} }}
/> />;
} }
function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
@@ -168,7 +169,7 @@ function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }
appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId }); appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId });
} }
}} }}
/> />;
} }
function RunActiveNoteButton({ note }: FloatingButtonContext) { function RunActiveNoteButton({ note }: FloatingButtonContext) {
@@ -177,7 +178,7 @@ function RunActiveNoteButton({ note }: FloatingButtonContext) {
icon="bx bx-play" icon="bx bx-play"
text={t("code_buttons.execute_button_title")} text={t("code_buttons.execute_button_title")}
triggerCommand="runActiveNote" triggerCommand="runActiveNote"
/> />;
} }
function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
@@ -186,7 +187,7 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
icon="bx bx-help-circle" icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")} text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} onClick={() => openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
/> />;
} }
function SaveToNoteButton({ note }: FloatingButtonContext) { function SaveToNoteButton({ note }: FloatingButtonContext) {
@@ -204,7 +205,7 @@ function SaveToNoteButton({ note }: FloatingButtonContext) {
await appContext.tabManager.getActiveContext()?.setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
} }
}} }}
/> />;
} }
function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) { function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) {
@@ -237,7 +238,7 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
/> />
</div> </div>
</> </>
) );
} }
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) { function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
@@ -253,8 +254,11 @@ function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonCon
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) { function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
const hiddenImageCopyRef = useRef<HTMLDivElement>(null); const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
const isEnabled = ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "") const isEnabled = (
&& note?.isContentAvailable() && isDefaultViewMode; !isNewLayout
&& ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode
);
return isEnabled && ( 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 position: "absolute" // Take out of the the hidden image from flexbox to prevent the layout being affected
}} /> }} />
</> </>
) );
} }
function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) { function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) {
@@ -295,7 +299,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
onClick={() => triggerEvent("exportPng")} onClick={() => triggerEvent("exportPng")}
/> />
</> </>
) );
} }
function InAppHelpButton({ note }: FloatingButtonContext) { function InAppHelpButton({ note }: FloatingButtonContext) {
@@ -308,7 +312,7 @@ function InAppHelpButton({ note }: FloatingButtonContext) {
text={t("help-button.title")} text={t("help-button.title")}
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)} onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
/> />
) );
} }
function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) { function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {

View File

@@ -139,6 +139,8 @@ function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNoteP
return ( return (
<ul className="breadcrumb-child-list"> <ul className="breadcrumb-child-list">
{childNotes.map((note) => { {childNotes.map((note) => {
if (note.noteId === "_hidden") return;
const childNotePath = `${notePath}/${note.noteId}`; const childNotePath = `${notePath}/${note.noteId}`;
return <li key={note.noteId}> return <li key={note.noteId}>
<FormListItem <FormListItem

View File

@@ -89,3 +89,24 @@ body.prefers-centered-content .inline-title {
flex-shrink: 0; 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;
}
}
}
}

View File

@@ -16,10 +16,13 @@ import server from "../../services/server";
import { formatDateTime } from "../../utils/formatters"; import { formatDateTime } from "../../utils/formatters";
import NoteIcon from "../note_icon"; import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title"; import NoteTitleWidget from "../note_title";
import { Badge, BadgeWithDropdown } from "../react/Badge"; import SimpleBadge, { Badge, BadgeWithDropdown } from "../react/Badge";
import Collapsible from "../react/Collapsible";
import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks"; import { useNoteBlob, useNoteContext, useNoteLabel, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils"; import { joinElements } from "../react/react_utils";
import { useEditedNotes } from "../ribbon/EditedNotesTab";
import { useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useNoteMetadata } from "../ribbon/NoteInfoTab";
import { onWheelHorizontalScroll } from "../widget_utils"; import { onWheelHorizontalScroll } from "../widget_utils";
@@ -72,6 +75,7 @@ export default function InlineTitle() {
</div> </div>
<NoteTitleDetails /> <NoteTitleDetails />
<EditedNotes />
<NoteTypeSwitcher /> <NoteTypeSwitcher />
</div> </div>
); );
@@ -301,3 +305,41 @@ function useBuiltinTemplates() {
return templates; return templates;
} }
//#endregion //#endregion
//#region Edited Notes
function EditedNotes() {
const { note } = useNoteContext();
const [ dateNote ] = useNoteLabel(note, "dateNote");
const [ editedNotesOpenInRibbon ] = useTriliumOptionBool("editedNotesOpenInRibbon");
return (note && dateNote &&
<Collapsible
className="edited-notes"
title={t("note_title.edited_notes")}
initiallyExpanded={editedNotesOpenInRibbon}
>
<EditedNotesContent note={note} />
</Collapsible>
);
}
function EditedNotesContent({ note }: { note: FNote }) {
const editedNotes = useEditedNotes(note);
return (
<>
{editedNotes?.map(editedNote => (
<SimpleBadge
key={editedNote.noteId}
title={(
<NoteLink
notePath={editedNote.noteId}
showNoteIcon
/>
)}
/>
))}
</>
);
}
//#endregion

View File

@@ -4,25 +4,8 @@ body.experimental-feature-new-layout {
} }
.title-actions { .title-actions {
padding: 0; &.visible {
display: flex; padding: 0.75em 15px;
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;
} }
} }
} }

View File

@@ -1,15 +1,38 @@
import CollectionProperties from "../note_bars/CollectionProperties";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import "./NoteTitleActions.css"; 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() { export default function NoteTitleActions() {
const { note } = useNoteContext(); const { note, ntxId } = useNoteContext();
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_"); const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
const noteType = useNoteProperty(note, "type"); const noteType = useNoteProperty(note, "type");
const items = [
note && noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />,
note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />
].filter(Boolean);
return ( return (
<div className="title-actions"> <div className={clsx("title-actions", items.length > 0 && "visible")}>
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />} {items}
</div> </div>
); );
} }
function SearchProperties({ note, ntxId }: { note: FNote, ntxId: string | null | undefined }) {
return (note &&
<Collapsible
title={t("search_definition.search_parameters")}
initiallyExpanded={note.isInHiddenSubtree()} // not saved searches
>
<SearchDefinitionTab note={note} ntxId={ntxId} hidden={false} />
</Collapsible>
);
}

View File

@@ -57,10 +57,16 @@
} }
.dropdown-note-info { .dropdown-note-info {
padding: 1em !important;
ul { ul {
--row-block-margin: .2em;
list-style-type: none; list-style-type: none;
padding: 0.5em; padding: 0;
margin: 0; margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table; display: table;
li { li {
@@ -68,7 +74,8 @@
> strong { > strong {
display: table-cell; display: table-cell;
padding: 0.2em 0; padding: var(--row-block-margin) 0;
opacity: .5;
} }
> span { > span {
@@ -85,9 +92,62 @@
padding: 0.5em; padding: 0.5em;
} }
.note-path-intro {
color: var(--muted-text-color);
}
.note-path-list { .note-path-list {
margin: 1em; margin: 12px 0;
padding: 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);
}
}
} }
} }

View File

@@ -1,6 +1,7 @@
import "./StatusBar.css"; import "./StatusBar.css";
import { Locale } from "@triliumnext/commons"; import { Locale } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx"; import clsx from "clsx";
import { type ComponentChildren } from "preact"; import { type ComponentChildren } from "preact";
import { createPortal } from "preact/compat"; import { createPortal } from "preact/compat";
@@ -18,14 +19,16 @@ import { formatDateTime } from "../../utils/formatters";
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
import Dropdown, { DropdownProps } from "../react/Dropdown"; import Dropdown, { DropdownProps } from "../react/Dropdown";
import { FormDropdownDivider, FormListItem } from "../react/FormList"; 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 Icon from "../react/Icon";
import LinkButton from "../react/LinkButton";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab"; import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor"; import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
import InheritedAttributesTab from "../ribbon/InheritedAttributesTab"; import InheritedAttributesTab from "../ribbon/InheritedAttributesTab";
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
import { useAttachments } from "../type_widgets/Attachment"; import { useAttachments } from "../type_widgets/Attachment";
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
import Breadcrumb from "./Breadcrumb"; import Breadcrumb from "./Breadcrumb";
@@ -40,17 +43,27 @@ interface StatusBarContext {
export default function StatusBar() { export default function StatusBar() {
const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext(); 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 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(); const isHiddenNote = note?.isInHiddenSubtree();
return ( return (
<div className="status-bar"> <div className="status-bar">
{attributesContext && <AttributesPane {...attributesContext} />} {attributesContext && <AttributesPane {...attributesContext} />}
{noteInfoContext && <SimilarNotesPane {...noteInfoContext} />}
<div className="status-bar-main-row"> <div className="status-bar-main-row">
{context && attributesContext && <> {context && attributesContext && noteInfoContext && <>
<Breadcrumb {...context} /> <Breadcrumb {...context} />
<div className="actions-row"> <div className="actions-row">
@@ -60,7 +73,7 @@ export default function StatusBar() {
<AttributesButton {...attributesContext} /> <AttributesButton {...attributesContext} />
<AttachmentCount {...context} /> <AttachmentCount {...context} />
<BacklinksBadge {...context} /> <BacklinksBadge {...context} />
<NoteInfoBadge {...context} /> <NoteInfoBadge {...noteInfoContext} />
</div> </div>
</>} </>}
</div> </div>
@@ -81,6 +94,7 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions
...titleOptions?.popperConfig, ...titleOptions?.popperConfig,
strategy: "fixed" strategy: "fixed"
}, },
animation: false,
...titleOptions ...titleOptions
}} }}
dropdownOptions={{ dropdownOptions={{
@@ -92,7 +106,7 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions
}} }}
text={<> text={<>
{icon && (<><Icon icon={icon} />&nbsp;</>)} {icon && (<><Icon icon={icon} />&nbsp;</>)}
{text} <span className="text">{text}</span>
</>} </>}
{...dropdownProps} {...dropdownProps}
> >
@@ -105,7 +119,7 @@ interface StatusBarButtonBaseProps {
className?: string; className?: string;
icon: string; icon: string;
title: string; title: string;
text?: string | number; text: string | number;
disabled?: boolean; disabled?: boolean;
active?: boolean; active?: boolean;
} }
@@ -120,6 +134,7 @@ function StatusBarButton({ className, icon, text, title, active, ...restProps }:
placement: "top", placement: "top",
fallbackPlacements: [ "top" ], fallbackPlacements: [ "top" ],
popperConfig: { strategy: "fixed" }, popperConfig: { strategy: "fixed" },
animation: false,
title title
}); });
@@ -136,7 +151,7 @@ function StatusBarButton({ className, icon, text, title, active, ...restProps }:
} }
}} }}
> >
<Icon icon={icon} />&nbsp;{text} <Icon icon={icon} />&nbsp;<span className="text">{text}</span>
</button> </button>
); );
} }
@@ -194,24 +209,41 @@ export function getLocaleName(locale: Locale | null | undefined) {
} }
//#endregion //#endregion
//#region Note info //#region Note info & Similar
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) { interface NoteInfoContext extends StatusBarContext {
similarNotesShown: boolean;
setSimilarNotesShown: (value: boolean) => void;
}
export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) {
const dropdownRef = useRef<BootstrapDropdown>(null);
const { metadata, ...sizeProps } = useNoteMetadata(note); const { metadata, ...sizeProps } = useNoteMetadata(note);
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
return (note && return (note &&
<StatusBarDropdown <StatusBarDropdown
icon="bx bx-info-circle" icon="bx bx-info-circle"
title={t("status_bar.note_info_title")} title={t("status_bar.note_info_title")}
dropdownRef={dropdownRef}
dropdownContainerClassName="dropdown-note-info" dropdownContainerClassName="dropdown-note-info"
dropdownOptions={{ autoClose: "outside" }} dropdownOptions={{ autoClose: "outside" }}
> >
<ul> <ul>
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} /> <NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} /> <NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} /> <NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} /> <NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} /> <NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
</ul> </ul>
<LinkButton
text={t("note_info_widget.show_similar_notes")}
onClick={() => {
dropdownRef.current?.hide();
setSimilarNotesShown(true);
}}
/>
</StatusBarDropdown> </StatusBarDropdown>
); );
} }
@@ -224,6 +256,14 @@ function NoteInfoValue({ text, title, value }: { text: string; title?: string, v
</li> </li>
); );
} }
function SimilarNotesPane({ note, similarNotesShown }: NoteInfoContext) {
return (similarNotesShown &&
<div className="similar-notes-pane">
<SimilarNotesTab note={note} />
</div>
);
}
//#endregion //#endregion
//#region Backlinks //#region Backlinks
@@ -233,7 +273,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) {
<StatusBarDropdown <StatusBarDropdown
className="backlinks-badge backlinks-widget" className="backlinks-badge backlinks-widget"
icon="bx bx-link" icon="bx bx-link"
text={count} text={t("status_bar.backlinks", { count })}
title={t("status_bar.backlinks_title", { count })} title={t("status_bar.backlinks_title", { count })}
dropdownContainerClassName="backlinks-items" dropdownContainerClassName="backlinks-items"
> >
@@ -252,7 +292,7 @@ function AttachmentCount({ note }: StatusBarContext) {
<StatusBarButton <StatusBarButton
className="attachment-count-button" className="attachment-count-button"
icon="bx bx-paperclip" icon="bx bx-paperclip"
text={count} text={t("status_bar.attachments", { count })}
title={t("status_bar.attachments_title", { count })} title={t("status_bar.attachments_title", { count })}
triggerCommand="showAttachments" triggerCommand="showAttachments"
/> />
@@ -330,13 +370,14 @@ function AttributesPane({ note, noteContext, attributesShown, setAttributesShown
//#region Note paths //#region Note paths
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) { function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
const count = sortedNotePaths?.length ?? 0;
return ( return (count > 1 &&
<StatusBarDropdown <StatusBarDropdown
title={t("status_bar.note_paths_title")} title={t("status_bar.note_paths_title")}
dropdownContainerClassName="dropdown-note-paths" dropdownContainerClassName="dropdown-note-paths"
icon="bx bx-directions" icon="bx bx-directions"
text={sortedNotePaths?.length} text={t("status_bar.note_paths", { count })}
> >
<NotePathsWidget <NotePathsWidget
sortedNotePaths={sortedNotePaths} sortedNotePaths={sortedNotePaths}

View File

@@ -0,0 +1,20 @@
.collection-properties {
padding: 0;
display: flex;
gap: 0.25em;
align-items: center;
width: 100%;
max-width: unset;
font-size: 0.8em;
.dropdown-menu {
input.form-control {
padding: 2px 8px;
margin-left: 1em;
}
}
.spacer {
flex-grow: 1;
}
}

View File

@@ -1,9 +1,14 @@
import "./CollectionProperties.css";
import { t } from "i18next"; import { t } from "i18next";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime"; import { Fragment } from "preact/jsx-runtime";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { openInAppHelpFromUrl } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface"; import { ViewTypeOptions } from "../collections/interface";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown"; import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList"; import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox"; import FormTextBox from "../react/FormTextBox";
@@ -12,9 +17,6 @@ import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config"; import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab"; import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
import ActionButton from "../react/ActionButton";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { openInAppHelpFromUrl } from "../../services/utils";
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = { const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: "bx bxs-grid", grid: "bx bxs-grid",
@@ -30,12 +32,12 @@ export default function CollectionProperties({ note }: { note: FNote }) {
const [ viewType, setViewType ] = useViewType(note); const [ viewType, setViewType ] = useViewType(note);
return ( return (
<> <div className="collection-properties">
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} /> <ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
<ViewOptions note={note} viewType={viewType} /> <ViewOptions note={note} viewType={viewType} />
<div className="spacer" /> <div className="spacer" />
<HelpButton note={note} /> <HelpButton note={note} />
</> </div>
); );
} }
@@ -187,9 +189,9 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo
{index < property.options.length - 1 && <FormDropdownDivider />} {index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment> </Fragment>
); );
} else {
return renderItem(option);
} }
return renderItem(option);
})} })}
</FormDropdownSubmenu> </FormDropdownSubmenu>
); );

View File

@@ -10,7 +10,7 @@ import Icon from "./Icon";
interface SimpleBadgeProps { interface SimpleBadgeProps {
className?: string; className?: string;
title: string; title: ComponentChildren;
} }
interface BadgeProps { interface BadgeProps {

View File

@@ -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);
}
}
}

View File

@@ -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<HTMLAttributes<HTMLDivElement>, "className"> {
title: string;
children: ComponentChildren;
initiallyExpanded?: boolean;
}
export default function Collapsible({ title, children, className, initiallyExpanded }: CollapsibleProps) {
const bodyRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const [ expanded, setExpanded ] = useState(initiallyExpanded);
const { height } = useElementSize(innerRef) ?? {};
const contentId = useUniqueName();
return (
<div className={clsx("collapsible", className, expanded && "expanded")}>
<button
className="collapsible-title"
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
aria-controls={contentId}
>
<Icon className="arrow" icon="bx bx-chevron-right" />&nbsp;
{title}
</button>
<div
id={contentId}
ref={bodyRef}
className="collapsible-body"
style={{ height: expanded ? height : "0" }}
aria-hidden={!expanded}
>
<div
ref={innerRef}
className="collapsible-inner-body"
>
{children}
</div>
</div>
</div>
);
}

View File

@@ -1,7 +1,9 @@
import { Ref } from "preact"; import { Ref } from "preact";
import Button, { ButtonProps } from "./Button";
import { useEffect, useRef } from "preact/hooks"; import { useEffect, useRef } from "preact/hooks";
import ActionButton, { ActionButtonProps } from "./ActionButton";
import Button, { ButtonProps } from "./Button";
interface FormFileUploadProps { interface FormFileUploadProps {
name?: string; name?: string;
onChange: (files: FileList | null) => void; onChange: (files: FileList | null) => void;
@@ -26,7 +28,7 @@ export default function FormFileUpload({ inputRef, name, onChange, multiple, hid
multiple={multiple} multiple={multiple}
onChange={e => onChange((e.target as HTMLInputElement).files)} /> onChange={e => onChange((e.target as HTMLInputElement).files)} />
</label> </label>
) );
} }
/** /**
@@ -49,5 +51,27 @@ export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonPr
onChange={onChange} onChange={onChange}
/> />
</> </>
) );
}
/**
* 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<ActionButtonProps, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<ActionButton
{...buttonProps}
onClick={() => inputRef.current?.click()}
/>
<FormFileUpload
inputRef={inputRef}
hidden
onChange={onChange}
/>
</>
);
} }

View File

@@ -1,13 +1,15 @@
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
import { ComponentChildren } from "preact";
import Icon from "./Icon";
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact/compat";
import "./FormList.css"; import "./FormList.css";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks"; import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
import clsx from "clsx"; import clsx from "clsx";
import { ComponentChildren } from "preact";
import { type CSSProperties,useEffect, useMemo, useRef, useState } from "preact/compat";
import { CommandNames } from "../../components/app_context";
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
import FormToggle from "./FormToggle"; import FormToggle from "./FormToggle";
import { useStaticTooltip } from "./hooks";
import Icon from "./Icon";
interface FormListOpts { interface FormListOpts {
children: ComponentChildren; children: ComponentChildren;
@@ -33,7 +35,7 @@ export default function FormList({ children, onSelect, style, fullHeight, wrappe
return () => { return () => {
$wrapperRef.off("hide.bs.dropdown"); $wrapperRef.off("hide.bs.dropdown");
dropdown.dispose(); dropdown.dispose();
} };
}, [ triggerRef, wrapperRef ]); }, [ triggerRef, wrapperRef ]);
const builtinStyles = useMemo(() => { const builtinStyles = useMemo(() => {
@@ -51,8 +53,7 @@ export default function FormList({ children, onSelect, style, fullHeight, wrappe
<button <button
ref={triggerRef} ref={triggerRef}
type="button" style="display: none;" type="button" style="display: none;"
data-bs-toggle="dropdown" data-bs-display="static"> data-bs-toggle="dropdown" data-bs-display="static" />
</button>
<div class="dropdown-menu static show" style={{ <div class="dropdown-menu static show" style={{
...style ?? {}, ...style ?? {},
@@ -199,7 +200,7 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
<li> <li>
<h6 className="dropdown-header">{text}</h6> <h6 className="dropdown-header">{text}</h6>
</li> </li>
) );
} }
export function FormDropdownDivider() { export function FormDropdownDivider() {
@@ -216,21 +217,22 @@ export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdo
const [ openOnMobile, setOpenOnMobile ] = useState(false); const [ openOnMobile, setOpenOnMobile ] = useState(false);
return ( return (
<li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}> <li
<span className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}
className="dropdown-toggle"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (isMobile()) { if (!isMobile() && onDropdownToggleClicked) {
setOpenOnMobile(!openOnMobile);
}
if (onDropdownToggleClicked) {
onDropdownToggleClicked(); onDropdownToggleClicked();
} }
}} }}
> >
<span className="dropdown-toggle" onClick={(e) => {
if (isMobile()) {
e.stopPropagation();
setOpenOnMobile(!openOnMobile);
}
}}>
<Icon icon={icon} />{" "} <Icon icon={icon} />{" "}
{title} {title}
</span> </span>

View File

@@ -1,15 +1,25 @@
import { ComponentChild } from "preact"; import { ComponentChild } from "preact";
import { CommandNames } from "../../components/app_context";
interface LinkButtonProps { interface LinkButtonProps {
onClick: () => void; onClick?: () => void;
text: ComponentChild; text: ComponentChild;
triggerCommand?: CommandNames;
} }
export default function LinkButton({ onClick, text }: LinkButtonProps) { export default function LinkButton({ onClick, text, triggerCommand }: LinkButtonProps) {
return ( return (
<a class="tn-link" href="javascript:" onClick={(e) => { <a class="tn-link" href="#"
data-trigger-command={triggerCommand}
role="button"
onKeyDown={(e)=> {
if (e.code === "Space") {
onClick?.();
}
}}
onClick={(e) => {
e.preventDefault(); e.preventDefault();
onClick(); onClick?.();
}}> }}>
{text} {text}
</a> </a>

View File

@@ -1,24 +1,16 @@
import { useEffect, useState } from "preact/hooks";
import { TabContext } from "./ribbon-interface";
import { EditedNotesResponse } from "@triliumnext/commons"; import { EditedNotesResponse } from "@triliumnext/commons";
import server from "../../services/server"; import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import FNote from "../../entities/fnote";
import froca from "../../services/froca"; import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
import NoteLink from "../react/NoteLink"; import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils"; import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
export default function EditedNotesTab({ note }: TabContext) { export default function EditedNotesTab({ note }: TabContext) {
const [ editedNotes, setEditedNotes ] = useState<EditedNotesResponse>(); const editedNotes = useEditedNotes(note);
useEffect(() => {
if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes);
});
}, [ note?.noteId ]);
return ( return (
<div className="edited-notes-widget" style={{ <div className="edited-notes-widget" style={{
@@ -31,7 +23,7 @@ export default function EditedNotesTab({ note }: TabContext) {
<div className="edited-notes-list use-tn-links"> <div className="edited-notes-list use-tn-links">
{joinElements(editedNotes.map(editedNote => { {joinElements(editedNotes.map(editedNote => {
return ( return (
<span className="edited-note-line"> <span key={editedNote.noteId} className="edited-note-line">
{editedNote.isDeleted ? ( {editedNote.isDeleted ? (
<i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i> <i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i>
) : ( ) : (
@@ -40,12 +32,28 @@ export default function EditedNotesTab({ note }: TabContext) {
</> </>
)} )}
</span> </span>
) );
}), " ")} }), " ")}
</div> </div>
) : ( ) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div> <div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
)} )}
</div> </div>
) );
}
export function useEditedNotes(note: FNote | null | undefined) {
const [ editedNotes, setEditedNotes ] = useState<EditedNotesResponse>();
useEffect(() => {
if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes);
});
}, [ note ]);
return editedNotes;
} }

View File

@@ -1,13 +1,13 @@
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import toast from "../../services/toast";
import { formatSize } from "../../services/utils"; import { formatSize } from "../../services/utils";
import Button from "../react/Button";
import { FormFileUploadButton } from "../react/FormFileUpload"; import { FormFileUploadButton } from "../react/FormFileUpload";
import { useNoteBlob, useNoteLabel } from "../react/hooks"; import { useNoteBlob, useNoteLabel } from "../react/hooks";
import Button from "../react/Button";
import protected_session_holder from "../../services/protected_session_holder";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import toast from "../../services/toast";
import server from "../../services/server";
import FNote from "../../entities/fnote";
export default function FilePropertiesTab({ note }: { note?: FNote | null }) { export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName"); const [ originalFileName ] = useNoteLabel(note, "originalFileName");
@@ -54,7 +54,20 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
icon="bx bx-folder-open" icon="bx bx-folder-open"
text={t("file_properties.upload_new_revision")} text={t("file_properties.upload_new_revision")}
disabled={!canAccessProtectedNote} disabled={!canAccessProtectedNote}
onChange={(fileToUpload) => { onChange={buildUploadNewFileRevisionListener(note)}
/>
</div>
</td>
</tr>
</tbody>
</table>
)}
</div>
);
}
export function buildUploadNewFileRevisionListener(note: FNote) {
return (fileToUpload: FileList | null) => {
if (!fileToUpload) { if (!fileToUpload) {
return; return;
} }
@@ -66,14 +79,5 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
toast.showError(t("file_properties.upload_failed")); toast.showError(t("file_properties.upload_failed"));
} }
}); });
}} };
/>
</div>
</td>
</tr>
</tbody>
</table>
)}
</div>
);
} }

View File

@@ -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 { 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 server from "../../services/server";
import toast from "../../services/toast"; 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) { export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName"); const [ originalFileName ] = useNoteLabel(note, "originalFileName");
@@ -60,7 +62,17 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
<FormFileUploadButton <FormFileUploadButton
text={t("image_properties.upload_new_revision")} text={t("image_properties.upload_new_revision")}
icon="bx bx-folder-open" icon="bx bx-folder-open"
onChange={async (files) => { onChange={buildUploadNewImageRevisionListener(note)}
/>
</div>
</>
)}
</div>
);
}
export function buildUploadNewImageRevisionListener(note: FNote) {
return async (files: FileList | null) => {
if (!files) return; if (!files) return;
const fileToUpload = files[0]; // copy to allow reset below const fileToUpload = files[0]; // copy to allow reset below
@@ -72,11 +84,5 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
} else { } else {
toast.showError(t("image_properties.upload_failed", { message: result.message })); toast.showError(t("image_properties.upload_failed", { message: result.message }));
} }
}} };
/>
</div>
</>
)}
</div>
)
} }

View File

@@ -1,31 +1,44 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons"; import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { useContext, useState } from "preact/hooks"; import { useContext } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context"; import appContext, { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import branches from "../../services/branches"; import branches from "../../services/branches";
import dialog from "../../services/dialog"; import dialog from "../../services/dialog";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import protected_session from "../../services/protected_session";
import server from "../../services/server"; import server from "../../services/server";
import toast from "../../services/toast"; import toast from "../../services/toast";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils"; import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import ws from "../../services/ws"; 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 ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown"; import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList"; import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab"; import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
import protected_session from "../../services/protected_session"; import NoteActionsCustom from "./NoteActionsCustom";
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function NoteActions() { export default function NoteActions() {
const { note, noteContext } = useNoteContext(); const { note, ntxId, noteContext } = useNoteContext();
return ( return (
<div className="ribbon-button-container" style={{ contain: "none" }}> <div className="ribbon-button-container" style={{ contain: "none" }}>
{isNewLayout && (
<>
{note && ntxId && <NoteActionsCustom note={note} ntxId={ntxId} />}
<MovePaneButton direction="left" />
<MovePaneButton direction="right" />
<ClosePaneButton />
<CreatePaneButton />
</>
)}
{note && !isNewLayout && <RevisionsButton note={note} />} {note && !isNewLayout && <RevisionsButton note={note} />}
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />} {note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
</div> </div>

View File

@@ -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 &&
<div className="note-actions-custom">
<CopyReferenceToClipboardButton {...innerProps} />
<NoteActionsCustomInner {...innerProps} />
</div>
);
}
//#region Note type mappings
function NoteActionsCustomInner(props: NoteActionsCustomInnerProps) {
switch (props.note.type) {
case "file":
return <FileActions {...props} />;
case "image":
return <ImageActions {...props} />;
default:
return null;
}
}
function FileActions(props: NoteActionsCustomInnerProps) {
return (
<>
<UploadNewRevisionButton {...props} onChange={buildUploadNewFileRevisionListener(props.note)} />
<OpenExternallyButton {...props} />
<DownloadFileButton {...props} />
</>
);
}
function ImageActions(props: NoteActionsCustomInnerProps) {
return (
<>
<UploadNewRevisionButton {...props} onChange={buildUploadNewImageRevisionListener(props.note)} />
<OpenExternallyButton {...props} />
<DownloadFileButton {...props} />
</>
);
}
//#endregion
//#region Shared buttons
function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps & {
onChange: (files: FileList | null) => void;
}) {
return (
<FormFileUploadActionButton
icon="bx bx-folder-open"
text={t("image_properties.upload_new_revision")}
disabled={!note.isContentAvailable()}
onChange={onChange}
/>
);
}
function OpenExternallyButton({ note }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-link-external"
text={t("file_properties.open")}
disabled={note.isProtected}
onClick={() => openNoteExternally(note.noteId, note.mime)}
/>
);
}
function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-download"
text={t("file_properties.download")}
disabled={!note.isContentAvailable()}
onClick={() => downloadFileNote(note.noteId)}
/>
);
}
function CopyReferenceToClipboardButton({ ntxId, noteType }: NoteActionsCustomInnerProps) {
const parentComponent = useContext(ParentComponent);
return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) &&
<ActionButton
text={t("image_properties.copy_reference_to_clipboard")}
icon="bx bx-copy"
onClick={() => parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })}
/>
);
}
//#endregion

View File

@@ -9,6 +9,7 @@ import LoadingSpinner from "../react/LoadingSpinner";
import { useTriliumEvent } from "../react/hooks"; import { useTriliumEvent } from "../react/hooks";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import LinkButton from "../react/LinkButton";
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); 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<ReturnType<typeof useNoteMetadata>, "metadata">) { export function NoteSizeWidget({ isLoading, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }: Omit<ReturnType<typeof useNoteMetadata>, "metadata">) {
return <> return <>
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && ( {!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<Button <LinkButton
className="calculate-button"
icon="bx bx-calculator"
text={t("note_info_widget.calculate")} text={t("note_info_widget.calculate")}
onClick={requestSizeInfo} onClick={requestSizeInfo}
/> />

View File

@@ -3,11 +3,13 @@ import { useEffect, useMemo, useState } from "preact/hooks";
import FNote, { NotePathRecord } from "../../entities/fnote"; import FNote, { NotePathRecord } from "../../entities/fnote";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree"; import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks"; import { useTriliumEvent } from "../react/hooks";
import NoteLink from "../react/NoteLink"; import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils"; import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface"; import { TabContext } from "./ribbon-interface";
import LinkButton from "../react/LinkButton";
import clsx from "clsx";
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) { export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
@@ -35,9 +37,9 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
)) : undefined} )) : undefined}
</ul> </ul>
<Button <LinkButton
triggerCommand="cloneNoteIdsTo"
text={t("note_paths.clone_button")} text={t("note_paths.clone_button")}
triggerCommand="cloneNoteIdsTo"
/> />
</> </>
</div> </div>
@@ -108,12 +110,15 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
return ( return (
<li class={classes}> <li class={classes}>
{joinElements(fullNotePaths.map(notePath => ( {joinElements(fullNotePaths.map((notePath, index, arr) => (
<NoteLink key={notePath} notePath={notePath} noPreview /> <NoteLink key={notePath}
className={clsx({"basename": (index === arr.length - 1)})}
notePath={notePath}
noPreview />
)), NOTE_PATH_TITLE_SEPARATOR)} )), NOTE_PATH_TITLE_SEPARATOR)}
{icons.map(({ icon, title }) => ( {icons.map(({ icon, title }) => (
<span key={title} class={icon} title={title} /> <i key={title} class={icon} title={title} />
))} ))}
</li> </li>
); );

View File

@@ -5,9 +5,9 @@ import clsx from "clsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { EventNames } from "../../components/app_context"; import { EventNames } from "../../components/app_context";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { Indexed, numberObjectsInPlace } from "../../services/utils"; import { Indexed, numberObjectsInPlace } from "../../services/utils";
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks"; import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import NoteActions from "./NoteActions";
import { TabConfiguration, TitleContext } from "./ribbon-interface"; import { TabConfiguration, TitleContext } from "./ribbon-interface";
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition"; import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
@@ -17,9 +17,7 @@ interface ComputedTab extends Indexed<TabConfiguration> {
shouldShow: boolean; shouldShow: boolean;
} }
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); export default function Ribbon() {
export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext(); const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
const noteType = useNoteProperty(note, "type"); const noteType = useNoteProperty(note, "type");
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>(); const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
@@ -32,8 +30,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
async function refresh() { async function refresh() {
const computedTabs: ComputedTab[] = []; const computedTabs: ComputedTab[] = [];
for (const tab of TAB_CONFIGURATION) { for (const tab of TAB_CONFIGURATION) {
const shouldAvoid = (isNewLayout && tab.avoidInNewLayout); const shouldShow = await shouldShowTab(tab.show, titleContext);
const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext);
computedTabs.push({ computedTabs.push({
...tab, ...tab,
shouldShow: !!shouldShow shouldShow: !!shouldShow
@@ -92,7 +89,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
/> />
))} ))}
</div> </div>
{children} <NoteActions />
</div> </div>
<div className="ribbon-body-container"> <div className="ribbon-body-container">
@@ -115,7 +112,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
noteContext={noteContext} noteContext={noteContext}
componentId={componentId} componentId={componentId}
activate={useCallback(() => { activate={useCallback(() => {
setActiveTabIndex(tab.index) setActiveTabIndex(tab.index);
}, [setActiveTabIndex])} }, [setActiveTabIndex])}
/> />
</div> </div>

View File

@@ -1,4 +1,3 @@
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import options from "../../services/options"; import options from "../../services/options";
import BasicPropertiesTab from "./BasicPropertiesTab"; import BasicPropertiesTab from "./BasicPropertiesTab";
@@ -18,8 +17,6 @@ import ScriptTab from "./ScriptTab";
import SearchDefinitionTab from "./SearchDefinitionTab"; import SearchDefinitionTab from "./SearchDefinitionTab";
import SimilarNotesTab from "./SimilarNotesTab"; import SimilarNotesTab from "./SimilarNotesTab";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{ {
title: t("classic_editor_toolbar.title"), title: t("classic_editor_toolbar.title"),
@@ -30,15 +27,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
toggleCommand: "toggleRibbonTabClassicEditor", toggleCommand: "toggleRibbonTabClassicEditor",
content: FormattingToolbar, content: FormattingToolbar,
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"), activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
stayInDom: !isNewLayout, stayInDom: true
avoidInNewLayout: true
}, },
{ {
title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"), title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
icon: "bx bx-play", icon: "bx bx-play",
content: ScriptTab, content: ScriptTab,
activate: true, activate: true,
show: ({ note }) => note && !isNewLayout && show: ({ note }) => note &&
(note.isTriliumScript() || note.isTriliumSqlite()) && (note.isTriliumScript() || note.isTriliumSqlite()) &&
(note.hasLabel("executeDescription") || note.hasLabel("executeButton")) (note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
}, },
@@ -60,14 +56,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
title: t("book_properties.book_properties"), title: t("book_properties.book_properties"),
icon: "bx bx-book", icon: "bx bx-book",
content: CollectionPropertiesTab, content: CollectionPropertiesTab,
show: ({ note }) => !isNewLayout && note?.type === "book" || note?.type === "search", show: ({ note }) => (note?.type === "book" || note?.type === "search"),
toggleCommand: "toggleRibbonTabBookProperties" toggleCommand: "toggleRibbonTabBookProperties"
}, },
{ {
title: t("note_properties.info"), title: t("note_properties.info"),
icon: "bx bx-info-square", icon: "bx bx-info-square",
content: NotePropertiesTab, content: NotePropertiesTab,
show: ({ note }) => !isNewLayout && !!note?.getLabelValue("pageUrl"), show: ({ note }) => !!note?.getLabelValue("pageUrl"),
activate: true activate: true
}, },
{ {
@@ -90,49 +86,49 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
title: t("basic_properties.basic_properties"), title: t("basic_properties.basic_properties"),
icon: "bx bx-slider", icon: "bx bx-slider",
content: BasicPropertiesTab, content: BasicPropertiesTab,
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), show: ({note}) => !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabBasicProperties" toggleCommand: "toggleRibbonTabBasicProperties"
}, },
{ {
title: t("owned_attribute_list.owned_attributes"), title: t("owned_attribute_list.owned_attributes"),
icon: "bx bx-list-check", icon: "bx bx-list-check",
content: OwnedAttributesTab, content: OwnedAttributesTab,
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), show: ({note}) => !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabOwnedAttributes", toggleCommand: "toggleRibbonTabOwnedAttributes",
stayInDom: !isNewLayout stayInDom: true
}, },
{ {
title: t("inherited_attribute_list.title"), title: t("inherited_attribute_list.title"),
icon: "bx bx-list-plus", icon: "bx bx-list-plus",
content: InheritedAttributesTab, content: InheritedAttributesTab,
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), show: ({note}) => !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabInheritedAttributes" toggleCommand: "toggleRibbonTabInheritedAttributes"
}, },
{ {
title: t("note_paths.title"), title: t("note_paths.title"),
icon: "bx bx-collection", icon: "bx bx-collection",
content: NotePathsTab, content: NotePathsTab,
show: !isNewLayout, show: true,
toggleCommand: "toggleRibbonTabNotePaths" toggleCommand: "toggleRibbonTabNotePaths"
}, },
{ {
title: t("note_map.title"), title: t("note_map.title"),
icon: "bx bxs-network-chart", icon: "bx bxs-network-chart",
content: NoteMapTab, content: NoteMapTab,
show: !isNewLayout, show: true,
toggleCommand: "toggleRibbonTabNoteMap" toggleCommand: "toggleRibbonTabNoteMap"
}, },
{ {
title: t("similar_notes.title"), title: t("similar_notes.title"),
icon: "bx bx-bar-chart", icon: "bx bx-bar-chart",
show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"), show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
content: SimilarNotesTab, content: SimilarNotesTab,
toggleCommand: "toggleRibbonTabSimilarNotes" toggleCommand: "toggleRibbonTabSimilarNotes"
}, },
{ {
title: t("note_info_widget.title"), title: t("note_info_widget.title"),
icon: "bx bx-info-circle", icon: "bx bx-info-circle",
show: ({ note }) => !isNewLayout && !!note, show: ({ note }) => !!note,
content: NoteInfoTab, content: NoteInfoTab,
toggleCommand: "toggleRibbonTabNoteInfo" toggleCommand: "toggleRibbonTabNoteInfo"
} }

View File

@@ -1,18 +1,19 @@
import FormTextArea from "../react/FormTextArea"; import { AttributeType } from "@triliumnext/commons";
import NoteAutocomplete from "../react/NoteAutocomplete";
import FormSelect from "../react/FormSelect";
import Icon from "../react/Icon";
import FormTextBox from "../react/FormTextBox";
import { ComponentChildren, VNode } from "preact"; import { ComponentChildren, VNode } from "preact";
import { useEffect, useMemo, useRef } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import { removeOwnedAttributesByNameOrType } from "../../services/attributes"; import { removeOwnedAttributesByNameOrType } from "../../services/attributes";
import { AttributeType } from "@triliumnext/commons";
import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { useEffect, useMemo, useRef } from "preact/hooks";
import appContext from "../../components/app_context";
import server from "../../services/server"; import server from "../../services/server";
import FormSelect from "../react/FormSelect";
import FormTextArea from "../react/FormTextArea";
import FormTextBox from "../react/FormTextBox";
import HelpRemoveButtons from "../react/HelpRemoveButtons"; import HelpRemoveButtons from "../react/HelpRemoveButtons";
import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
import Icon from "../react/Icon";
import NoteAutocomplete from "../react/NoteAutocomplete";
export interface SearchOption { export interface SearchOption {
attributeName: string; attributeName: string;
@@ -134,7 +135,7 @@ function SearchOption({ note, title, titleIcon, children, help, attributeName, a
}} }}
/> />
</tr> </tr>
) );
} }
function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) { function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) {
@@ -210,7 +211,7 @@ function SearchStringOption({ note, refreshResults, error, ...restProps }: Searc
} }
}} }}
/> />
</SearchOption> </SearchOption>;
} }
function SearchScriptOption({ note, ...restProps }: SearchOptionProps) { function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
@@ -232,7 +233,7 @@ function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
noteIdChanged={noteId => setSearchScript(noteId ?? "root")} noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
placeholder={t("search_script.placeholder")} placeholder={t("search_script.placeholder")}
/> />
</SearchOption> </SearchOption>;
} }
function AncestorOption({ note, ...restProps}: SearchOptionProps) { function AncestorOption({ note, ...restProps}: SearchOptionProps) {
@@ -245,9 +246,9 @@ function AncestorOption({ note, ...restProps}: SearchOptionProps) {
{ value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` } { value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` }
]; ];
for (let i=2; i<=9; i++) options.push({ value: "eq" + i, label: t("ancestor.depth_eq", { count: i }) }); for (let i=2; i<=9; i++) options.push({ value: `eq${ i}`, label: t("ancestor.depth_eq", { count: i }) });
for (let i=0; i<=9; i++) options.push({ value: "gt" + i, label: t("ancestor.depth_gt", { count: i }) }); for (let i=0; i<=9; i++) options.push({ value: `gt${ i}`, label: t("ancestor.depth_gt", { count: i }) });
for (let i=2; i<=9; i++) options.push({ value: "lt" + i, label: t("ancestor.depth_lt", { count: i }) }); for (let i=2; i<=9; i++) options.push({ value: `lt${ i}`, label: t("ancestor.depth_lt", { count: i }) });
return options; return options;
}, []); }, []);
@@ -279,7 +280,7 @@ function FastSearchOption({ ...restProps }: SearchOptionProps) {
titleIcon="bx bx-run" title={t("fast_search.fast_search")} titleIcon="bx bx-run" title={t("fast_search.fast_search")}
help={t("fast_search.description")} help={t("fast_search.description")}
{...restProps} {...restProps}
/> />;
} }
function DebugOption({ ...restProps }: SearchOptionProps) { function DebugOption({ ...restProps }: SearchOptionProps) {
@@ -290,14 +291,14 @@ function DebugOption({ ...restProps }: SearchOptionProps) {
{t("debug.access_info")} {t("debug.access_info")}
</>} </>}
{...restProps} {...restProps}
/> />;
} }
function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) { function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) {
return <SearchOption return <SearchOption
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")} titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
{...restProps} {...restProps}
/> />;
} }
function OrderByOption({ note, ...restProps }: SearchOptionProps) { function OrderByOption({ note, ...restProps }: SearchOptionProps) {
@@ -340,7 +341,7 @@ function OrderByOption({ note, ...restProps }: SearchOptionProps) {
{ value: "desc", title: t("order_by.desc") } { value: "desc", title: t("order_by.desc") }
]} ]}
/> />
</SearchOption> </SearchOption>;
} }
function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) { function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) {
@@ -356,5 +357,5 @@ function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) {
type="number" min="1" step="1" type="number" min="1" step="1"
currentValue={limit ?? defaultValue} onChange={setLimit} currentValue={limit ?? defaultValue} onChange={setLimit}
/> />
</SearchOption> </SearchOption>;
} }

View File

@@ -1,28 +1,34 @@
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { TabContext } from "./ribbon-interface";
import { SaveSearchNoteResponse } from "@triliumnext/commons";
import attributes from "../../services/attributes";
import FNote from "../../entities/fnote";
import toast from "../../services/toast";
import froca from "../../services/froca";
import { useContext, useEffect, useState } from "preact/hooks";
import { ParentComponent } from "../react/react_utils";
import { useTriliumEvent } from "../react/hooks";
import appContext from "../../components/app_context";
import server from "../../services/server";
import ws from "../../services/ws";
import tree from "../../services/tree";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
import Dropdown from "../react/Dropdown";
import Icon from "../react/Icon";
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
import { FormListHeader, FormListItem } from "../react/FormList";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import { getErrorMessage } from "../../services/utils";
import "./SearchDefinitionTab.css"; import "./SearchDefinitionTab.css";
export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext) { import { SaveSearchNoteResponse } from "@triliumnext/commons";
import { useContext, useEffect, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
import tree from "../../services/tree";
import { getErrorMessage } from "../../services/utils";
import ws from "../../services/ws";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import CollectionProperties from "../note_bars/CollectionProperties";
import Button from "../react/Button";
import Dropdown from "../react/Dropdown";
import { FormListHeader, FormListItem } from "../react/FormList";
import { useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>(); const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
const [ error, setError ] = useState<{ message: string }>(); const [ error, setError ] = useState<{ message: string }>();
@@ -54,7 +60,7 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext)
try { try {
const result = await froca.loadSearchNote(noteId); const result = await froca.loadSearchNote(noteId);
if (result?.error) { if (result?.error) {
setError({ message: result?.error}) setError({ message: result?.error});
} else { } else {
setError(undefined); setError(undefined);
} }
@@ -76,7 +82,7 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext)
return ( return (
<div className="search-definition-widget"> <div className="search-definition-widget">
<div className="search-settings"> <div className="search-settings">
{note && !hidden && {note && !hidden && (
<table className="search-setting-table"> <table className="search-setting-table">
<tbody> <tbody>
<tr> <tr>
@@ -109,6 +115,11 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext)
defaultValue={defaultValue} defaultValue={defaultValue}
/>; />;
})} })}
{isNewLayout && <tr className="view-options">
<td className="title-column">{t("search_definition.view_options")}</td>
<td><CollectionProperties note={note} /></td>
</tr>}
</tbody> </tbody>
<BulkActionsList note={note} /> <BulkActionsList note={note} />
<tbody className="search-actions"> <tbody className="search-actions">
@@ -154,10 +165,10 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext)
</tr> </tr>
</tbody> </tbody>
</table> </table>
} )}
</div> </div>
</div> </div>
) );
} }
function BulkActionsList({ note }: { note: FNote }) { function BulkActionsList({ note }: { note: FNote }) {
@@ -183,7 +194,7 @@ function BulkActionsList({ note }: { note: FNote }) {
bulkAction.doRender() bulkAction.doRender()
))} ))}
</tbody> </tbody>
) );
} }
function AddBulkActionButton({ note }: { note: FNote }) { function AddBulkActionButton({ note }: { note: FNote }) {
@@ -203,5 +214,5 @@ function AddBulkActionButton({ note }: { note: FNote }) {
</> </>
))} ))}
</Dropdown> </Dropdown>
) );
} }

View File

@@ -1,12 +1,13 @@
import { useEffect, useState } from "preact/hooks";
import { TabContext } from "./ribbon-interface";
import { SimilarNoteResponse } from "@triliumnext/commons"; import { SimilarNoteResponse } from "@triliumnext/commons";
import server from "../../services/server"; import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import froca from "../../services/froca";
import NoteLink from "../react/NoteLink";
export default function SimilarNotesTab({ note }: TabContext) { import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
import NoteLink from "../react/NoteLink";
import { TabContext } from "./ribbon-interface";
export default function SimilarNotesTab({ note }: Pick<TabContext, "note">) {
const [ similarNotes, setSimilarNotes ] = useState<SimilarNoteResponse>(); const [ similarNotes, setSimilarNotes ] = useState<SimilarNoteResponse>();
useEffect(() => { useEffect(() => {
@@ -42,5 +43,5 @@ export default function SimilarNotesTab({ note }: TabContext) {
)} )}
</div> </div>
</div> </div>
) );
} }

View File

@@ -1,7 +1,8 @@
import { KeyboardActionNames } from "@triliumnext/commons"; import { KeyboardActionNames } from "@triliumnext/commons";
import { VNode } from "preact";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import { VNode } from "preact";
export interface TabContext { export interface TabContext {
note: FNote | null | undefined; note: FNote | null | undefined;
@@ -30,5 +31,4 @@ export interface TabConfiguration {
* By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed. * By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
*/ */
stayInDom?: boolean; stayInDom?: boolean;
avoidInNewLayout?: boolean;
} }

View File

@@ -207,9 +207,6 @@ body.experimental-feature-new-layout .classic-toolbar-widget {
vertical-align: middle !important; vertical-align: middle !important;
} }
.note-info-widget .calculate-button {
padding: 0 10px;
}
/* #endregion */ /* #endregion */
/* #region Similar Notes */ /* #region Similar Notes */
@@ -448,8 +445,22 @@ body.experimental-feature-new-layout {
} }
.ribbon-button-container { .ribbon-button-container {
--button-gap: 5px;
border-bottom: 0 !important; border-bottom: 0 !important;
margin: 0; margin: 0;
gap: var(--button-gap);
.note-actions-custom {
display: flex;
align-items: center;
height: 36px;
gap: var(--button-gap);
&> button:last-of-type {
margin-right: 1em;
}
}
} }
} }
/* #endregion */ /* #endregion */

View File

@@ -18,6 +18,7 @@ type Labels = {
language: string; language: string;
originalFileName: string; originalFileName: string;
pageUrl: string; pageUrl: string;
dateNote: string;
// Search // Search
searchString: string; searchString: string;