feat(layout): move the note actions into the breadcrumb area

This commit is contained in:
Elian Doran 2025-12-09 20:04:28 +02:00
parent e58d6bf2a3
commit fe7ca210dd
No known key found for this signature in database
4 changed files with 134 additions and 133 deletions

View File

@ -47,6 +47,7 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import Breadcrumb from "../widgets/Breadcrumb.jsx"; import Breadcrumb from "../widgets/Breadcrumb.jsx";
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx"; import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
import { experimentalFeatures, isExperimentalFeatureEnabled } from "../services/experimental_features.js"; import { experimentalFeatures, isExperimentalFeatureEnabled } from "../services/experimental_features.js";
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
export default class DesktopLayout { export default class DesktopLayout {
@ -139,13 +140,14 @@ export default class DesktopLayout {
.child(<MovePaneButton direction="right" />) .child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />) .child(<ClosePaneButton />)
.child(<CreatePaneButton />) .child(<CreatePaneButton />)
.optChild(isNewLayout, <NoteActions />)
) )
.child(new FlexContainer("row") .child(new FlexContainer("row")
.class("title-row") .class("title-row")
.child(<NoteIconWidget />) .child(<NoteIconWidget />)
.child(<NoteTitleWidget />) .child(<NoteTitleWidget />)
) )
.optChild(!isNewLayout, <Ribbon />) .optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
.child(new WatchedFileUpdateStatusWidget()) .child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />) .child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child( .child(

View File

@ -1,115 +1,111 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons"; import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import { ParentComponent } from "../react/react_utils";
import { t } from "../../services/i18n"
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks";
import { useTriliumOption } from "../react/hooks";
import ActionButton from "../react/ActionButton"
import appContext, { CommandNames } from "../../components/app_context"; import appContext, { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import branches from "../../services/branches"; import branches from "../../services/branches";
import dialog from "../../services/dialog"; import dialog from "../../services/dialog";
import Dropdown from "../react/Dropdown"; import { t } from "../../services/i18n";
import FNote from "../../entities/fnote"
import NoteContext from "../../components/note_context";
import server from "../../services/server"; import server from "../../services/server";
import toast from "../../services/toast"; import toast from "../../services/toast";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import ws from "../../services/ws"; import ws from "../../services/ws";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteProperty, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
interface NoteActionsProps { export default function NoteActions() {
note?: FNote; const { note, noteContext } = useNoteContext();
noteContext?: NoteContext; return (
} <div className="ribbon-button-container" style={{ contain: "none" }}>
{note && <RevisionsButton note={note} />}
export default function NoteActions({ note, noteContext }: NoteActionsProps) { {note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
return ( </div>
<> );
{note && <RevisionsButton note={note} />}
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
</>
);
} }
function RevisionsButton({ note }: { note: FNote }) { function RevisionsButton({ note }: { note: FNote }) {
const isEnabled = !["launcher", "doc"].includes(note?.type ?? ""); const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
return (isEnabled && return (isEnabled &&
<ActionButton <ActionButton
icon="bx bx-history" icon="bx bx-history"
text={t("revisions_button.note_revisions")} text={t("revisions_button.note_revisions")}
triggerCommand="showRevisions" triggerCommand="showRevisions"
titlePosition="bottom" titlePosition="bottom"
/> />
); );
} }
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
const noteType = useNoteProperty(note, "type") ?? ""; const noteType = useNoteProperty(note, "type") ?? "";
const [ viewType ] = useNoteLabel(note, "viewType"); const [viewType] = useNoteLabel(note, "viewType");
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help"); const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? "")); const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
const isElectron = getIsElectron(); const isElectron = getIsElectron();
const isMac = getIsMac(); const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType); const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType); const isSearchOrBook = ["search", "book"].includes(noteType);
const [ syncServerHost ] = useTriliumOption("syncServerHost"); const [syncServerHost] = useTriliumOption("syncServerHost");
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
return ( return (
<Dropdown <Dropdown
buttonClassName="bx bx-dots-vertical-rounded" buttonClassName="bx bx-dots-vertical-rounded"
className="note-actions" className="note-actions"
hideToggleArrow hideToggleArrow
noSelectButtonStyle noSelectButtonStyle
iconAction> iconAction>
{isReadOnly && <> {isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")} <CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
command={() => enableEditing()} /> command={() => enableEditing()} />
<FormDropdownDivider /> <FormDropdownDivider />
</>} </>}
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> } {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")} />}
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} /> <CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} /> <CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />} {isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")} <CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"} disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")} <CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"} disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath, notePath: noteContext.notePath,
defaultType: "single" defaultType: "single"
})} /> })} />
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} /> <CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} /> <CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} /> <CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
{(syncServerHost && isElectron) && {(syncServerHost && isElectron) &&
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} /> <CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
} }
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} /> <CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive <CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
disabled={isInOptionsOrHelp} disabled={isInOptionsOrHelp}
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])} command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
/> />
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} /> <CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />} {glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
</Dropdown> </Dropdown>
); );
} }
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
@ -129,46 +125,46 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?:
throw new Error("Editor crashed."); throw new Error("Editor crashed.");
}); });
}); });
}}>Crash editor</FormListItem>)} }}>Crash editor</FormListItem>)}
</> </>
) );
} }
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) { function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
return <FormListItem return <FormListItem
icon={icon} icon={icon}
title={title} title={title}
triggerCommand={typeof command === "string" ? command : undefined} triggerCommand={typeof command === "string" ? command : undefined}
onClick={typeof command === "function" ? command : undefined} onClick={typeof command === "function" ? command : undefined}
disabled={disabled} disabled={disabled}
>{text}</FormListItem> >{text}</FormListItem>;
} }
function ConvertToAttachment({ note }: { note: FNote }) { function ConvertToAttachment({ note }: { note: FNote }) {
return ( return (
<FormListItem <FormListItem
icon="bx bx-paperclip" icon="bx bx-paperclip"
onClick={async () => { onClick={async () => {
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) { if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
return; return;
} }
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`); const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
if (!newAttachment) { if (!newAttachment) {
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title })); toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
return; return;
} }
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, { await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: { viewScope: {
viewMode: "attachments", viewMode: "attachments",
attachmentId: newAttachment.attachmentId attachmentId: newAttachment.attachmentId
} }
}); });
}} }}
>{t("note_actions.convert_into_attachment")}</FormListItem> >{t("note_actions.convert_into_attachment")}</FormListItem>
) );
} }

View File

@ -16,7 +16,7 @@ interface ComputedTab extends Indexed<TabConfiguration> {
shouldShow: boolean; shouldShow: boolean;
} }
export default function Ribbon() { export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext(); const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
const noteType = useNoteProperty(note, "type"); const noteType = useNoteProperty(note, "type");
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>(); const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
@ -99,9 +99,7 @@ export default function Ribbon() {
/> />
))} ))}
</div> </div>
<div className="ribbon-button-container"> {children}
{ note && <NoteActions note={note} noteContext={noteContext} /> }
</div>
</div> </div>
<div className="ribbon-body-container"> <div className="ribbon-body-container">

View File

@ -417,14 +417,19 @@ body[dir=rtl] .attribute-list-editor {
/* #endregion */ /* #endregion */
/* #region Experimental layout */ /* #region Experimental layout */
body.experimental-feature-new-layout .ribbon-container { body.experimental-feature-new-layout {
display: flex; .ribbon-container {
flex-direction: column-reverse; display: flex;
border-top: 1px solid var(--main-border-color); flex-direction: column-reverse;
border-top: 1px solid var(--main-border-color);
.ribbon-tab-spacer, .ribbon-tab-spacer,
.ribbon-button-container, .ribbon-body {
.ribbon-body { border-bottom: 0 !important;
}
}
.ribbon-button-container {
border-bottom: 0 !important; border-bottom: 0 !important;
} }
} }