mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 03:53:37 -06:00
feat(layout): move the note actions into the breadcrumb area
This commit is contained in:
parent
e58d6bf2a3
commit
fe7ca210dd
@ -47,6 +47,7 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||
import { experimentalFeatures, isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -139,13 +140,14 @@ export default class DesktopLayout {
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
.child(<CreatePaneButton />)
|
||||
.optChild(isNewLayout, <NoteActions />)
|
||||
)
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
)
|
||||
.optChild(!isNewLayout, <Ribbon />)
|
||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
|
||||
@ -1,115 +1,111 @@
|
||||
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 { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks";
|
||||
import { useTriliumOption } from "../react/hooks";
|
||||
import ActionButton from "../react/ActionButton"
|
||||
|
||||
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 Dropdown from "../react/Dropdown";
|
||||
import FNote from "../../entities/fnote"
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { t } from "../../services/i18n";
|
||||
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 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 {
|
||||
note?: FNote;
|
||||
noteContext?: NoteContext;
|
||||
}
|
||||
|
||||
export default function NoteActions({ note, noteContext }: NoteActionsProps) {
|
||||
return (
|
||||
<>
|
||||
{note && <RevisionsButton note={note} />}
|
||||
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
|
||||
</>
|
||||
);
|
||||
export default function NoteActions() {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
return (
|
||||
<div className="ribbon-button-container" style={{ contain: "none" }}>
|
||||
{note && <RevisionsButton note={note} />}
|
||||
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionsButton({ note }: { note: FNote }) {
|
||||
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
||||
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
||||
|
||||
return (isEnabled &&
|
||||
<ActionButton
|
||||
icon="bx bx-history"
|
||||
text={t("revisions_button.note_revisions")}
|
||||
triggerCommand="showRevisions"
|
||||
titlePosition="bottom"
|
||||
/>
|
||||
);
|
||||
return (isEnabled &&
|
||||
<ActionButton
|
||||
icon="bx bx-history"
|
||||
text={t("revisions_button.note_revisions")}
|
||||
triggerCommand="showRevisions"
|
||||
titlePosition="bottom"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
|
||||
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
||||
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
|
||||
const isElectron = getIsElectron();
|
||||
const isMac = getIsMac();
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
|
||||
const isSearchOrBook = ["search", "book"].includes(noteType);
|
||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
const [viewType] = useNoteLabel(note, "viewType");
|
||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
|
||||
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
||||
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
|
||||
const isElectron = getIsElectron();
|
||||
const isMac = getIsMac();
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
|
||||
const isSearchOrBook = ["search", "book"].includes(noteType);
|
||||
const [syncServerHost] = useTriliumOption("syncServerHost");
|
||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-dots-vertical-rounded"
|
||||
className="note-actions"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
iconAction>
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-dots-vertical-rounded"
|
||||
className="note-actions"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
iconAction>
|
||||
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
command={() => enableEditing()} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
command={() => enableEditing()} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment note={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="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")} />}
|
||||
<FormDropdownDivider />
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment note={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="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")} />}
|
||||
<FormDropdownDivider />
|
||||
|
||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
||||
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
|
||||
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
||||
notePath: noteContext.notePath,
|
||||
defaultType: "single"
|
||||
})} />
|
||||
<FormDropdownDivider />
|
||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
||||
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
|
||||
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
||||
notePath: noteContext.notePath,
|
||||
defaultType: "single"
|
||||
})} />
|
||||
<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="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")} />
|
||||
{(syncServerHost && isElectron) &&
|
||||
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
|
||||
}
|
||||
<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="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")} />
|
||||
{(syncServerHost && isElectron) &&
|
||||
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
|
||||
}
|
||||
<FormDropdownDivider />
|
||||
|
||||
<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
|
||||
disabled={isInOptionsOrHelp}
|
||||
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
|
||||
/>
|
||||
<FormDropdownDivider />
|
||||
<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
|
||||
disabled={isInOptionsOrHelp}
|
||||
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
|
||||
/>
|
||||
<FormDropdownDivider />
|
||||
|
||||
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
||||
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
|
||||
</Dropdown>
|
||||
);
|
||||
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
||||
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
@ -129,46 +125,46 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?:
|
||||
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 }) {
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
triggerCommand={typeof command === "string" ? command : undefined}
|
||||
onClick={typeof command === "function" ? command : undefined}
|
||||
disabled={disabled}
|
||||
>{text}</FormListItem>
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
triggerCommand={typeof command === "string" ? command : undefined}
|
||||
onClick={typeof command === "function" ? command : undefined}
|
||||
disabled={disabled}
|
||||
>{text}</FormListItem>;
|
||||
}
|
||||
|
||||
function ConvertToAttachment({ note }: { note: FNote }) {
|
||||
return (
|
||||
<FormListItem
|
||||
icon="bx bx-paperclip"
|
||||
onClick={async () => {
|
||||
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<FormListItem
|
||||
icon="bx bx-paperclip"
|
||||
onClick={async () => {
|
||||
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
|
||||
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) {
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
|
||||
return;
|
||||
}
|
||||
if (!newAttachment) {
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
||||
viewScope: {
|
||||
viewMode: "attachments",
|
||||
attachmentId: newAttachment.attachmentId
|
||||
}
|
||||
});
|
||||
}}
|
||||
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
||||
)
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
||||
viewScope: {
|
||||
viewMode: "attachments",
|
||||
attachmentId: newAttachment.attachmentId
|
||||
}
|
||||
});
|
||||
}}
|
||||
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ interface ComputedTab extends Indexed<TabConfiguration> {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
export default function Ribbon() {
|
||||
export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
|
||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
||||
@ -99,9 +99,7 @@ export default function Ribbon() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="ribbon-button-container">
|
||||
{ note && <NoteActions note={note} noteContext={noteContext} /> }
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="ribbon-body-container">
|
||||
|
||||
@ -417,14 +417,19 @@ body[dir=rtl] .attribute-list-editor {
|
||||
/* #endregion */
|
||||
|
||||
/* #region Experimental layout */
|
||||
body.experimental-feature-new-layout .ribbon-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
body.experimental-feature-new-layout {
|
||||
.ribbon-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
|
||||
.ribbon-tab-spacer,
|
||||
.ribbon-button-container,
|
||||
.ribbon-body {
|
||||
.ribbon-tab-spacer,
|
||||
.ribbon-body {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon-button-container {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user