mirror of
https://github.com/zadam/trilium.git
synced 2025-12-15 20:59:54 +01:00
New layout refinement (#8053)
This commit is contained in:
@@ -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(<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)
|
||||
.setParent(appContext)
|
||||
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
|
||||
@@ -144,9 +131,19 @@ export default class DesktopLayout {
|
||||
.child(
|
||||
new SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
.child(titleRow)
|
||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||
.optChild(isNewLayout, <Ribbon />)
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.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(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
|
||||
@@ -253,8 +254,11 @@ function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonCon
|
||||
|
||||
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const hiddenImageCopyRef = useRef<HTMLDivElement>(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) {
|
||||
|
||||
@@ -139,6 +139,8 @@ function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNoteP
|
||||
return (
|
||||
<ul className="breadcrumb-child-list">
|
||||
{childNotes.map((note) => {
|
||||
if (note.noteId === "_hidden") return;
|
||||
|
||||
const childNotePath = `${notePath}/${note.noteId}`;
|
||||
return <li key={note.noteId}>
|
||||
<FormListItem
|
||||
|
||||
@@ -89,3 +89,24 @@ body.prefers-centered-content .inline-title {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,13 @@ import server from "../../services/server";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import { Badge, BadgeWithDropdown } from "../react/Badge";
|
||||
import SimpleBadge, { Badge, BadgeWithDropdown } from "../react/Badge";
|
||||
import Collapsible from "../react/Collapsible";
|
||||
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 { useEditedNotes } from "../ribbon/EditedNotesTab";
|
||||
import { useNoteMetadata } from "../ribbon/NoteInfoTab";
|
||||
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||
|
||||
@@ -72,6 +75,7 @@ export default function InlineTitle() {
|
||||
</div>
|
||||
|
||||
<NoteTitleDetails />
|
||||
<EditedNotes />
|
||||
<NoteTypeSwitcher />
|
||||
</div>
|
||||
);
|
||||
@@ -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 &&
|
||||
<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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" && <SearchProperties note={note} ntxId={ntxId} />,
|
||||
note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<div className="title-actions">
|
||||
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
||||
<div className={clsx("title-actions", items.length > 0 && "visible")}>
|
||||
{items}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="status-bar">
|
||||
{attributesContext && <AttributesPane {...attributesContext} />}
|
||||
{noteInfoContext && <SimilarNotesPane {...noteInfoContext} />}
|
||||
|
||||
<div className="status-bar-main-row">
|
||||
{context && attributesContext && <>
|
||||
{context && attributesContext && noteInfoContext && <>
|
||||
<Breadcrumb {...context} />
|
||||
|
||||
<div className="actions-row">
|
||||
@@ -60,7 +73,7 @@ export default function StatusBar() {
|
||||
<AttributesButton {...attributesContext} />
|
||||
<AttachmentCount {...context} />
|
||||
<BacklinksBadge {...context} />
|
||||
<NoteInfoBadge {...context} />
|
||||
<NoteInfoBadge {...noteInfoContext} />
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
@@ -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 && (<><Icon icon={icon} /> </>)}
|
||||
{text}
|
||||
<span className="text">{text}</span>
|
||||
</>}
|
||||
{...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 }:
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} /> {text}
|
||||
<Icon icon={icon} /> <span className="text">{text}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -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<BootstrapDropdown>(null);
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
|
||||
|
||||
return (note &&
|
||||
<StatusBarDropdown
|
||||
icon="bx bx-info-circle"
|
||||
title={t("status_bar.note_info_title")}
|
||||
dropdownRef={dropdownRef}
|
||||
dropdownContainerClassName="dropdown-note-info"
|
||||
dropdownOptions={{ autoClose: "outside" }}
|
||||
>
|
||||
<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.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.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} />} />
|
||||
</ul>
|
||||
|
||||
<LinkButton
|
||||
text={t("note_info_widget.show_similar_notes")}
|
||||
onClick={() => {
|
||||
dropdownRef.current?.hide();
|
||||
setSimilarNotesShown(true);
|
||||
}}
|
||||
/>
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
@@ -224,6 +256,14 @@ function NoteInfoValue({ text, title, value }: { text: string; title?: string, v
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function SimilarNotesPane({ note, similarNotesShown }: NoteInfoContext) {
|
||||
return (similarNotesShown &&
|
||||
<div className="similar-notes-pane">
|
||||
<SimilarNotesTab note={note} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Backlinks
|
||||
@@ -233,7 +273,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) {
|
||||
<StatusBarDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
icon="bx bx-link"
|
||||
text={count}
|
||||
text={t("status_bar.backlinks", { count })}
|
||||
title={t("status_bar.backlinks_title", { count })}
|
||||
dropdownContainerClassName="backlinks-items"
|
||||
>
|
||||
@@ -252,7 +292,7 @@ function AttachmentCount({ note }: StatusBarContext) {
|
||||
<StatusBarButton
|
||||
className="attachment-count-button"
|
||||
icon="bx bx-paperclip"
|
||||
text={count}
|
||||
text={t("status_bar.attachments", { count })}
|
||||
title={t("status_bar.attachments_title", { count })}
|
||||
triggerCommand="showAttachments"
|
||||
/>
|
||||
@@ -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 &&
|
||||
<StatusBarDropdown
|
||||
title={t("status_bar.note_paths_title")}
|
||||
dropdownContainerClassName="dropdown-note-paths"
|
||||
icon="bx bx-directions"
|
||||
text={sortedNotePaths?.length}
|
||||
text={t("status_bar.note_paths", { count })}
|
||||
>
|
||||
<NotePathsWidget
|
||||
sortedNotePaths={sortedNotePaths}
|
||||
|
||||
20
apps/client/src/widgets/note_bars/CollectionProperties.css
Normal file
20
apps/client/src/widgets/note_bars/CollectionProperties.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import "./CollectionProperties.css";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
@@ -12,9 +17,6 @@ import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
||||
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> = {
|
||||
grid: "bx bxs-grid",
|
||||
@@ -30,12 +32,12 @@ export default function CollectionProperties({ note }: { note: FNote }) {
|
||||
const [ viewType, setViewType ] = useViewType(note);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="collection-properties">
|
||||
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
|
||||
<ViewOptions note={note} viewType={viewType} />
|
||||
<div className="spacer" />
|
||||
<HelpButton note={note} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,9 +189,9 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo
|
||||
{index < property.options.length - 1 && <FormDropdownDivider />}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return renderItem(option);
|
||||
}
|
||||
return renderItem(option);
|
||||
|
||||
})}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import Icon from "./Icon";
|
||||
|
||||
interface SimpleBadgeProps {
|
||||
className?: string;
|
||||
title: string;
|
||||
title: ComponentChildren;
|
||||
}
|
||||
|
||||
interface BadgeProps {
|
||||
|
||||
32
apps/client/src/widgets/react/Collapsible.css
Normal file
32
apps/client/src/widgets/react/Collapsible.css
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/client/src/widgets/react/Collapsible.tsx
Normal file
52
apps/client/src/widgets/react/Collapsible.tsx
Normal 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" />
|
||||
{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>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -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)} />
|
||||
</label>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,5 +51,27 @@ export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonPr
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { CommandNames } from "../../components/app_context";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
|
||||
|
||||
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
||||
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 { useStaticTooltip } from "./hooks";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface FormListOpts {
|
||||
children: ComponentChildren;
|
||||
@@ -33,7 +35,7 @@ export default function FormList({ children, onSelect, style, fullHeight, wrappe
|
||||
return () => {
|
||||
$wrapperRef.off("hide.bs.dropdown");
|
||||
dropdown.dispose();
|
||||
}
|
||||
};
|
||||
}, [ triggerRef, wrapperRef ]);
|
||||
|
||||
const builtinStyles = useMemo(() => {
|
||||
@@ -51,8 +53,7 @@ export default function FormList({ children, onSelect, style, fullHeight, wrappe
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button" style="display: none;"
|
||||
data-bs-toggle="dropdown" data-bs-display="static">
|
||||
</button>
|
||||
data-bs-toggle="dropdown" data-bs-display="static" />
|
||||
|
||||
<div class="dropdown-menu static show" style={{
|
||||
...style ?? {},
|
||||
@@ -199,7 +200,7 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
|
||||
<li>
|
||||
<h6 className="dropdown-header">{text}</h6>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function FormDropdownDivider() {
|
||||
@@ -216,21 +217,22 @@ export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdo
|
||||
const [ openOnMobile, setOpenOnMobile ] = useState(false);
|
||||
|
||||
return (
|
||||
<li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}>
|
||||
<span
|
||||
className="dropdown-toggle"
|
||||
onClick={(e) => {
|
||||
<li
|
||||
className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isMobile() && onDropdownToggleClicked) {
|
||||
onDropdownToggleClicked();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-toggle" onClick={(e) => {
|
||||
if (isMobile()) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isMobile()) {
|
||||
setOpenOnMobile(!openOnMobile);
|
||||
}
|
||||
|
||||
if (onDropdownToggleClicked) {
|
||||
onDropdownToggleClicked();
|
||||
}
|
||||
}}
|
||||
>
|
||||
setOpenOnMobile(!openOnMobile);
|
||||
}
|
||||
}}>
|
||||
<Icon icon={icon} />{" "}
|
||||
{title}
|
||||
</span>
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { ComponentChild } from "preact";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
|
||||
interface LinkButtonProps {
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
text: ComponentChild;
|
||||
triggerCommand?: CommandNames;
|
||||
}
|
||||
|
||||
export default function LinkButton({ onClick, text }: LinkButtonProps) {
|
||||
export default function LinkButton({ onClick, text, triggerCommand }: LinkButtonProps) {
|
||||
return (
|
||||
<a class="tn-link" href="javascript:" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}>
|
||||
<a class="tn-link" href="#"
|
||||
data-trigger-command={triggerCommand}
|
||||
role="button"
|
||||
onKeyDown={(e)=> {
|
||||
if (e.code === "Space") {
|
||||
onClick?.();
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick?.();
|
||||
}}>
|
||||
{text}
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { EditedNotesResponse } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function EditedNotesTab({ note }: TabContext) {
|
||||
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?.noteId ]);
|
||||
const editedNotes = useEditedNotes(note);
|
||||
|
||||
return (
|
||||
<div className="edited-notes-widget" style={{
|
||||
@@ -31,7 +23,7 @@ export default function EditedNotesTab({ note }: TabContext) {
|
||||
<div className="edited-notes-list use-tn-links">
|
||||
{joinElements(editedNotes.map(editedNote => {
|
||||
return (
|
||||
<span className="edited-note-line">
|
||||
<span key={editedNote.noteId} className="edited-note-line">
|
||||
{editedNote.isDeleted ? (
|
||||
<i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i>
|
||||
) : (
|
||||
@@ -40,12 +32,28 @@ export default function EditedNotesTab({ note }: TabContext) {
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}), " ")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</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;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import FNote from "../../entities/fnote";
|
||||
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 Button from "../react/Button";
|
||||
import { FormFileUploadButton } from "../react/FormFileUpload";
|
||||
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 }) {
|
||||
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
|
||||
@@ -54,19 +54,7 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
icon="bx bx-folder-open"
|
||||
text={t("file_properties.upload_new_revision")}
|
||||
disabled={!canAccessProtectedNote}
|
||||
onChange={(fileToUpload) => {
|
||||
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"));
|
||||
}
|
||||
});
|
||||
}}
|
||||
onChange={buildUploadNewFileRevisionListener(note)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
@@ -77,3 +65,19 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
<FormFileUploadButton
|
||||
text={t("image_properties.upload_new_revision")}
|
||||
icon="bx bx-folder-open"
|
||||
onChange={async (files) => {
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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 }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<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 && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
|
||||
</div>
|
||||
@@ -79,14 +92,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
||||
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
||||
{isNewLayout && <CommandItem command="toggleRibbonTabNoteMap" icon="bx bxs-network-chart" disabled={isInOptionsOrHelp} text={t("note_actions.note_map")} />}
|
||||
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
{isNewLayout && isNormalViewMode && !isHelpPage && <>
|
||||
<NoteBasicProperties note={note} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
|
||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||
@@ -98,7 +111,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
})} />
|
||||
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
|
||||
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
||||
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
<CommandItem command="showRevisions" icon="bx bx-history" text={t("note_actions.view_revisions")} />
|
||||
@@ -107,7 +120,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
<FormDropdownDivider />
|
||||
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment note={note} />}
|
||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")}
|
||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")}
|
||||
/>}
|
||||
|
||||
<FormDropdownSubmenu icon="bx bx-wrench" title={t("note_actions.advanced")} dropStart>
|
||||
@@ -122,7 +135,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
</FormDropdownSubmenu>
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
|
||||
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
|
||||
disabled={isInOptionsOrHelp}
|
||||
command={() => 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")}
|
||||
/>
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
|
||||
122
apps/client/src/widgets/ribbon/NoteActionsCustom.tsx
Normal file
122
apps/client/src/widgets/ribbon/NoteActionsCustom.tsx
Normal 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
|
||||
@@ -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<ReturnType<typeof useNoteMetadata>, "metadata">) {
|
||||
return <>
|
||||
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
|
||||
<Button
|
||||
className="calculate-button"
|
||||
icon="bx bx-calculator"
|
||||
<LinkButton
|
||||
text={t("note_info_widget.calculate")}
|
||||
onClick={requestSizeInfo}
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,13 @@ import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||
import Button from "../react/Button";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import LinkButton from "../react/LinkButton";
|
||||
import clsx from "clsx";
|
||||
|
||||
|
||||
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
@@ -35,9 +37,9 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
|
||||
)) : undefined}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
triggerCommand="cloneNoteIdsTo"
|
||||
<LinkButton
|
||||
text={t("note_paths.clone_button")}
|
||||
triggerCommand="cloneNoteIdsTo"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
@@ -108,12 +110,15 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
|
||||
return (
|
||||
<li class={classes}>
|
||||
{joinElements(fullNotePaths.map(notePath => (
|
||||
<NoteLink key={notePath} notePath={notePath} noPreview />
|
||||
{joinElements(fullNotePaths.map((notePath, index, arr) => (
|
||||
<NoteLink key={notePath}
|
||||
className={clsx({"basename": (index === arr.length - 1)})}
|
||||
notePath={notePath}
|
||||
noPreview />
|
||||
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||
|
||||
{icons.map(({ icon, title }) => (
|
||||
<span key={title} class={icon} title={title} />
|
||||
<i key={title} class={icon} title={title} />
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -5,9 +5,9 @@ import clsx from "clsx";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { EventNames } from "../../components/app_context";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||
import NoteActions from "./NoteActions";
|
||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||
|
||||
@@ -17,9 +17,7 @@ interface ComputedTab extends Indexed<TabConfiguration> {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
|
||||
export default function Ribbon() {
|
||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
||||
@@ -32,8 +30,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
||||
async function refresh() {
|
||||
const computedTabs: ComputedTab[] = [];
|
||||
for (const tab of TAB_CONFIGURATION) {
|
||||
const shouldAvoid = (isNewLayout && tab.avoidInNewLayout);
|
||||
const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext);
|
||||
const shouldShow = await shouldShowTab(tab.show, titleContext);
|
||||
computedTabs.push({
|
||||
...tab,
|
||||
shouldShow: !!shouldShow
|
||||
@@ -92,7 +89,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{children}
|
||||
<NoteActions />
|
||||
</div>
|
||||
|
||||
<div className="ribbon-body-container">
|
||||
@@ -115,7 +112,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
||||
noteContext={noteContext}
|
||||
componentId={componentId}
|
||||
activate={useCallback(() => {
|
||||
setActiveTabIndex(tab.index)
|
||||
setActiveTabIndex(tab.index);
|
||||
}, [setActiveTabIndex])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import BasicPropertiesTab from "./BasicPropertiesTab";
|
||||
@@ -18,8 +17,6 @@ import ScriptTab from "./ScriptTab";
|
||||
import SearchDefinitionTab from "./SearchDefinitionTab";
|
||||
import SimilarNotesTab from "./SimilarNotesTab";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
{
|
||||
title: t("classic_editor_toolbar.title"),
|
||||
@@ -30,15 +27,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||
content: FormattingToolbar,
|
||||
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
|
||||
stayInDom: !isNewLayout,
|
||||
avoidInNewLayout: true
|
||||
stayInDom: true
|
||||
},
|
||||
{
|
||||
title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
|
||||
icon: "bx bx-play",
|
||||
content: ScriptTab,
|
||||
activate: true,
|
||||
show: ({ note }) => note && !isNewLayout &&
|
||||
show: ({ note }) => note &&
|
||||
(note.isTriliumScript() || note.isTriliumSqlite()) &&
|
||||
(note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
|
||||
},
|
||||
@@ -60,14 +56,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
title: t("book_properties.book_properties"),
|
||||
icon: "bx bx-book",
|
||||
content: CollectionPropertiesTab,
|
||||
show: ({ note }) => !isNewLayout && note?.type === "book" || note?.type === "search",
|
||||
show: ({ note }) => (note?.type === "book" || note?.type === "search"),
|
||||
toggleCommand: "toggleRibbonTabBookProperties"
|
||||
},
|
||||
{
|
||||
title: t("note_properties.info"),
|
||||
icon: "bx bx-info-square",
|
||||
content: NotePropertiesTab,
|
||||
show: ({ note }) => !isNewLayout && !!note?.getLabelValue("pageUrl"),
|
||||
show: ({ note }) => !!note?.getLabelValue("pageUrl"),
|
||||
activate: true
|
||||
},
|
||||
{
|
||||
@@ -90,49 +86,49 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
title: t("basic_properties.basic_properties"),
|
||||
icon: "bx bx-slider",
|
||||
content: BasicPropertiesTab,
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabBasicProperties"
|
||||
},
|
||||
{
|
||||
title: t("owned_attribute_list.owned_attributes"),
|
||||
icon: "bx bx-list-check",
|
||||
content: OwnedAttributesTab,
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabOwnedAttributes",
|
||||
stayInDom: !isNewLayout
|
||||
stayInDom: true
|
||||
},
|
||||
{
|
||||
title: t("inherited_attribute_list.title"),
|
||||
icon: "bx bx-list-plus",
|
||||
content: InheritedAttributesTab,
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabInheritedAttributes"
|
||||
},
|
||||
{
|
||||
title: t("note_paths.title"),
|
||||
icon: "bx bx-collection",
|
||||
content: NotePathsTab,
|
||||
show: !isNewLayout,
|
||||
show: true,
|
||||
toggleCommand: "toggleRibbonTabNotePaths"
|
||||
},
|
||||
{
|
||||
title: t("note_map.title"),
|
||||
icon: "bx bxs-network-chart",
|
||||
content: NoteMapTab,
|
||||
show: !isNewLayout,
|
||||
show: true,
|
||||
toggleCommand: "toggleRibbonTabNoteMap"
|
||||
},
|
||||
{
|
||||
title: t("similar_notes.title"),
|
||||
icon: "bx bx-bar-chart",
|
||||
show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||
show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||
content: SimilarNotesTab,
|
||||
toggleCommand: "toggleRibbonTabSimilarNotes"
|
||||
},
|
||||
{
|
||||
title: t("note_info_widget.title"),
|
||||
icon: "bx bx-info-circle",
|
||||
show: ({ note }) => !isNewLayout && !!note,
|
||||
show: ({ note }) => !!note,
|
||||
content: NoteInfoTab,
|
||||
toggleCommand: "toggleRibbonTabNoteInfo"
|
||||
}
|
||||
|
||||
@@ -1,360 +1,361 @@
|
||||
import FormTextArea from "../react/FormTextArea";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
import FormSelect from "../react/FormSelect";
|
||||
import Icon from "../react/Icon";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
import { ComponentChildren, VNode } from "preact";
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { removeOwnedAttributesByNameOrType } from "../../services/attributes";
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
import appContext from "../../components/app_context";
|
||||
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 { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
|
||||
export interface SearchOption {
|
||||
attributeName: string;
|
||||
attributeType: "label" | "relation";
|
||||
icon: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
component: (props: SearchOptionProps) => VNode;
|
||||
defaultValue?: string;
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
||||
attributeName: string;
|
||||
attributeType: "label" | "relation";
|
||||
icon: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
component: (props: SearchOptionProps) => VNode;
|
||||
defaultValue?: string;
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
||||
}
|
||||
|
||||
interface SearchOptionProps {
|
||||
note: FNote;
|
||||
refreshResults: () => void;
|
||||
attributeName: string;
|
||||
attributeType: "label" | "relation";
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
||||
defaultValue?: string;
|
||||
error?: { message: string };
|
||||
note: FNote;
|
||||
refreshResults: () => void;
|
||||
attributeName: string;
|
||||
attributeType: "label" | "relation";
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
||||
defaultValue?: string;
|
||||
error?: { message: string };
|
||||
}
|
||||
|
||||
export const SEARCH_OPTIONS: SearchOption[] = [
|
||||
{
|
||||
attributeName: "searchString",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-text",
|
||||
label: t("search_definition.search_string"),
|
||||
component: SearchStringOption
|
||||
},
|
||||
{
|
||||
attributeName: "searchScript",
|
||||
attributeType: "relation",
|
||||
defaultValue: "root",
|
||||
icon: "bx bx-code",
|
||||
label: t("search_definition.search_script"),
|
||||
component: SearchScriptOption
|
||||
},
|
||||
{
|
||||
attributeName: "ancestor",
|
||||
attributeType: "relation",
|
||||
defaultValue: "root",
|
||||
icon: "bx bx-filter-alt",
|
||||
label: t("search_definition.ancestor"),
|
||||
component: AncestorOption,
|
||||
additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ]
|
||||
},
|
||||
{
|
||||
attributeName: "fastSearch",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-run",
|
||||
label: t("search_definition.fast_search"),
|
||||
tooltip: t("search_definition.fast_search_description"),
|
||||
component: FastSearchOption
|
||||
},
|
||||
{
|
||||
attributeName: "includeArchivedNotes",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-archive",
|
||||
label: t("search_definition.include_archived"),
|
||||
tooltip: t("search_definition.include_archived_notes_description"),
|
||||
component: IncludeArchivedNotesOption
|
||||
},
|
||||
{
|
||||
attributeName: "orderBy",
|
||||
attributeType: "label",
|
||||
defaultValue: "relevancy",
|
||||
icon: "bx bx-arrow-from-top",
|
||||
label: t("search_definition.order_by"),
|
||||
component: OrderByOption,
|
||||
additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ]
|
||||
},
|
||||
{
|
||||
attributeName: "limit",
|
||||
attributeType: "label",
|
||||
defaultValue: "10",
|
||||
icon: "bx bx-stop",
|
||||
label: t("search_definition.limit"),
|
||||
tooltip: t("search_definition.limit_description"),
|
||||
component: LimitOption
|
||||
},
|
||||
{
|
||||
attributeName: "debug",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-bug",
|
||||
label: t("search_definition.debug"),
|
||||
tooltip: t("search_definition.debug_description"),
|
||||
component: DebugOption
|
||||
}
|
||||
{
|
||||
attributeName: "searchString",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-text",
|
||||
label: t("search_definition.search_string"),
|
||||
component: SearchStringOption
|
||||
},
|
||||
{
|
||||
attributeName: "searchScript",
|
||||
attributeType: "relation",
|
||||
defaultValue: "root",
|
||||
icon: "bx bx-code",
|
||||
label: t("search_definition.search_script"),
|
||||
component: SearchScriptOption
|
||||
},
|
||||
{
|
||||
attributeName: "ancestor",
|
||||
attributeType: "relation",
|
||||
defaultValue: "root",
|
||||
icon: "bx bx-filter-alt",
|
||||
label: t("search_definition.ancestor"),
|
||||
component: AncestorOption,
|
||||
additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ]
|
||||
},
|
||||
{
|
||||
attributeName: "fastSearch",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-run",
|
||||
label: t("search_definition.fast_search"),
|
||||
tooltip: t("search_definition.fast_search_description"),
|
||||
component: FastSearchOption
|
||||
},
|
||||
{
|
||||
attributeName: "includeArchivedNotes",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-archive",
|
||||
label: t("search_definition.include_archived"),
|
||||
tooltip: t("search_definition.include_archived_notes_description"),
|
||||
component: IncludeArchivedNotesOption
|
||||
},
|
||||
{
|
||||
attributeName: "orderBy",
|
||||
attributeType: "label",
|
||||
defaultValue: "relevancy",
|
||||
icon: "bx bx-arrow-from-top",
|
||||
label: t("search_definition.order_by"),
|
||||
component: OrderByOption,
|
||||
additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ]
|
||||
},
|
||||
{
|
||||
attributeName: "limit",
|
||||
attributeType: "label",
|
||||
defaultValue: "10",
|
||||
icon: "bx bx-stop",
|
||||
label: t("search_definition.limit"),
|
||||
tooltip: t("search_definition.limit_description"),
|
||||
component: LimitOption
|
||||
},
|
||||
{
|
||||
attributeName: "debug",
|
||||
attributeType: "label",
|
||||
icon: "bx bx-bug",
|
||||
label: t("search_definition.debug"),
|
||||
tooltip: t("search_definition.debug_description"),
|
||||
component: DebugOption
|
||||
}
|
||||
];
|
||||
|
||||
function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: {
|
||||
note: FNote;
|
||||
title: string,
|
||||
titleIcon?: string,
|
||||
children?: ComponentChildren,
|
||||
help?: ComponentChildren,
|
||||
attributeName: string,
|
||||
attributeType: AttributeType,
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
|
||||
note: FNote;
|
||||
title: string,
|
||||
titleIcon?: string,
|
||||
children?: ComponentChildren,
|
||||
help?: ComponentChildren,
|
||||
attributeName: string,
|
||||
attributeType: AttributeType,
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
|
||||
}) {
|
||||
return (
|
||||
<tr className={attributeName}>
|
||||
<td className="title-column">
|
||||
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
|
||||
{title}
|
||||
</td>
|
||||
<td>{children}</td>
|
||||
<HelpRemoveButtons
|
||||
help={help}
|
||||
removeText={t("abstract_search_option.remove_this_search_option")}
|
||||
onRemove={() => {
|
||||
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
|
||||
if (additionalAttributesToDelete) {
|
||||
for (const { type, name } of additionalAttributesToDelete) {
|
||||
removeOwnedAttributesByNameOrType(note, type, name);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</tr>
|
||||
)
|
||||
return (
|
||||
<tr className={attributeName}>
|
||||
<td className="title-column">
|
||||
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
|
||||
{title}
|
||||
</td>
|
||||
<td>{children}</td>
|
||||
<HelpRemoveButtons
|
||||
help={help}
|
||||
removeText={t("abstract_search_option.remove_this_search_option")}
|
||||
onRemove={() => {
|
||||
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
|
||||
if (additionalAttributesToDelete) {
|
||||
for (const { type, name } of additionalAttributesToDelete) {
|
||||
removeOwnedAttributesByNameOrType(note, type, name);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) {
|
||||
const [ searchString, setSearchString ] = useNoteLabel(note, "searchString");
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const currentValue = useRef(searchString ?? "");
|
||||
const spacedUpdate = useSpacedUpdate(async () => {
|
||||
const searchString = currentValue.current;
|
||||
appContext.lastSearchString = searchString;
|
||||
setSearchString(searchString);
|
||||
const [ searchString, setSearchString ] = useNoteLabel(note, "searchString");
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const currentValue = useRef(searchString ?? "");
|
||||
const spacedUpdate = useSpacedUpdate(async () => {
|
||||
const searchString = currentValue.current;
|
||||
appContext.lastSearchString = searchString;
|
||||
setSearchString(searchString);
|
||||
|
||||
if (note.title.startsWith(t("search_string.search_prefix"))) {
|
||||
await server.put(`notes/${note.noteId}/title`, {
|
||||
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}`
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// React to errors
|
||||
const { showTooltip, hideTooltip } = useTooltip(inputRef, {
|
||||
trigger: "manual",
|
||||
title: `${t("search_string.error", { error: error?.message })}`,
|
||||
html: true,
|
||||
placement: "bottom"
|
||||
});
|
||||
|
||||
// Auto-focus.
|
||||
useEffect(() => inputRef.current?.focus(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
showTooltip();
|
||||
setTimeout(() => hideTooltip(), 4000);
|
||||
} else {
|
||||
hideTooltip();
|
||||
}
|
||||
}, [ error ]);
|
||||
|
||||
return <SearchOption
|
||||
title={t("search_string.title_column")}
|
||||
help={<>
|
||||
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
|
||||
<ul style="marigin-bottom: 0;">
|
||||
<li>{t("search_string.full_text_search")}</li>
|
||||
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
|
||||
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
|
||||
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
|
||||
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
|
||||
<li><code>#year <= 2000</code> - {t("search_string.label_year_comparison")}</li>
|
||||
<li><code>note.dateCreated >= MONTH-1</code> - {t("search_string.label_date_created")}</li>
|
||||
</ul>
|
||||
</>}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormTextArea
|
||||
inputRef={inputRef}
|
||||
className="search-string"
|
||||
placeholder={t("search_string.placeholder")}
|
||||
currentValue={searchString ?? ""}
|
||||
onChange={text => {
|
||||
currentValue.current = text;
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
// this also in effect disallows new lines in query string.
|
||||
// on one hand, this makes sense since search string is a label
|
||||
// on the other hand, it could be nice for structuring long search string. It's probably a niche case though.
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
refreshResults();
|
||||
if (note.title.startsWith(t("search_string.search_prefix"))) {
|
||||
await server.put(`notes/${note.noteId}/title`, {
|
||||
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SearchOption>
|
||||
}, 1000);
|
||||
|
||||
// React to errors
|
||||
const { showTooltip, hideTooltip } = useTooltip(inputRef, {
|
||||
trigger: "manual",
|
||||
title: `${t("search_string.error", { error: error?.message })}`,
|
||||
html: true,
|
||||
placement: "bottom"
|
||||
});
|
||||
|
||||
// Auto-focus.
|
||||
useEffect(() => inputRef.current?.focus(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
showTooltip();
|
||||
setTimeout(() => hideTooltip(), 4000);
|
||||
} else {
|
||||
hideTooltip();
|
||||
}
|
||||
}, [ error ]);
|
||||
|
||||
return <SearchOption
|
||||
title={t("search_string.title_column")}
|
||||
help={<>
|
||||
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
|
||||
<ul style="marigin-bottom: 0;">
|
||||
<li>{t("search_string.full_text_search")}</li>
|
||||
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
|
||||
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
|
||||
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
|
||||
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
|
||||
<li><code>#year <= 2000</code> - {t("search_string.label_year_comparison")}</li>
|
||||
<li><code>note.dateCreated >= MONTH-1</code> - {t("search_string.label_date_created")}</li>
|
||||
</ul>
|
||||
</>}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormTextArea
|
||||
inputRef={inputRef}
|
||||
className="search-string"
|
||||
placeholder={t("search_string.placeholder")}
|
||||
currentValue={searchString ?? ""}
|
||||
onChange={text => {
|
||||
currentValue.current = text;
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
// this also in effect disallows new lines in query string.
|
||||
// on one hand, this makes sense since search string is a label
|
||||
// on the other hand, it could be nice for structuring long search string. It's probably a niche case though.
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
refreshResults();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SearchOption>;
|
||||
}
|
||||
|
||||
function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
|
||||
const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript");
|
||||
const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript");
|
||||
|
||||
return <SearchOption
|
||||
title={t("search_script.title")}
|
||||
help={<>
|
||||
<p>{t("search_script.description1")}</p>
|
||||
<p>{t("search_script.description2")}</p>
|
||||
<p>{t("search_script.example_title")}</p>
|
||||
<pre>{t("search_script.example_code")}</pre>
|
||||
{t("search_script.note")}
|
||||
</>}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<NoteAutocomplete
|
||||
noteId={searchScript !== "root" ? searchScript ?? undefined : undefined}
|
||||
noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
|
||||
placeholder={t("search_script.placeholder")}
|
||||
/>
|
||||
</SearchOption>
|
||||
return <SearchOption
|
||||
title={t("search_script.title")}
|
||||
help={<>
|
||||
<p>{t("search_script.description1")}</p>
|
||||
<p>{t("search_script.description2")}</p>
|
||||
<p>{t("search_script.example_title")}</p>
|
||||
<pre>{t("search_script.example_code")}</pre>
|
||||
{t("search_script.note")}
|
||||
</>}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<NoteAutocomplete
|
||||
noteId={searchScript !== "root" ? searchScript ?? undefined : undefined}
|
||||
noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
|
||||
placeholder={t("search_script.placeholder")}
|
||||
/>
|
||||
</SearchOption>;
|
||||
}
|
||||
|
||||
function AncestorOption({ note, ...restProps}: SearchOptionProps) {
|
||||
const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor");
|
||||
const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth");
|
||||
const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor");
|
||||
const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth");
|
||||
|
||||
const options = useMemo(() => {
|
||||
const options: { value: string | undefined; label: string }[] = [
|
||||
{ value: "", label: t("ancestor.depth_doesnt_matter") },
|
||||
{ value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` }
|
||||
];
|
||||
const options = useMemo(() => {
|
||||
const options: { value: string | undefined; label: string }[] = [
|
||||
{ value: "", label: t("ancestor.depth_doesnt_matter") },
|
||||
{ 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=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: `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=2; i<=9; i++) options.push({ value: `lt${ i}`, label: t("ancestor.depth_lt", { count: i }) });
|
||||
|
||||
return options;
|
||||
}, []);
|
||||
return options;
|
||||
}, []);
|
||||
|
||||
return <SearchOption
|
||||
title={t("ancestor.label")}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<NoteAutocomplete
|
||||
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
|
||||
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
|
||||
placeholder={t("ancestor.placeholder")}
|
||||
/>
|
||||
return <SearchOption
|
||||
title={t("ancestor.label")}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<NoteAutocomplete
|
||||
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
|
||||
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
|
||||
placeholder={t("ancestor.placeholder")}
|
||||
/>
|
||||
|
||||
<div style="margin-inline-start: 10px; margin-inline-end: 10px">{t("ancestor.depth_label")}:</div>
|
||||
<FormSelect
|
||||
values={options}
|
||||
keyProperty="value" titleProperty="label"
|
||||
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
|
||||
style={{ flexShrink: 3 }}
|
||||
/>
|
||||
</div>
|
||||
</SearchOption>;
|
||||
<div style="margin-inline-start: 10px; margin-inline-end: 10px">{t("ancestor.depth_label")}:</div>
|
||||
<FormSelect
|
||||
values={options}
|
||||
keyProperty="value" titleProperty="label"
|
||||
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
|
||||
style={{ flexShrink: 3 }}
|
||||
/>
|
||||
</div>
|
||||
</SearchOption>;
|
||||
}
|
||||
|
||||
function FastSearchOption({ ...restProps }: SearchOptionProps) {
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-run" title={t("fast_search.fast_search")}
|
||||
help={t("fast_search.description")}
|
||||
{...restProps}
|
||||
/>
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-run" title={t("fast_search.fast_search")}
|
||||
help={t("fast_search.description")}
|
||||
{...restProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
function DebugOption({ ...restProps }: SearchOptionProps) {
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-bug" title={t("debug.debug")}
|
||||
help={<>
|
||||
<p>{t("debug.debug_info")}</p>
|
||||
{t("debug.access_info")}
|
||||
</>}
|
||||
{...restProps}
|
||||
/>
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-bug" title={t("debug.debug")}
|
||||
help={<>
|
||||
<p>{t("debug.debug_info")}</p>
|
||||
{t("debug.access_info")}
|
||||
</>}
|
||||
{...restProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) {
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
|
||||
{...restProps}
|
||||
/>
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
|
||||
{...restProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
function OrderByOption({ note, ...restProps }: SearchOptionProps) {
|
||||
const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy");
|
||||
const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection");
|
||||
const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy");
|
||||
const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection");
|
||||
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-arrow-from-top"
|
||||
title={t("order_by.order_by")}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormSelect
|
||||
className="w-auto d-inline"
|
||||
currentValue={orderBy ?? "relevancy"} onChange={setOrderBy}
|
||||
keyProperty="value" titleProperty="title"
|
||||
values={[
|
||||
{ value: "relevancy", title: t("order_by.relevancy") },
|
||||
{ value: "title", title: t("order_by.title") },
|
||||
{ value: "dateCreated", title: t("order_by.date_created") },
|
||||
{ value: "dateModified", title: t("order_by.date_modified") },
|
||||
{ value: "contentSize", title: t("order_by.content_size") },
|
||||
{ value: "contentAndAttachmentsSize", title: t("order_by.content_and_attachments_size") },
|
||||
{ value: "contentAndAttachmentsAndRevisionsSize", title: t("order_by.content_and_attachments_and_revisions_size") },
|
||||
{ value: "revisionCount", title: t("order_by.revision_count") },
|
||||
{ value: "childrenCount", title: t("order_by.children_count") },
|
||||
{ value: "parentCount", title: t("order_by.parent_count") },
|
||||
{ value: "ownedLabelCount", title: t("order_by.owned_label_count") },
|
||||
{ value: "ownedRelationCount", title: t("order_by.owned_relation_count") },
|
||||
{ value: "targetRelationCount", title: t("order_by.target_relation_count") },
|
||||
{ value: "random", title: t("order_by.random") }
|
||||
]}
|
||||
/>
|
||||
{" "}
|
||||
<FormSelect
|
||||
className="w-auto d-inline"
|
||||
currentValue={orderDirection ?? "asc"} onChange={setOrderDirection}
|
||||
keyProperty="value" titleProperty="title"
|
||||
values={[
|
||||
{ value: "asc", title: t("order_by.asc") },
|
||||
{ value: "desc", title: t("order_by.desc") }
|
||||
]}
|
||||
/>
|
||||
</SearchOption>
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-arrow-from-top"
|
||||
title={t("order_by.order_by")}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormSelect
|
||||
className="w-auto d-inline"
|
||||
currentValue={orderBy ?? "relevancy"} onChange={setOrderBy}
|
||||
keyProperty="value" titleProperty="title"
|
||||
values={[
|
||||
{ value: "relevancy", title: t("order_by.relevancy") },
|
||||
{ value: "title", title: t("order_by.title") },
|
||||
{ value: "dateCreated", title: t("order_by.date_created") },
|
||||
{ value: "dateModified", title: t("order_by.date_modified") },
|
||||
{ value: "contentSize", title: t("order_by.content_size") },
|
||||
{ value: "contentAndAttachmentsSize", title: t("order_by.content_and_attachments_size") },
|
||||
{ value: "contentAndAttachmentsAndRevisionsSize", title: t("order_by.content_and_attachments_and_revisions_size") },
|
||||
{ value: "revisionCount", title: t("order_by.revision_count") },
|
||||
{ value: "childrenCount", title: t("order_by.children_count") },
|
||||
{ value: "parentCount", title: t("order_by.parent_count") },
|
||||
{ value: "ownedLabelCount", title: t("order_by.owned_label_count") },
|
||||
{ value: "ownedRelationCount", title: t("order_by.owned_relation_count") },
|
||||
{ value: "targetRelationCount", title: t("order_by.target_relation_count") },
|
||||
{ value: "random", title: t("order_by.random") }
|
||||
]}
|
||||
/>
|
||||
{" "}
|
||||
<FormSelect
|
||||
className="w-auto d-inline"
|
||||
currentValue={orderDirection ?? "asc"} onChange={setOrderDirection}
|
||||
keyProperty="value" titleProperty="title"
|
||||
values={[
|
||||
{ value: "asc", title: t("order_by.asc") },
|
||||
{ value: "desc", title: t("order_by.desc") }
|
||||
]}
|
||||
/>
|
||||
</SearchOption>;
|
||||
}
|
||||
|
||||
function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) {
|
||||
const [ limit, setLimit ] = useNoteLabel(note, "limit");
|
||||
const [ limit, setLimit ] = useNoteLabel(note, "limit");
|
||||
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-stop"
|
||||
title={t("limit.limit")}
|
||||
help={t("limit.take_first_x_results")}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormTextBox
|
||||
type="number" min="1" step="1"
|
||||
currentValue={limit ?? defaultValue} onChange={setLimit}
|
||||
/>
|
||||
</SearchOption>
|
||||
return <SearchOption
|
||||
titleIcon="bx bx-stop"
|
||||
title={t("limit.limit")}
|
||||
help={t("limit.take_first_x_results")}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormTextBox
|
||||
type="number" min="1" step="1"
|
||||
currentValue={limit ?? defaultValue} onChange={setLimit}
|
||||
/>
|
||||
</SearchOption>;
|
||||
}
|
||||
|
||||
@@ -1,207 +1,218 @@
|
||||
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";
|
||||
|
||||
export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
|
||||
const [ error, setError ] = useState<{ message: string }>();
|
||||
import { SaveSearchNoteResponse } from "@triliumnext/commons";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
function refreshOptions() {
|
||||
if (!note) return;
|
||||
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 availableOptions: SearchOption[] = [];
|
||||
const activeOptions: SearchOption[] = [];
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
for (const searchOption of SEARCH_OPTIONS) {
|
||||
const attr = note.getAttribute(searchOption.attributeType, searchOption.attributeName);
|
||||
if (attr) {
|
||||
activeOptions.push(searchOption);
|
||||
} else {
|
||||
availableOptions.push(searchOption);
|
||||
}
|
||||
}
|
||||
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
|
||||
const [ error, setError ] = useState<{ message: string }>();
|
||||
|
||||
setSearchOptions({ availableOptions, activeOptions });
|
||||
}
|
||||
function refreshOptions() {
|
||||
if (!note) return;
|
||||
|
||||
async function refreshResults() {
|
||||
const noteId = note?.noteId;
|
||||
if (!noteId) {
|
||||
return;
|
||||
}
|
||||
const availableOptions: SearchOption[] = [];
|
||||
const activeOptions: SearchOption[] = [];
|
||||
|
||||
try {
|
||||
const result = await froca.loadSearchNote(noteId);
|
||||
if (result?.error) {
|
||||
setError({ message: result?.error})
|
||||
} else {
|
||||
setError(undefined);
|
||||
for (const searchOption of SEARCH_OPTIONS) {
|
||||
const attr = note.getAttribute(searchOption.attributeType, searchOption.attributeName);
|
||||
if (attr) {
|
||||
activeOptions.push(searchOption);
|
||||
} else {
|
||||
availableOptions.push(searchOption);
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
toast.showError(getErrorMessage(e));
|
||||
|
||||
setSearchOptions({ availableOptions, activeOptions });
|
||||
}
|
||||
|
||||
parentComponent?.triggerEvent("searchRefreshed", { ntxId });
|
||||
}
|
||||
|
||||
// Refresh the list of available and active options.
|
||||
useEffect(refreshOptions, [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
|
||||
refreshOptions();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="search-definition-widget">
|
||||
<div className="search-settings">
|
||||
{note && !hidden &&
|
||||
<table className="search-setting-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="title-column">{t("search_definition.add_search_option")}</td>
|
||||
<td colSpan={2} className="add-search-option">
|
||||
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||
<Button
|
||||
size="small"
|
||||
icon={icon}
|
||||
text={label}
|
||||
title={tooltip}
|
||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AddBulkActionButton note={note} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody className="search-options">
|
||||
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
|
||||
const Component = component;
|
||||
return <Component
|
||||
attributeName={attributeName}
|
||||
attributeType={attributeType}
|
||||
note={note}
|
||||
refreshResults={refreshResults}
|
||||
error={error}
|
||||
additionalAttributesToDelete={additionalAttributesToDelete}
|
||||
defaultValue={defaultValue}
|
||||
/>;
|
||||
})}
|
||||
</tbody>
|
||||
<BulkActionsList note={note} />
|
||||
<tbody className="search-actions">
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<div className="search-actions-container">
|
||||
<Button
|
||||
icon="bx bx-search"
|
||||
text={t("search_definition.search_button")}
|
||||
keyboardShortcut="Enter"
|
||||
onClick={refreshResults}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon="bx bxs-zap"
|
||||
text={t("search_definition.search_execute")}
|
||||
onClick={async () => {
|
||||
await server.post(`search-and-execute-note/${note.noteId}`);
|
||||
refreshResults();
|
||||
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
||||
}}
|
||||
/>
|
||||
|
||||
{note.isHiddenCompletely() && <Button
|
||||
icon="bx bx-save"
|
||||
text={t("search_definition.save_to_note")}
|
||||
onClick={async () => {
|
||||
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
|
||||
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
||||
// See https://www.i18next.com/translation-function/interpolation#unescape
|
||||
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
async function refreshResults() {
|
||||
const noteId = note?.noteId;
|
||||
if (!noteId) {
|
||||
return;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
try {
|
||||
const result = await froca.loadSearchNote(noteId);
|
||||
if (result?.error) {
|
||||
setError({ message: result?.error});
|
||||
} else {
|
||||
setError(undefined);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
toast.showError(getErrorMessage(e));
|
||||
}
|
||||
|
||||
parentComponent?.triggerEvent("searchRefreshed", { ntxId });
|
||||
}
|
||||
|
||||
// Refresh the list of available and active options.
|
||||
useEffect(refreshOptions, [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
|
||||
refreshOptions();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="search-definition-widget">
|
||||
<div className="search-settings">
|
||||
{note && !hidden && (
|
||||
<table className="search-setting-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="title-column">{t("search_definition.add_search_option")}</td>
|
||||
<td colSpan={2} className="add-search-option">
|
||||
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||
<Button
|
||||
size="small"
|
||||
icon={icon}
|
||||
text={label}
|
||||
title={tooltip}
|
||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AddBulkActionButton note={note} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody className="search-options">
|
||||
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
|
||||
const Component = component;
|
||||
return <Component
|
||||
attributeName={attributeName}
|
||||
attributeType={attributeType}
|
||||
note={note}
|
||||
refreshResults={refreshResults}
|
||||
error={error}
|
||||
additionalAttributesToDelete={additionalAttributesToDelete}
|
||||
defaultValue={defaultValue}
|
||||
/>;
|
||||
})}
|
||||
|
||||
{isNewLayout && <tr className="view-options">
|
||||
<td className="title-column">{t("search_definition.view_options")}</td>
|
||||
<td><CollectionProperties note={note} /></td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
<BulkActionsList note={note} />
|
||||
<tbody className="search-actions">
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<div className="search-actions-container">
|
||||
<Button
|
||||
icon="bx bx-search"
|
||||
text={t("search_definition.search_button")}
|
||||
keyboardShortcut="Enter"
|
||||
onClick={refreshResults}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon="bx bxs-zap"
|
||||
text={t("search_definition.search_execute")}
|
||||
onClick={async () => {
|
||||
await server.post(`search-and-execute-note/${note.noteId}`);
|
||||
refreshResults();
|
||||
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
||||
}}
|
||||
/>
|
||||
|
||||
{note.isHiddenCompletely() && <Button
|
||||
icon="bx bx-save"
|
||||
text={t("search_definition.save_to_note")}
|
||||
onClick={async () => {
|
||||
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
|
||||
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
||||
// See https://www.i18next.com/translation-function/interpolation#unescape
|
||||
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BulkActionsList({ note }: { note: FNote }) {
|
||||
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
|
||||
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
|
||||
|
||||
function refreshBulkActions() {
|
||||
if (note) {
|
||||
setBulkActions(bulk_action.parseActions(note));
|
||||
function refreshBulkActions() {
|
||||
if (note) {
|
||||
setBulkActions(bulk_action.parseActions(note));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// React to changes.
|
||||
useEffect(refreshBulkActions, [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) {
|
||||
refreshBulkActions();
|
||||
}
|
||||
});
|
||||
// React to changes.
|
||||
useEffect(refreshBulkActions, [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) {
|
||||
refreshBulkActions();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<tbody className="action-options">
|
||||
{bulkActions?.map(bulkAction => (
|
||||
bulkAction.doRender()
|
||||
))}
|
||||
</tbody>
|
||||
)
|
||||
return (
|
||||
<tbody className="action-options">
|
||||
{bulkActions?.map(bulkAction => (
|
||||
bulkAction.doRender()
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
function AddBulkActionButton({ note }: { note: FNote }) {
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName="action-add-toggle btn btn-sm"
|
||||
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
|
||||
noSelectButtonStyle
|
||||
>
|
||||
{ACTION_GROUPS.map(({ actions, title }) => (
|
||||
<>
|
||||
<FormListHeader text={title} />
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName="action-add-toggle btn btn-sm"
|
||||
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
|
||||
noSelectButtonStyle
|
||||
>
|
||||
{ACTION_GROUPS.map(({ actions, title }) => (
|
||||
<>
|
||||
<FormListHeader text={title} />
|
||||
|
||||
{actions.map(({ actionName, actionTitle }) => (
|
||||
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
|
||||
{actions.map(({ actionName, actionTitle }) => (
|
||||
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</Dropdown>
|
||||
)
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { SimilarNoteResponse } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
import { t } from "../../services/i18n";
|
||||
import froca from "../../services/froca";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
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>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,7 +18,7 @@ export default function SimilarNotesTab({ note }: TabContext) {
|
||||
await froca.getNotes(noteIds, true); // preload all at once
|
||||
}
|
||||
setSimilarNotes(similarNotes);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}, [ note?.noteId ]);
|
||||
@@ -42,5 +43,5 @@ export default function SimilarNotesTab({ note }: TabContext) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { VNode } from "preact";
|
||||
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { VNode } from "preact";
|
||||
|
||||
export interface TabContext {
|
||||
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.
|
||||
*/
|
||||
stayInDom?: boolean;
|
||||
avoidInNewLayout?: boolean;
|
||||
}
|
||||
|
||||
@@ -207,9 +207,6 @@ body.experimental-feature-new-layout .classic-toolbar-widget {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.note-info-widget .calculate-button {
|
||||
padding: 0 10px;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Similar Notes */
|
||||
@@ -448,8 +445,22 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
|
||||
.ribbon-button-container {
|
||||
--button-gap: 5px;
|
||||
|
||||
border-bottom: 0 !important;
|
||||
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 */
|
||||
|
||||
@@ -18,6 +18,7 @@ type Labels = {
|
||||
language: string;
|
||||
originalFileName: string;
|
||||
pageUrl: string;
|
||||
dateNote: string;
|
||||
|
||||
// Search
|
||||
searchString: string;
|
||||
|
||||
Reference in New Issue
Block a user