mirror of
https://github.com/zadam/trilium.git
synced 2025-12-12 10:24:23 +01:00
New layout improvements (#8012)
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
This commit is contained in:
commit
84bde62e05
@ -140,7 +140,7 @@ export default class DesktopLayout {
|
||||
.class("breadcrumb-row")
|
||||
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
||||
.child(<Breadcrumb />)
|
||||
.child(<BreadcrumbBadges />)
|
||||
.optChild(isNewLayout, <BreadcrumbBadges />)
|
||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
|
||||
@ -52,5 +52,5 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(<PopupEditorDialog />)
|
||||
.child(<CallToActionDialog />)
|
||||
.child(<ToastContainer />)
|
||||
.child(<ToastContainer />);
|
||||
}
|
||||
|
||||
@ -1321,6 +1321,11 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dropdown-submenu.dropstart > .dropdown-menu {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: calc(100% - 2px);
|
||||
}
|
||||
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: calc(-100% + 10px);
|
||||
}
|
||||
|
||||
@ -176,6 +176,11 @@ body.desktop .dropdown-submenu .dropdown-menu {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
:root .dropdown-item:focus-visible {
|
||||
outline: 2px solid var(--input-focus-outline-color) !important;
|
||||
background-color: transparent;
|
||||
@ -249,7 +254,7 @@ html body .dropdown-item[disabled] {
|
||||
}
|
||||
|
||||
/* Menu item arrow */
|
||||
.dropdown-menu .dropdown-toggle::after {
|
||||
.dropdown-submenu:not(.dropstart) .dropdown-toggle::after {
|
||||
content: "\ed3b" !important;
|
||||
position: absolute;
|
||||
display: flex !important;
|
||||
@ -265,6 +270,22 @@ html body .dropdown-item[disabled] {
|
||||
color: var(--menu-item-arrow-color) !important;
|
||||
}
|
||||
|
||||
.dropdown-submenu.dropstart .dropdown-toggle::before {
|
||||
content: "\ea4d" !important;
|
||||
position: absolute;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
margin: unset !important;
|
||||
border: unset !important;
|
||||
padding: 0 4px;
|
||||
font-family: boxicons;
|
||||
font-size: 1.2em;
|
||||
color: var(--menu-item-arrow-color) !important;
|
||||
}
|
||||
|
||||
body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after {
|
||||
content: "\ea4d" !important;
|
||||
}
|
||||
|
||||
@ -689,6 +689,7 @@
|
||||
"export_note": "Export note",
|
||||
"delete_note": "Delete note",
|
||||
"print_note": "Print note",
|
||||
"view_revisions": "Note revisions...",
|
||||
"save_revision": "Save revision",
|
||||
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
|
||||
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
|
||||
@ -1750,8 +1751,8 @@
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "type note's title here...",
|
||||
"created_on": "Created on {{date}}",
|
||||
"last_modified": "Last modified on {{date}}"
|
||||
"created_on": "Created on <Value />",
|
||||
"last_modified": "Last modified on <Value />"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "No notes have been found for given search parameters.",
|
||||
@ -2132,8 +2133,18 @@
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Read-only",
|
||||
"read_only_explicit_description": "This note has been manually set to read-only.\nClick to edit it temporarily.",
|
||||
"read_only_auto": "Auto read-only",
|
||||
"read_only_auto_description": "This note was set automatically to read-only mode for performance reasons. This automatic limit is adjustable from settings.\n\nClick to edit it temporarily.",
|
||||
"read_only_temporarily_disabled": "Temporarily editable",
|
||||
"read_only_temporarily_disabled_description": "This note is currently editable, but it is normally read-only. The note will go back to being read-only as soon as you navigate to another note.\n\nClick to re-enable read-only mode.",
|
||||
"shared_publicly": "Shared publicly",
|
||||
"shared_locally": "Shared locally"
|
||||
"shared_publicly_description": "This note has been published online at {{- link}}, and is publicly accessible.\n\nClick to navigate to the shared note or right click for more options.",
|
||||
"shared_locally": "Shared locally",
|
||||
"shared_locally_description": "This note is shared on the local network only at {{- link}}.\n\nClick to navigate to the shared note or right click for more options.",
|
||||
"backlinks_one": "{{count}} backlink",
|
||||
"backlinks_other": "{{count}} backlinks",
|
||||
"backlinks_description_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.",
|
||||
"backlinks_description_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks."
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,49 @@
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
container-type: inline-size;
|
||||
|
||||
@container (max-width: 700px) {
|
||||
.breadcrumb-badges {
|
||||
flex-shrink: 0;
|
||||
|
||||
>* {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 0 2px;
|
||||
|
||||
>* {
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.breadcrumb {
|
||||
.btn.icon-action {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .breadcrumb-row {
|
||||
@ -53,11 +96,23 @@ body.experimental-feature-new-layout .breadcrumb-row {
|
||||
}
|
||||
|
||||
.dropdown-item span,
|
||||
.dropdown-item strong {
|
||||
.dropdown-item strong,
|
||||
.breadcrumb-last-item {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.breadcrumb-last-item {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0 10px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import "./Breadcrumb.css";
|
||||
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
@ -12,6 +12,8 @@ import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./
|
||||
import Icon from "./react/Icon";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
import link_context_menu from "../menus/link_context_menu";
|
||||
import { TitleEditor } from "./collections/board";
|
||||
import server from "../services/server";
|
||||
|
||||
const COLLAPSE_THRESHOLD = 5;
|
||||
const INITIAL_ITEMS = 2;
|
||||
@ -27,10 +29,7 @@ export default function Breadcrumb() {
|
||||
<>
|
||||
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
|
||||
<Fragment key={item}>
|
||||
{index === 0
|
||||
? <BreadcrumbRoot noteContext={noteContext} />
|
||||
: <BreadcrumbItem notePath={item} />
|
||||
}
|
||||
<BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
|
||||
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
|
||||
</Fragment>
|
||||
))}
|
||||
@ -38,7 +37,7 @@ export default function Breadcrumb() {
|
||||
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
|
||||
<Fragment key={item}>
|
||||
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
|
||||
<BreadcrumbItem notePath={item} />
|
||||
<BreadcrumbItem index={notePath.length - FINAL_ITEMS + index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
@ -47,7 +46,7 @@ export default function Breadcrumb() {
|
||||
<Fragment key={item}>
|
||||
{index === 0
|
||||
? <BreadcrumbRoot noteContext={noteContext} />
|
||||
: <BreadcrumbItem notePath={item} />
|
||||
: <BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
|
||||
}
|
||||
{(index < notePath.length - 1 || note?.hasChildren()) &&
|
||||
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
|
||||
@ -76,15 +75,56 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ notePath }: { notePath: string }) {
|
||||
function BreadcrumbLink({ notePath }: { notePath: string }) {
|
||||
return (
|
||||
<NoteLink
|
||||
notePath={notePath}
|
||||
noPreview
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
|
||||
const noteId = notePath.split("/").at(-1);
|
||||
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
|
||||
const [ isEditing, setIsEditing ] = useState(false);
|
||||
const title = useNoteProperty(note, "title");
|
||||
|
||||
if (!note) return null;
|
||||
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="breadcrumb-last-item tn-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>{title}</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleEditor
|
||||
currentValue={title}
|
||||
save={(newTitle) => { return server.put(`notes/${noteId}/title`, { title: newTitle.trim() }); }}
|
||||
dismiss={() => setIsEditing(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) {
|
||||
if (index === 0) {
|
||||
return <BreadcrumbRoot noteContext={noteContext} />;
|
||||
}
|
||||
|
||||
if (index === notePathLength - 1) {
|
||||
return <BreadcrumbLastItem notePath={notePath} />;
|
||||
}
|
||||
|
||||
return <BreadcrumbLink notePath={notePath} />;
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
|
||||
return (
|
||||
<Dropdown
|
||||
|
||||
@ -1,23 +1,71 @@
|
||||
.component.breadcrumb-badges {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.breadcrumb-badges {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
contain: none;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
--badge-radius: 12px;
|
||||
|
||||
.breadcrumb-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--badge-radius);
|
||||
font-size: 0.75em;
|
||||
background-color: var(--badge-background-color);
|
||||
color: var(--badge-text-color);
|
||||
background-color: var(--color, transparent);
|
||||
color: white;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--badge-background-hover-color);
|
||||
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 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--badge-radius);
|
||||
|
||||
&.dropdown-backlinks-badge .dropdown-menu {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.breadcrumb-badge {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import "./BreadcrumbBadges.css";
|
||||
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useIsNoteReadOnly, useNoteContext } from "./react/hooks";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren, MouseEventHandler } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../services/i18n";
|
||||
import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions";
|
||||
import Dropdown, { DropdownProps } from "./react/Dropdown";
|
||||
import { useIsNoteReadOnly, useNoteContext, useStaticTooltip } from "./react/hooks";
|
||||
import Icon from "./react/Icon";
|
||||
import { useShareInfo } from "./shared_info";
|
||||
import clsx from "clsx";
|
||||
import { t } from "../services/i18n";
|
||||
|
||||
export default function BreadcrumbBadges() {
|
||||
return (
|
||||
<div className="breadcrumb-badges">
|
||||
<ReadOnlyBadge />
|
||||
<ShareBadge />
|
||||
<BacklinksBadge />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -20,37 +25,113 @@ function ReadOnlyBadge() {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||
const isTemporarilyEditable = noteContext?.viewScope?.readOnlyTemporarilyDisabled;
|
||||
|
||||
return (isReadOnly &&
|
||||
<Badge
|
||||
icon="bx bx-lock"
|
||||
onClick={() => enableEditing()}>
|
||||
{isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")}
|
||||
</Badge>
|
||||
);
|
||||
if (isTemporarilyEditable) {
|
||||
return <Badge
|
||||
icon="bx bx-lock-open-alt"
|
||||
text={t("breadcrumb_badges.read_only_temporarily_disabled")}
|
||||
tooltip={t("breadcrumb_badges.read_only_temporarily_disabled_description")}
|
||||
className="temporarily-editable-badge"
|
||||
onClick={() => enableEditing(false)}
|
||||
/>;
|
||||
} else if (isReadOnly) {
|
||||
return <Badge
|
||||
icon="bx bx-lock-alt"
|
||||
text={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")}
|
||||
tooltip={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit_description") : t("breadcrumb_badges.read_only_auto_description")}
|
||||
className="read-only-badge"
|
||||
onClick={() => enableEditing()}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
function ShareBadge() {
|
||||
const { note } = useNoteContext();
|
||||
const { isSharedExternally, link } = useShareInfo(note);
|
||||
const { isSharedExternally, link, linkHref } = useShareInfo(note);
|
||||
|
||||
return (link &&
|
||||
<Badge
|
||||
icon={isSharedExternally ? "bx bx-world" : "bx bx-link"}
|
||||
>
|
||||
{isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")}
|
||||
</Badge>
|
||||
text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")}
|
||||
tooltip={isSharedExternally ?
|
||||
t("breadcrumb_badges.shared_publicly_description", { link }) :
|
||||
t("breadcrumb_badges.shared_locally_description", { link })
|
||||
}
|
||||
className="share-badge"
|
||||
href={linkHref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Badge({ icon, children, onClick }: { icon: string, children: ComponentChildren, onClick?: () => void }) {
|
||||
function BacklinksBadge() {
|
||||
const { note, viewScope } = useNoteContext();
|
||||
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
return (note && count > 0 &&
|
||||
<BadgeWithDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
icon="bx bx-revision"
|
||||
text={t("breadcrumb_badges.backlinks", { count })}
|
||||
tooltip={t("breadcrumb_badges.backlinks_description", { count })}
|
||||
dropdownOptions={{
|
||||
dropdownContainerClassName: "backlinks-items"
|
||||
}}
|
||||
>
|
||||
<BacklinksList note={note} />
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
interface BadgeProps {
|
||||
text: string;
|
||||
icon?: string;
|
||||
className: string;
|
||||
tooltip?: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useStaticTooltip(containerRef, {
|
||||
placement: "bottom",
|
||||
fallbackPlacements: [ "bottom" ],
|
||||
animation: false,
|
||||
html: true,
|
||||
title: tooltip
|
||||
});
|
||||
|
||||
const content = <>
|
||||
{icon && <><Icon icon={icon} /> </>}
|
||||
<span class="text">{text}</span>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("breadcrumb-badge", { "clickable": !!onClick })}
|
||||
ref={containerRef}
|
||||
className={clsx("breadcrumb-badge", className, { "clickable": !!onClick })}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
{children}
|
||||
{href ? <a href={href}>{content}</a> : <span>{content}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & {
|
||||
children: ComponentChildren,
|
||||
dropdownOptions?: Partial<DropdownProps>
|
||||
}) {
|
||||
return (
|
||||
<Dropdown
|
||||
className={`dropdown-${className}`}
|
||||
text={<Badge className={className} {...props} />}
|
||||
noDropdownListStyle
|
||||
noSelectButtonStyle
|
||||
hideToggleArrow
|
||||
title={tooltip}
|
||||
titlePosition="bottom"
|
||||
dropdownOptions={{ popperConfig: { placement: "bottom", strategy: "fixed" } }}
|
||||
{...dropdownOptions}
|
||||
>{children}</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ 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 { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/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";
|
||||
@ -20,6 +20,7 @@ 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;
|
||||
@ -76,6 +77,8 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
ToggleReadOnlyButton
|
||||
];
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
|
||||
return isEnabled && <FloatingButton
|
||||
@ -308,22 +311,9 @@ function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
}
|
||||
|
||||
function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
let [ backlinkCount, setBacklinkCount ] = useState(0);
|
||||
let [ popupOpen, setPopupOpen ] = useState(false);
|
||||
const [ popupOpen, setPopupOpen ] = useState(false);
|
||||
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function refresh() {
|
||||
if (!isDefaultViewMode) return;
|
||||
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => refresh(), [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (needsRefresh(note, loadResults)) refresh();
|
||||
});
|
||||
const backlinkCount = useBacklinkCount(note, isDefaultViewMode);
|
||||
|
||||
// Determine the max height of the container.
|
||||
const { windowHeight } = useWindowSize();
|
||||
@ -336,7 +326,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
}
|
||||
}, [ popupOpen, windowHeight ]);
|
||||
|
||||
const isEnabled = isDefaultViewMode && backlinkCount > 0;
|
||||
const isEnabled = !isNewLayout && isDefaultViewMode && backlinkCount > 0;
|
||||
return (isEnabled &&
|
||||
<div className="backlinks-widget has-overflow">
|
||||
<div
|
||||
@ -355,15 +345,34 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksList({ note }: { note: FNote }) {
|
||||
export function useBacklinkCount(note: FNote | null | undefined, isDefaultViewMode: boolean) {
|
||||
const [ backlinkCount, setBacklinkCount ] = useState(0);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (!note || !isDefaultViewMode) return;
|
||||
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}, [ isDefaultViewMode, note ]);
|
||||
|
||||
useEffect(() => refresh(), [ refresh ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (note && needsRefresh(note, loadResults)) refresh();
|
||||
});
|
||||
|
||||
return backlinkCount;
|
||||
}
|
||||
|
||||
export function BacklinksList({ note }: { note: FNote }) {
|
||||
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
|
||||
|
||||
function refresh() {
|
||||
server.get<BacklinksResponse>(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => {
|
||||
// prefetch all
|
||||
const noteIds = backlinks
|
||||
.filter(bl => "noteId" in bl)
|
||||
.map((bl) => bl.noteId);
|
||||
.filter(bl => "noteId" in bl)
|
||||
.map((bl) => bl.noteId);
|
||||
await froca.getNotes(noteIds);
|
||||
setBacklinks(backlinks);
|
||||
});
|
||||
|
||||
@ -1,23 +1,60 @@
|
||||
import { t } from "../services/i18n";
|
||||
import { type ComponentChild } from "preact";
|
||||
|
||||
import { formatDateTime } from "../utils/formatters";
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
import { useNoteContext, useStaticTooltip } from "./react/hooks";
|
||||
import { joinElements } from "./react/react_utils";
|
||||
import { useNoteMetadata } from "./ribbon/NoteInfoTab";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
export default function NoteTitleDetails() {
|
||||
const { note } = useNoteContext();
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const { metadata } = useNoteMetadata(note);
|
||||
const isHiddenNote = note?.noteId.startsWith("_");
|
||||
const isDefaultView = noteContext?.viewScope?.viewMode === "default";
|
||||
|
||||
const items: ComponentChild[] = [
|
||||
(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 (
|
||||
<div className="title-details">
|
||||
{joinElements([
|
||||
metadata?.dateCreated && <li>
|
||||
{t("note_title.created_on", { date: formatDateTime(metadata.dateCreated, "medium", "none")} )}
|
||||
</li>,
|
||||
metadata?.dateModified && <li>
|
||||
{t("note_title.last_modified", { date: formatDateTime(metadata.dateModified, "medium", "none")} )}
|
||||
</li>
|
||||
], " • ")}
|
||||
{joinElements(items, " • ")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextWithValue({ i18nKey, value, valueTooltip }: {
|
||||
i18nKey: string;
|
||||
value: string;
|
||||
valueTooltip: string;
|
||||
}) {
|
||||
const listItemRef = useRef<HTMLLIElement>(null);
|
||||
useStaticTooltip(listItemRef, {
|
||||
selector: "span.value",
|
||||
title: valueTooltip,
|
||||
popperConfig: { placement: "bottom" }
|
||||
});
|
||||
|
||||
return (
|
||||
<li ref={listItemRef}>
|
||||
<Trans
|
||||
i18nKey={i18nKey}
|
||||
components={{
|
||||
Value: <span className="value">{value}</span> as React.ReactElement
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import shortcutService from "../../services/shortcuts.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import type { Attribute } from "../../services/attribute_parser.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="attr-detail tn-tool-dialog">
|
||||
@ -309,6 +310,8 @@ interface SearchRelatedResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
private $title!: JQuery<HTMLElement>;
|
||||
private $inputName!: JQuery<HTMLElement>;
|
||||
@ -579,6 +582,13 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
|
||||
if (isNewLayout) {
|
||||
this.$widget
|
||||
.css("top", "unset")
|
||||
.css("bottom", 70)
|
||||
.css("max-height", "80vh");
|
||||
}
|
||||
|
||||
if (focus === "name") {
|
||||
this.$inputName.trigger("focus").trigger("select");
|
||||
}
|
||||
|
||||
@ -243,7 +243,7 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
|
||||
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
|
||||
currentValue?: string;
|
||||
placeholder?: string;
|
||||
save: (newValue: string) => void;
|
||||
save: (newValue: string) => void | Promise<void>;
|
||||
dismiss: () => void;
|
||||
isNewItem?: boolean;
|
||||
mode?: "normal" | "multiline" | "relation";
|
||||
|
||||
@ -31,6 +31,7 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .note-title-widget {
|
||||
|
||||
@ -1,26 +1,32 @@
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import "./PopupEditor.css";
|
||||
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { NoteContextContext, ParentComponent } from "../react/react_utils";
|
||||
import NoteDetail from "../NoteDetail";
|
||||
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import tree from "../../services/tree";
|
||||
import utils from "../../services/utils";
|
||||
import NoteList from "../collections/NoteList";
|
||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import PromotedAttributes from "../PromotedAttributes";
|
||||
import FloatingButtons from "../FloatingButtons";
|
||||
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
|
||||
import utils from "../../services/utils";
|
||||
import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteDetail from "../NoteDetail";
|
||||
import PromotedAttributes from "../PromotedAttributes";
|
||||
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { NoteContextContext, ParentComponent } from "../react/react_utils";
|
||||
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
|
||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||
import { t } from "../../services/i18n";
|
||||
import appContext from "../../components/app_context";
|
||||
import BreadcrumbBadges from "../BreadcrumbBadges";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function PopupEditor() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
@ -61,7 +67,10 @@ export default function PopupEditor() {
|
||||
<NoteContextContext.Provider value={noteContext}>
|
||||
<DialogWrapper>
|
||||
<Modal
|
||||
title={<TitleRow />}
|
||||
title={<>
|
||||
<TitleRow />
|
||||
{isNewLayout && <BreadcrumbBadges />}
|
||||
</>}
|
||||
customTitleBarButtons={[{
|
||||
iconClassName: "bx-expand-alt",
|
||||
title: t("popup-editor.maximize"),
|
||||
@ -75,19 +84,17 @@ export default function PopupEditor() {
|
||||
className="popup-editor-dialog"
|
||||
size="lg"
|
||||
show={shown}
|
||||
onShown={() => {
|
||||
parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId });
|
||||
}}
|
||||
onShown={() => parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId })}
|
||||
onHidden={() => setShown(false)}
|
||||
keepInDom // needed for faster loading
|
||||
noFocus // automatic focus breaks block popup
|
||||
>
|
||||
<ReadOnlyNoteInfoBar />
|
||||
{!isNewLayout && <ReadOnlyNoteInfoBar />}
|
||||
<PromotedAttributes />
|
||||
|
||||
{isMobile
|
||||
? <MobileEditorToolbar inPopupEditor />
|
||||
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
|
||||
? <MobileEditorToolbar inPopupEditor />
|
||||
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
|
||||
|
||||
<FloatingButtons items={items} />
|
||||
<NoteDetail />
|
||||
@ -95,7 +102,7 @@ export default function PopupEditor() {
|
||||
</Modal>
|
||||
</DialogWrapper>
|
||||
</NoteContextContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogWrapper({ children }: { children: ComponentChildren }) {
|
||||
@ -107,7 +114,7 @@ export function DialogWrapper({ children }: { children: ComponentChildren }) {
|
||||
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function TitleRow() {
|
||||
@ -116,5 +123,5 @@ export function TitleRow() {
|
||||
<NoteIcon />
|
||||
<NoteTitleWidget />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,30 +29,73 @@ body.desktop .note-title-widget input.note-title {
|
||||
font-size: 180%;
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .title-row,
|
||||
body.experimental-feature-new-layout .title-details {
|
||||
max-width: var(--max-content-width);
|
||||
}
|
||||
body.experimental-feature-new-layout {
|
||||
.title-row,
|
||||
.title-details {
|
||||
max-width: var(--max-content-width);
|
||||
padding: 0;
|
||||
padding-inline-start: 24px;
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .title-row {
|
||||
margin-top: 2em;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.title-row {
|
||||
margin-left: 12px;
|
||||
|
||||
body.experimental-feature-new-layout .title-details {
|
||||
margin-top: 0;
|
||||
contain: none;
|
||||
padding: 0;
|
||||
padding-inline-start: 24px;
|
||||
opacity: 0.85;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.note-icon-widget {
|
||||
padding: 0;
|
||||
width: 41px;
|
||||
}
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout.prefers-centered-content .title-row,
|
||||
body.experimental-feature-new-layout.prefers-centered-content .title-details {
|
||||
margin-inline: auto;
|
||||
.note-split.type-code:not(.mime-text-x-sqlite) .title-row,
|
||||
.note-split.type-code:not(.mime-text-x-sqlite) .title-details {
|
||||
background-color: var(--main-background-color);
|
||||
}
|
||||
|
||||
.title-details {
|
||||
margin-top: 0;
|
||||
contain: none;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
|
||||
span.value {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.note-split.view-mode-default {
|
||||
.title-row {
|
||||
padding-top: 2em;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.title-details {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) {
|
||||
.title-row,
|
||||
.title-details {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
padding-inline-start: 15px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.title-details {
|
||||
margin-bottom: 0.2em;
|
||||
opacity: 0.65;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
&.prefers-centered-content .title-row,
|
||||
&.prefers-centered-content .title-details {
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,6 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
|
||||
this.$widget.addClass(utils.getNoteTypeClass(note.type));
|
||||
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
||||
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
|
||||
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
|
||||
this.$widget.toggleClass("protected", note.isProtected);
|
||||
|
||||
|
||||
@ -117,8 +117,8 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
aria-expanded="false"
|
||||
id={id ?? ariaId}
|
||||
disabled={disabled}
|
||||
onMouseOver={() => showTooltip()}
|
||||
onMouseLeave={() => hideTooltip()}
|
||||
onMouseEnter={showTooltip}
|
||||
onMouseLeave={hideTooltip}
|
||||
{...buttonProps}
|
||||
>
|
||||
{text}
|
||||
|
||||
@ -161,11 +161,16 @@ export function FormDropdownDivider() {
|
||||
return <div className="dropdown-divider" />;
|
||||
}
|
||||
|
||||
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
|
||||
export function FormDropdownSubmenu({ icon, title, children, dropStart }: {
|
||||
icon: string,
|
||||
title: ComponentChildren,
|
||||
children: ComponentChildren,
|
||||
dropStart?: boolean
|
||||
}) {
|
||||
const [ openOnMobile, setOpenOnMobile ] = useState(false);
|
||||
|
||||
return (
|
||||
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
|
||||
<li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}>
|
||||
<span
|
||||
className="dropdown-toggle"
|
||||
onClick={(e) => {
|
||||
@ -184,5 +189,5 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t
|
||||
{children}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -201,7 +201,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
|
||||
return [
|
||||
(value === "true"),
|
||||
(newValue) => setValue(newValue ? "true" : "false")
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,17 +217,18 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
|
||||
return [
|
||||
(parseInt(value, 10)),
|
||||
(newValue) => setValue(newValue)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
|
||||
*
|
||||
* @param name the name of the option to listen for.
|
||||
* @param needsRefresh whether to reload the frontend whenever the value is changed.
|
||||
* @returns an array where the first value is the current option value and the second value is the setter.
|
||||
*/
|
||||
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
|
||||
const [ value, setValue ] = useTriliumOption(name);
|
||||
export function useTriliumOptionJson<T>(name: OptionNames, needsRefresh?: boolean): [ T, (newValue: T) => Promise<void> ] {
|
||||
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
|
||||
useDebugValue(name);
|
||||
return [
|
||||
(JSON.parse(value) as T),
|
||||
@ -845,9 +846,9 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h
|
||||
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
|
||||
const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const enableEditing = useCallback(() => {
|
||||
const enableEditing = useCallback((enabled = true) => {
|
||||
if (noteContext?.viewScope) {
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
|
||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
|
||||
}
|
||||
}, [noteContext]);
|
||||
@ -862,7 +863,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||
setIsReadOnly(false);
|
||||
setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/u
|
||||
import ws from "../../services/ws";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteProperty, useTriliumOption } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
@ -98,7 +98,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
}
|
||||
<FormDropdownDivider />
|
||||
|
||||
<CommandItem command="showRevisions" icon="bx bx-history" text={t("revisions_button.note_revisions")} />
|
||||
<CommandItem command="showRevisions" icon="bx bx-history" text={t("note_actions.view_revisions")} />
|
||||
<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}
|
||||
@ -114,23 +114,22 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
|
||||
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
return (
|
||||
<>
|
||||
<FormListHeader text="Development-only Actions" />
|
||||
<FormDropdownSubmenu title="Development Actions" icon="bx bx-wrench" dropStart>
|
||||
<FormListItem
|
||||
icon="bx bx-printer"
|
||||
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
||||
>Open print page</FormListItem>
|
||||
{note.type === "text" && (
|
||||
<FormListItem
|
||||
icon="bx bx-error"
|
||||
onClick={() => {
|
||||
noteContext?.getTextEditor(editor => {
|
||||
editor.editing.view.change(() => {
|
||||
throw new Error("Editor crashed.");
|
||||
});
|
||||
<FormListItem
|
||||
icon="bx bx-error"
|
||||
disabled={note.type !== "text"}
|
||||
onClick={() => {
|
||||
noteContext?.getTextEditor(editor => {
|
||||
editor.editing.view.change(() => {
|
||||
throw new Error("Editor crashed.");
|
||||
});
|
||||
}}>Crash editor</FormListItem>)}
|
||||
</>
|
||||
});
|
||||
}}>Crash editor</FormListItem>
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
{
|
||||
title: t("classic_editor_toolbar.title"),
|
||||
icon: "bx bx-text",
|
||||
show: async ({ note, noteContext }) => note?.type === "text"
|
||||
show: async ({ note, noteContext }) => note?.type === "text" && noteContext?.viewScope?.viewMode === "default"
|
||||
&& options.get("textNoteEditorType") === "ckeditor-classic"
|
||||
&& !(await noteContext?.isReadOnly()),
|
||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||
|
||||
@ -26,6 +26,7 @@ export default function SharedInfo() {
|
||||
|
||||
export function useShareInfo(note: FNote | null | undefined) {
|
||||
const [ link, setLink ] = useState<string>();
|
||||
const [ linkHref, setLinkHref ] = useState<string>();
|
||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
||||
|
||||
function refresh() {
|
||||
@ -52,9 +53,10 @@ export function useShareInfo(note: FNote | null | undefined) {
|
||||
}
|
||||
|
||||
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
|
||||
setLinkHref(link);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
useEffect(refresh, [ note, syncServerHost ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.name?.startsWith("_share") && attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
@ -63,7 +65,7 @@ export function useShareInfo(note: FNote | null | undefined) {
|
||||
}
|
||||
});
|
||||
|
||||
return { link, isSharedExternally: !!syncServerHost };
|
||||
return { link, linkHref, isSharedExternally: !!syncServerHost };
|
||||
}
|
||||
|
||||
function getShareId(note: FNote) {
|
||||
|
||||
@ -158,7 +158,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function VacuumDatabaseOptions() {
|
||||
@ -175,11 +175,11 @@ function VacuumDatabaseOptions() {
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ExperimentalOptions() {
|
||||
const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson<string[]>("experimentalFeatures");
|
||||
const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson<string[]>("experimentalFeatures", true);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("experimental_features.title")}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user