New layout: Note info (#8015)

This commit is contained in:
Elian Doran 2025-12-11 17:18:19 +02:00 committed by GitHub
commit c3829f82ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 146 additions and 89 deletions

View File

@ -14,6 +14,7 @@ import NoteLink from "./react/NoteLink";
import link_context_menu from "../menus/link_context_menu"; import link_context_menu from "../menus/link_context_menu";
import { TitleEditor } from "./collections/board"; import { TitleEditor } from "./collections/board";
import server from "../services/server"; import server from "../services/server";
import { NoteInfoBadge } from "./BreadcrumbBadges";
const COLLAPSE_THRESHOLD = 5; const COLLAPSE_THRESHOLD = 5;
const INITIAL_ITEMS = 2; const INITIAL_ITEMS = 2;
@ -119,7 +120,10 @@ function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { inde
} }
if (index === notePathLength - 1) { if (index === notePathLength - 1) {
return <BreadcrumbLastItem notePath={notePath} />; return <>
<BreadcrumbLastItem notePath={notePath} />
<NoteInfoBadge note={noteContext?.note} />
</>;
} }
return <BreadcrumbLink notePath={notePath} />; return <BreadcrumbLink notePath={notePath} />;

View File

@ -9,63 +9,87 @@
flex-shrink: 1; flex-shrink: 1;
overflow: hidden; overflow: hidden;
--badge-radius: 12px; --badge-radius: 12px;
}
.breadcrumb-badge { .breadcrumb-badge {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 6px; padding: 2px 6px;
border-radius: var(--badge-radius); border-radius: var(--badge-radius);
font-size: 0.75em; font-size: 0.75em;
background-color: var(--color, transparent); background-color: var(--color, transparent);
color: white; color: white;
min-width: 0; min-width: 0;
flex-shrink: 1; flex-shrink: 1;
&.clickable { &.clickable {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black); background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black);
}
}
&.temporarily-editable-badge { --color: #4fa52b; }
&.read-only-badge { --color: #e33f3b; }
&.share-badge { --color: #3b82f6; }
&.backlinks-badge { color: var(--badge-text-color); }
a {
color: inherit;
text-decoration: none;
}
> * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
.dropdown { &.temporarily-editable-badge { --color: #4fa52b; }
&.read-only-badge { --color: #e33f3b; }
&.share-badge { --color: #3b82f6; }
&.backlinks-badge { color: var(--badge-text-color); }
a {
color: inherit !important;
text-decoration: none;
}
> * {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
border-radius: var(--badge-radius); }
}
&.dropdown-backlinks-badge .dropdown-menu {
min-width: 500px; .breadcrumb-dropdown-badge {
} min-width: 0;
overflow: hidden;
.breadcrumb-badge { text-overflow: ellipsis;
border-radius: 0; white-space: nowrap;
} border-radius: var(--badge-radius);
.btn { &.dropdown-backlinks-badge .dropdown-menu {
border: 0; min-width: 500px;
margin: 0; }
padding: 0;
} &.dropdown-note-info-badge {
.dropdown-menu.show ul {
list-style-type: none;
padding: 0.5em;
margin: 0;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: 0.2em 0;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
.breadcrumb-badge {
border-radius: 0;
}
.btn {
border: 0;
margin: 0;
padding: 0;
} }
} }

View File

@ -5,11 +5,14 @@ import { ComponentChildren, MouseEventHandler } from "preact";
import { useRef } from "preact/hooks"; import { useRef } from "preact/hooks";
import { t } from "../services/i18n"; import { t } from "../services/i18n";
import { formatDateTime } from "../utils/formatters";
import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions"; import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions";
import Dropdown, { DropdownProps } from "./react/Dropdown"; import Dropdown, { DropdownProps } from "./react/Dropdown";
import { useIsNoteReadOnly, useNoteContext, useStaticTooltip } from "./react/hooks"; import { useIsNoteReadOnly, useNoteContext, useStaticTooltip } from "./react/hooks";
import Icon from "./react/Icon"; import Icon from "./react/Icon";
import { NoteSizeWidget, useNoteMetadata } from "./ribbon/NoteInfoTab";
import { useShareInfo } from "./shared_info"; import { useShareInfo } from "./shared_info";
import FNote from "../entities/fnote";
export default function BreadcrumbBadges() { export default function BreadcrumbBadges() {
return ( return (
@ -21,6 +24,35 @@ export default function BreadcrumbBadges() {
); );
} }
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
return (note &&
<BadgeWithDropdown
icon="bx bx-info-circle"
className="note-info-badge"
dropdownOptions={{ dropdownOptions: { autoClose: "outside" } }}
>
<ul>
<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>
</BadgeWithDropdown>
);
}
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
return (
<li>
<strong title={title}>{text}{": "}</strong>
<span>{value}</span>
</li>
);
}
function ReadOnlyBadge() { function ReadOnlyBadge() {
const { note, noteContext } = useNoteContext(); const { note, noteContext } = useNoteContext();
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
@ -83,7 +115,7 @@ function BacklinksBadge() {
} }
interface BadgeProps { interface BadgeProps {
text: string; text?: string;
icon?: string; icon?: string;
className: string; className: string;
tooltip?: string; tooltip?: string;
@ -123,15 +155,21 @@ function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...p
}) { }) {
return ( return (
<Dropdown <Dropdown
className={`dropdown-${className}`} className={`breadcrumb-dropdown-badge dropdown-${className}`}
text={<Badge className={className} {...props} />} text={<Badge className={className} {...props} />}
noDropdownListStyle noDropdownListStyle
noSelectButtonStyle noSelectButtonStyle
hideToggleArrow hideToggleArrow
title={tooltip} title={tooltip}
titlePosition="bottom" titlePosition="bottom"
dropdownOptions={{ popperConfig: { placement: "bottom", strategy: "fixed" } }}
{...dropdownOptions} {...dropdownOptions}
dropdownOptions={{
...dropdownOptions?.dropdownOptions,
popperConfig: {
...dropdownOptions?.dropdownOptions?.popperConfig,
placement: "bottom", strategy: "fixed"
}
}}
>{children}</Dropdown> >{children}</Dropdown>
); );
} }

View File

@ -9,26 +9,12 @@ import { useRef } from "preact/hooks";
export default function NoteTitleDetails() { export default function NoteTitleDetails() {
const { note, noteContext } = useNoteContext(); const { note, noteContext } = useNoteContext();
const { metadata } = useNoteMetadata(note);
const isHiddenNote = note?.noteId.startsWith("_"); const isHiddenNote = note?.noteId.startsWith("_");
const isDefaultView = noteContext?.viewScope?.viewMode === "default"; const isDefaultView = noteContext?.viewScope?.viewMode === "default";
const items: ComponentChild[] = [ const items: ComponentChild[] = [].filter(item => !!item);
(isDefaultView && !isHiddenNote && metadata?.dateCreated &&
<TextWithValue
i18nKey="note_title.created_on"
value={formatDateTime(metadata.dateCreated, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateCreated, "full", "long")}
/>),
(isDefaultView && !isHiddenNote && metadata?.dateModified &&
<TextWithValue
i18nKey="note_title.last_modified"
value={formatDateTime(metadata.dateModified, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateModified, "full", "long")}
/>)
].filter(item => !!item);
return ( return items.length && (
<div className="title-details"> <div className="title-details">
{joinElements(items, " • ")} {joinElements(items, " • ")}
</div> </div>

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { TabContext } from "./ribbon-interface";
import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons"; import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
import server from "../../services/server"; import server from "../../services/server";
import Button from "../react/Button"; import Button from "../react/Button";
@ -13,8 +12,8 @@ import FNote from "../../entities/fnote";
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function NoteInfoTab({ note }: TabContext) { export default function NoteInfoTab({ note }: { note: FNote | null | undefined }) {
const { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo } = useNoteMetadata(note); const { metadata, ...sizeProps } = useNoteMetadata(note);
return ( return (
<div className="note-info-widget"> <div className="note-info-widget">
@ -42,23 +41,7 @@ export default function NoteInfoTab({ note }: TabContext) {
<div className="note-info-item"> <div className="note-info-item">
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span> <span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
<span className="note-info-size-col-span"> <span className="note-info-size-col-span">
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && ( <NoteSizeWidget {...sizeProps} />
<Button
className="calculate-button"
icon="bx bx-calculator"
text={t("note_info_widget.calculate")}
onClick={requestSizeInfo}
/>
)}
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</span> </span>
</div> </div>
</> </>
@ -67,6 +50,28 @@ export default function NoteInfoTab({ note }: TabContext) {
); );
} }
export function NoteSizeWidget({ isLoading, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }: Omit<ReturnType<typeof useNoteMetadata>, "metadata">) {
return <>
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<Button
className="calculate-button"
icon="bx bx-calculator"
text={t("note_info_widget.calculate")}
onClick={requestSizeInfo}
/>
)}
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</>;
}
export function useNoteMetadata(note: FNote | null | undefined) { export function useNoteMetadata(note: FNote | null | undefined) {
const [ isLoading, setIsLoading ] = useState(false); const [ isLoading, setIsLoading ] = useState(false);
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>(); const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();

View File

@ -132,7 +132,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{ {
title: t("note_info_widget.title"), title: t("note_info_widget.title"),
icon: "bx bx-info-circle", icon: "bx bx-info-circle",
show: ({ note }) => !!note, show: ({ note }) => !isNewLayout && !!note,
content: NoteInfoTab, content: NoteInfoTab,
toggleCommand: "toggleRibbonTabNoteInfo" toggleCommand: "toggleRibbonTabNoteInfo"
} }