mirror of
https://github.com/zadam/trilium.git
synced 2025-12-12 10:24:23 +01:00
New layout: Note info (#8015)
This commit is contained in:
commit
c3829f82ab
@ -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} />;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>();
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user