mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 03:53:37 -06:00
Experimental layout (#8005)
This commit is contained in:
commit
f8b292dfa3
@ -46,6 +46,12 @@ import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
|||||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||||
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
||||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||||
|
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||||
|
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||||
|
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||||
|
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
|
||||||
|
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@ -71,6 +77,12 @@ export default class DesktopLayout {
|
|||||||
*/
|
*/
|
||||||
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
||||||
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
||||||
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
|
|
||||||
|
const titleRow = new FlexContainer("row")
|
||||||
|
.class("title-row")
|
||||||
|
.child(<NoteIconWidget />)
|
||||||
|
.child(<NoteTitleWidget />);
|
||||||
|
|
||||||
const rootContainer = new RootContainer(true)
|
const rootContainer = new RootContainer(true)
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
@ -126,30 +138,27 @@ export default class DesktopLayout {
|
|||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.class("breadcrumb-row")
|
.class("breadcrumb-row")
|
||||||
.css("height", "30px")
|
|
||||||
.css("min-height", "30px")
|
|
||||||
.css("align-items", "center")
|
|
||||||
.css("padding", "10px")
|
|
||||||
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
||||||
.child(<Breadcrumb />)
|
.child(<Breadcrumb />)
|
||||||
|
.child(<BreadcrumbBadges />)
|
||||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||||
.child(<MovePaneButton direction="left" />)
|
.child(<MovePaneButton direction="left" />)
|
||||||
.child(<MovePaneButton direction="right" />)
|
.child(<MovePaneButton direction="right" />)
|
||||||
.child(<ClosePaneButton />)
|
.child(<ClosePaneButton />)
|
||||||
.child(<CreatePaneButton />)
|
.child(<CreatePaneButton />)
|
||||||
|
.optChild(isNewLayout, <NoteActions />)
|
||||||
)
|
)
|
||||||
.child(new FlexContainer("row")
|
.optChild(!isNewLayout, titleRow)
|
||||||
.class("title-row")
|
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||||
.child(<NoteIconWidget />)
|
.optChild(isNewLayout, <StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||||
.child(<NoteTitleWidget />)
|
|
||||||
)
|
|
||||||
.child(<Ribbon />)
|
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.child(new ContentHeader()
|
.optChild(isNewLayout, titleRow)
|
||||||
|
.optChild(isNewLayout, <NoteTitleDetails />)
|
||||||
|
.optChild(!isNewLayout, new ContentHeader()
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
.child(<SharedInfo />)
|
.child(<SharedInfo />)
|
||||||
)
|
)
|
||||||
@ -167,6 +176,7 @@ export default class DesktopLayout {
|
|||||||
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||||
...this.customWidgets.get("note-detail-pane")
|
...this.customWidgets.get("note-detail-pane")
|
||||||
)
|
)
|
||||||
|
.optChild(isNewLayout, <Ribbon />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(...this.customWidgets.get("center-pane"))
|
.child(...this.customWidgets.get("center-pane"))
|
||||||
|
|||||||
41
apps/client/src/services/experimental_features.ts
Normal file
41
apps/client/src/services/experimental_features.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { t } from "./i18n";
|
||||||
|
import options from "./options";
|
||||||
|
|
||||||
|
interface ExperimentalFeature {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const experimentalFeatures = [
|
||||||
|
{
|
||||||
|
id: "new-layout",
|
||||||
|
name: t("experimental_features.new_layout_name"),
|
||||||
|
description: t("experimental_features.new_layout_description"),
|
||||||
|
}
|
||||||
|
] as const satisfies ExperimentalFeature[];
|
||||||
|
|
||||||
|
type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
|
||||||
|
|
||||||
|
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||||
|
|
||||||
|
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||||
|
return getEnabledFeatures().has(featureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnabledExperimentalFeatureIds() {
|
||||||
|
return getEnabledFeatures().values();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledFeatures() {
|
||||||
|
if (!enabledFeatures) {
|
||||||
|
let features: ExperimentalFeatureId[] = [];
|
||||||
|
try {
|
||||||
|
features = JSON.parse(options.get("experimentalFeatures")) as ExperimentalFeatureId[];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse experimental features from options:", e);
|
||||||
|
}
|
||||||
|
enabledFeatures = new Set(features);
|
||||||
|
}
|
||||||
|
return enabledFeatures;
|
||||||
|
}
|
||||||
@ -1096,6 +1096,12 @@
|
|||||||
"vacuuming_database": "Vacuuming database...",
|
"vacuuming_database": "Vacuuming database...",
|
||||||
"database_vacuumed": "Database has been vacuumed"
|
"database_vacuumed": "Database has been vacuumed"
|
||||||
},
|
},
|
||||||
|
"experimental_features": {
|
||||||
|
"title": "Experimental Options",
|
||||||
|
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
|
||||||
|
"new_layout_name": "New Layout",
|
||||||
|
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
|
||||||
|
},
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"theme_defined": "Theme defined",
|
"theme_defined": "Theme defined",
|
||||||
"fonts": "Fonts",
|
"fonts": "Fonts",
|
||||||
@ -1743,7 +1749,9 @@
|
|||||||
"printing_pdf": "Exporting to PDF in progress..."
|
"printing_pdf": "Exporting to PDF in progress..."
|
||||||
},
|
},
|
||||||
"note_title": {
|
"note_title": {
|
||||||
"placeholder": "type note's title here..."
|
"placeholder": "type note's title here...",
|
||||||
|
"created_on": "Created on {{date}}",
|
||||||
|
"last_modified": "Last modified on {{date}}"
|
||||||
},
|
},
|
||||||
"search_result": {
|
"search_result": {
|
||||||
"no_notes_found": "No notes have been found for given search parameters.",
|
"no_notes_found": "No notes have been found for given search parameters.",
|
||||||
@ -2121,5 +2129,11 @@
|
|||||||
"tab_history_navigation_buttons": {
|
"tab_history_navigation_buttons": {
|
||||||
"go-back": "Go back to previous note",
|
"go-back": "Go back to previous note",
|
||||||
"go-forward": "Go forward to next note"
|
"go-forward": "Go forward to next note"
|
||||||
|
},
|
||||||
|
"breadcrumb_badges": {
|
||||||
|
"read_only_explicit": "Read-only",
|
||||||
|
"read_only_auto": "Auto read-only",
|
||||||
|
"shared_publicly": "Shared publicly",
|
||||||
|
"shared_locally": "Shared locally"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
.breadcrumb-row {
|
.breadcrumb-row {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.experimental-feature-new-layout .breadcrumb-row {
|
||||||
|
padding-inline-end: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component.breadcrumb {
|
.component.breadcrumb {
|
||||||
|
|||||||
23
apps/client/src/widgets/BreadcrumbBadges.css
Normal file
23
apps/client/src/widgets/BreadcrumbBadges.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.component.breadcrumb-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
contain: none;
|
||||||
|
|
||||||
|
.breadcrumb-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
background-color: var(--badge-background-color);
|
||||||
|
color: var(--badge-text-color);
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--badge-background-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
apps/client/src/widgets/BreadcrumbBadges.tsx
Normal file
56
apps/client/src/widgets/BreadcrumbBadges.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import "./BreadcrumbBadges.css";
|
||||||
|
|
||||||
|
import { ComponentChildren } from "preact";
|
||||||
|
import { useIsNoteReadOnly, useNoteContext } 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 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReadOnlyBadge() {
|
||||||
|
const { note, noteContext } = useNoteContext();
|
||||||
|
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||||
|
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||||
|
|
||||||
|
return (isReadOnly &&
|
||||||
|
<Badge
|
||||||
|
icon="bx bx-lock"
|
||||||
|
onClick={() => enableEditing()}>
|
||||||
|
{isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShareBadge() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
const { isSharedExternally, link } = 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Badge({ icon, children, onClick }: { icon: string, children: ComponentChildren, onClick?: () => void }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("breadcrumb-badge", { "clickable": !!onClick })}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Icon icon={icon} />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
apps/client/src/widgets/NoteTitleDetails.tsx
Normal file
23
apps/client/src/widgets/NoteTitleDetails.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { t } from "../services/i18n";
|
||||||
|
import { formatDateTime } from "../utils/formatters";
|
||||||
|
import { useNoteContext } from "./react/hooks";
|
||||||
|
import { joinElements } from "./react/react_utils";
|
||||||
|
import { useNoteMetadata } from "./ribbon/NoteInfoTab";
|
||||||
|
|
||||||
|
export default function NoteTitleDetails() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
const { metadata } = useNoteMetadata(note);
|
||||||
|
|
||||||
|
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>
|
||||||
|
], " • ")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import FlexContainer from "./flex_container.js";
|
|||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import type BasicWidget from "../basic_widget.js";
|
import type BasicWidget from "../basic_widget.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
|
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||||
@ -37,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
this.#setBackdropEffects();
|
this.#setBackdropEffects();
|
||||||
this.#setThemeCapabilities();
|
this.#setThemeCapabilities();
|
||||||
this.#setLocaleAndDirection(options.get("locale"));
|
this.#setLocaleAndDirection(options.get("locale"));
|
||||||
|
this.#setExperimentalFeatures();
|
||||||
|
|
||||||
return super.render();
|
return super.render();
|
||||||
}
|
}
|
||||||
@ -56,7 +58,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
|
|
||||||
if (loadResults.isOptionReloaded("maxContentWidth")
|
if (loadResults.isOptionReloaded("maxContentWidth")
|
||||||
|| loadResults.isOptionReloaded("centerContent")) {
|
|| loadResults.isOptionReloaded("centerContent")) {
|
||||||
|
|
||||||
this.#setMaxContentWidth();
|
this.#setMaxContentWidth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,6 +101,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#setExperimentalFeatures() {
|
||||||
|
for (const featureId of getEnabledExperimentalFeatureIds()) {
|
||||||
|
document.body.classList.add(`experimental-feature-${featureId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#setLocaleAndDirection(locale: string) {
|
#setLocaleAndDirection(locale: string) {
|
||||||
const correspondingLocale = LOCALES.find(l => l.id === locale);
|
const correspondingLocale = LOCALES.find(l => l.id === locale);
|
||||||
document.body.lang = locale;
|
document.body.lang = locale;
|
||||||
|
|||||||
@ -28,3 +28,31 @@ body.mobile .note-title-widget input.note-title {
|
|||||||
body.desktop .note-title-widget input.note-title {
|
body.desktop .note-title-widget input.note-title {
|
||||||
font-size: 180%;
|
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 {
|
||||||
|
margin-top: 2em;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.experimental-feature-new-layout.prefers-centered-content .title-row,
|
||||||
|
body.experimental-feature-new-layout.prefers-centered-content .title-details {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,115 +1,115 @@
|
|||||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||||
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
|
|
||||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
|
||||||
import { ParentComponent } from "../react/react_utils";
|
|
||||||
import { t } from "../../services/i18n"
|
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
import { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks";
|
|
||||||
import { useTriliumOption } from "../react/hooks";
|
|
||||||
import ActionButton from "../react/ActionButton"
|
|
||||||
import appContext, { CommandNames } from "../../components/app_context";
|
import appContext, { CommandNames } from "../../components/app_context";
|
||||||
|
import NoteContext from "../../components/note_context";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
import branches from "../../services/branches";
|
import branches from "../../services/branches";
|
||||||
import dialog from "../../services/dialog";
|
import dialog from "../../services/dialog";
|
||||||
import Dropdown from "../react/Dropdown";
|
import { t } from "../../services/i18n";
|
||||||
import FNote from "../../entities/fnote"
|
|
||||||
import NoteContext from "../../components/note_context";
|
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
|
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||||
import ws from "../../services/ws";
|
import ws from "../../services/ws";
|
||||||
|
import ActionButton from "../react/ActionButton";
|
||||||
|
import Dropdown from "../react/Dropdown";
|
||||||
|
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
|
||||||
|
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteProperty, useTriliumOption } from "../react/hooks";
|
||||||
|
import { ParentComponent } from "../react/react_utils";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||||
|
|
||||||
interface NoteActionsProps {
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
note?: FNote;
|
|
||||||
noteContext?: NoteContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NoteActions({ note, noteContext }: NoteActionsProps) {
|
export default function NoteActions() {
|
||||||
return (
|
const { note, noteContext } = useNoteContext();
|
||||||
<>
|
return (
|
||||||
{note && <RevisionsButton note={note} />}
|
<div className="ribbon-button-container" style={{ contain: "none" }}>
|
||||||
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
|
{note && !isNewLayout && <RevisionsButton note={note} />}
|
||||||
</>
|
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RevisionsButton({ note }: { note: FNote }) {
|
function RevisionsButton({ note }: { note: FNote }) {
|
||||||
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
||||||
|
|
||||||
return (isEnabled &&
|
return (isEnabled &&
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="bx bx-history"
|
icon="bx bx-history"
|
||||||
text={t("revisions_button.note_revisions")}
|
text={t("revisions_button.note_revisions")}
|
||||||
triggerCommand="showRevisions"
|
triggerCommand="showRevisions"
|
||||||
titlePosition="bottom"
|
titlePosition="bottom"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
const noteType = useNoteProperty(note, "type") ?? "";
|
const noteType = useNoteProperty(note, "type") ?? "";
|
||||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
const [viewType] = useNoteLabel(note, "viewType");
|
||||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||||
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
|
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
|
||||||
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
||||||
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
|
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
|
||||||
const isElectron = getIsElectron();
|
const isElectron = getIsElectron();
|
||||||
const isMac = getIsMac();
|
const isMac = getIsMac();
|
||||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
|
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
|
||||||
const isSearchOrBook = ["search", "book"].includes(noteType);
|
const isSearchOrBook = ["search", "book"].includes(noteType);
|
||||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
const [syncServerHost] = useTriliumOption("syncServerHost");
|
||||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
buttonClassName="bx bx-dots-vertical-rounded"
|
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
|
||||||
className="note-actions"
|
className="note-actions"
|
||||||
hideToggleArrow
|
hideToggleArrow
|
||||||
noSelectButtonStyle
|
noSelectButtonStyle
|
||||||
iconAction>
|
iconAction>
|
||||||
|
|
||||||
{isReadOnly && <>
|
{isReadOnly && <>
|
||||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||||
command={() => enableEditing()} />
|
command={() => enableEditing()} />
|
||||||
<FormDropdownDivider />
|
<FormDropdownDivider />
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
|
{canBeConvertedToAttachment && <ConvertToAttachment note={note} />}
|
||||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
|
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
|
||||||
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
||||||
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
||||||
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
|
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
|
||||||
<FormDropdownDivider />
|
<FormDropdownDivider />
|
||||||
|
|
||||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||||
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
||||||
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
|
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
|
||||||
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
||||||
notePath: noteContext.notePath,
|
notePath: noteContext.notePath,
|
||||||
defaultType: "single"
|
defaultType: "single"
|
||||||
})} />
|
})} />
|
||||||
<FormDropdownDivider />
|
<FormDropdownDivider />
|
||||||
|
|
||||||
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
|
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
|
||||||
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
|
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
|
||||||
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
|
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
|
||||||
{(syncServerHost && isElectron) &&
|
{(syncServerHost && isElectron) &&
|
||||||
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
|
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
|
||||||
}
|
}
|
||||||
<FormDropdownDivider />
|
<FormDropdownDivider />
|
||||||
|
|
||||||
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
|
<CommandItem command="showRevisions" icon="bx bx-history" text={t("revisions_button.note_revisions")} />
|
||||||
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
|
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
|
||||||
disabled={isInOptionsOrHelp}
|
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
|
||||||
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
|
disabled={isInOptionsOrHelp}
|
||||||
/>
|
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
|
||||||
<FormDropdownDivider />
|
/>
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
||||||
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
|
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||||
@ -129,46 +129,46 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?:
|
|||||||
throw new Error("Editor crashed.");
|
throw new Error("Editor crashed.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}>Crash editor</FormListItem>)}
|
}}>Crash editor</FormListItem>)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
||||||
return <FormListItem
|
return <FormListItem
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={title}
|
title={title}
|
||||||
triggerCommand={typeof command === "string" ? command : undefined}
|
triggerCommand={typeof command === "string" ? command : undefined}
|
||||||
onClick={typeof command === "function" ? command : undefined}
|
onClick={typeof command === "function" ? command : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>{text}</FormListItem>
|
>{text}</FormListItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConvertToAttachment({ note }: { note: FNote }) {
|
function ConvertToAttachment({ note }: { note: FNote }) {
|
||||||
return (
|
return (
|
||||||
<FormListItem
|
<FormListItem
|
||||||
icon="bx bx-paperclip"
|
icon="bx bx-paperclip"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
|
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
|
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
|
||||||
|
|
||||||
if (!newAttachment) {
|
if (!newAttachment) {
|
||||||
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
|
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
||||||
viewScope: {
|
viewScope: {
|
||||||
viewMode: "attachments",
|
viewMode: "attachments",
|
||||||
attachmentId: newAttachment.attachmentId
|
attachmentId: newAttachment.attachmentId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,30 +8,13 @@ import { formatDateTime } from "../../utils/formatters";
|
|||||||
import { formatSize } from "../../services/utils";
|
import { formatSize } from "../../services/utils";
|
||||||
import LoadingSpinner from "../react/LoadingSpinner";
|
import LoadingSpinner from "../react/LoadingSpinner";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
|
||||||
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
|
|
||||||
export default function NoteInfoTab({ note }: TabContext) {
|
export default function NoteInfoTab({ note }: TabContext) {
|
||||||
const [ metadata, setMetadata ] = useState<MetadataResponse>();
|
const { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo } = useNoteMetadata(note);
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
|
||||||
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();
|
|
||||||
const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState<SubtreeSizeResponse>();
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
if (note) {
|
|
||||||
server.get<MetadataResponse>(`notes/${note?.noteId}/metadata`).then(setMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNoteSizeResponse(undefined);
|
|
||||||
setSubtreeSizeResponse(undefined);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(refresh, [ note?.noteId ]);
|
|
||||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
|
||||||
const noteId = note?.noteId;
|
|
||||||
if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="note-info-widget">
|
<div className="note-info-widget">
|
||||||
@ -41,14 +24,14 @@ export default function NoteInfoTab({ note }: TabContext) {
|
|||||||
<span>{t("note_info_widget.note_id")}:</span>
|
<span>{t("note_info_widget.note_id")}:</span>
|
||||||
<span className="note-info-id selectable-text">{note.noteId}</span>
|
<span className="note-info-id selectable-text">{note.noteId}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="note-info-item">
|
{!isNewLayout && <div className="note-info-item">
|
||||||
<span>{t("note_info_widget.created")}:</span>
|
<span>{t("note_info_widget.created")}:</span>
|
||||||
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
|
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
|
||||||
</div>
|
</div>}
|
||||||
<div className="note-info-item">
|
{!isNewLayout && <div className="note-info-item">
|
||||||
<span>{t("note_info_widget.modified")}:</span>
|
<span>{t("note_info_widget.modified")}:</span>
|
||||||
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
|
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
|
||||||
</div>
|
</div>}
|
||||||
<div className="note-info-item">
|
<div className="note-info-item">
|
||||||
<span>{t("note_info_widget.type")}:</span>
|
<span>{t("note_info_widget.type")}:</span>
|
||||||
<span>
|
<span>
|
||||||
@ -64,16 +47,7 @@ export default function NoteInfoTab({ note }: TabContext) {
|
|||||||
className="calculate-button"
|
className="calculate-button"
|
||||||
icon="bx bx-calculator"
|
icon="bx bx-calculator"
|
||||||
text={t("note_info_widget.calculate")}
|
text={t("note_info_widget.calculate")}
|
||||||
onClick={() => {
|
onClick={requestSizeInfo}
|
||||||
setIsLoading(true);
|
|
||||||
setTimeout(async () => {
|
|
||||||
await Promise.allSettled([
|
|
||||||
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
|
|
||||||
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
|
|
||||||
]);
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 0);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -90,5 +64,45 @@ export default function NoteInfoTab({ note }: TabContext) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNoteMetadata(note: FNote | null | undefined) {
|
||||||
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
|
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();
|
||||||
|
const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState<SubtreeSizeResponse>();
|
||||||
|
const [ metadata, setMetadata ] = useState<MetadataResponse>();
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (note) {
|
||||||
|
server.get<MetadataResponse>(`notes/${note?.noteId}/metadata`).then(setMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNoteSizeResponse(undefined);
|
||||||
|
setSubtreeSizeResponse(undefined);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSizeInfo() {
|
||||||
|
if (!note) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setTimeout(async () => {
|
||||||
|
await Promise.allSettled([
|
||||||
|
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
|
||||||
|
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
|
||||||
|
]);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(refresh, [ note ]);
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
const noteId = note?.noteId;
|
||||||
|
if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import "./style.css";
|
|||||||
|
|
||||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||||
import { EventNames } from "../../components/app_context";
|
import { EventNames } from "../../components/app_context";
|
||||||
import NoteActions from "./NoteActions";
|
|
||||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||||
|
|
||||||
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||||
|
|
||||||
@ -16,7 +16,9 @@ interface ComputedTab extends Indexed<TabConfiguration> {
|
|||||||
shouldShow: boolean;
|
shouldShow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Ribbon() {
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
|
|
||||||
|
export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
|
||||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
||||||
const noteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
||||||
@ -29,7 +31,8 @@ export default function Ribbon() {
|
|||||||
async function refresh() {
|
async function refresh() {
|
||||||
const computedTabs: ComputedTab[] = [];
|
const computedTabs: ComputedTab[] = [];
|
||||||
for (const tab of TAB_CONFIGURATION) {
|
for (const tab of TAB_CONFIGURATION) {
|
||||||
const shouldShow = await shouldShowTab(tab.show, titleContext);
|
const shouldAvoid = (isNewLayout && tab.avoidInNewLayout);
|
||||||
|
const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext);
|
||||||
computedTabs.push({
|
computedTabs.push({
|
||||||
...tab,
|
...tab,
|
||||||
shouldShow: !!shouldShow
|
shouldShow: !!shouldShow
|
||||||
@ -99,9 +102,7 @@ export default function Ribbon() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="ribbon-button-container">
|
{children}
|
||||||
{ note && <NoteActions note={note} noteContext={noteContext} /> }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ribbon-body-container">
|
<div className="ribbon-body-container">
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import FormattingToolbar from "./FormattingToolbar";
|
|||||||
import options from "../../services/options";
|
import options from "../../services/options";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { TabConfiguration } from "./ribbon-interface";
|
import { TabConfiguration } from "./ribbon-interface";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||||
|
|
||||||
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||||
{
|
{
|
||||||
@ -27,7 +28,8 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
|||||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||||
content: FormattingToolbar,
|
content: FormattingToolbar,
|
||||||
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
|
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
|
||||||
stayInDom: true
|
stayInDom: !isExperimentalFeatureEnabled("new-layout"),
|
||||||
|
avoidInNewLayout: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
|
title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
|
||||||
|
|||||||
@ -30,4 +30,5 @@ export interface TabConfiguration {
|
|||||||
* By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
|
* By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
|
||||||
*/
|
*/
|
||||||
stayInDom?: boolean;
|
stayInDom?: boolean;
|
||||||
|
avoidInNewLayout?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -415,3 +415,24 @@ body[dir=rtl] .attribute-list-editor {
|
|||||||
pointer-events: none; /* makes it unclickable */
|
pointer-events: none; /* makes it unclickable */
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #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);
|
||||||
|
|
||||||
|
.ribbon-tab-spacer,
|
||||||
|
.ribbon-tab-title,
|
||||||
|
.ribbon-body {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ribbon-button-container {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|||||||
@ -10,8 +10,23 @@ import RawHtml from "./react/RawHtml";
|
|||||||
|
|
||||||
export default function SharedInfo() {
|
export default function SharedInfo() {
|
||||||
const { note } = useNoteContext();
|
const { note } = useNoteContext();
|
||||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
const { isSharedExternally, link } = useShareInfo(note);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
|
||||||
|
{link && (
|
||||||
|
<RawHtml html={isSharedExternally
|
||||||
|
? t("shared_info.shared_publicly", { link })
|
||||||
|
: t("shared_info.shared_locally", { link })} />
|
||||||
|
)}
|
||||||
|
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
|
||||||
|
</InfoBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShareInfo(note: FNote | null | undefined) {
|
||||||
const [ link, setLink ] = useState<string>();
|
const [ link, setLink ] = useState<string>();
|
||||||
|
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
@ -48,16 +63,7 @@ export default function SharedInfo() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return { link, isSharedExternally: !!syncServerHost };
|
||||||
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
|
|
||||||
{link && (
|
|
||||||
<RawHtml html={syncServerHost
|
|
||||||
? t("shared_info.shared_publicly", { link })
|
|
||||||
: t("shared_info.shared_locally", { link })} />
|
|
||||||
)}
|
|
||||||
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
|
|
||||||
</InfoBar>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShareId(note: FNote) {
|
function getShareId(note: FNote) {
|
||||||
@ -66,4 +72,4 @@ function getShareId(note: FNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return note.getOwnedLabelValue("shareAlias") || note.noteId;
|
return note.getOwnedLabelValue("shareAlias") || note.noteId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import FormText from "../../react/FormText";
|
|||||||
import OptionsSection from "./components/OptionsSection"
|
import OptionsSection from "./components/OptionsSection"
|
||||||
import Column from "../../react/Column";
|
import Column from "../../react/Column";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import CheckboxList from "./components/CheckboxList";
|
||||||
|
import { experimentalFeatures } from "../../../services/experimental_features";
|
||||||
|
import { useTriliumOptionJson } from "../../react/hooks";
|
||||||
|
|
||||||
export default function AdvancedSettings() {
|
export default function AdvancedSettings() {
|
||||||
return <>
|
return <>
|
||||||
@ -14,6 +17,7 @@ export default function AdvancedSettings() {
|
|||||||
<DatabaseIntegrityOptions />
|
<DatabaseIntegrityOptions />
|
||||||
<DatabaseAnonymizationOptions />
|
<DatabaseAnonymizationOptions />
|
||||||
<VacuumDatabaseOptions />
|
<VacuumDatabaseOptions />
|
||||||
|
<ExperimentalOptions />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,14 +48,14 @@ function DatabaseIntegrityOptions() {
|
|||||||
return (
|
return (
|
||||||
<OptionsSection title={t("database_integrity_check.title")}>
|
<OptionsSection title={t("database_integrity_check.title")}>
|
||||||
<FormText>{t("database_integrity_check.description")}</FormText>
|
<FormText>{t("database_integrity_check.description")}</FormText>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
text={t("database_integrity_check.check_button")}
|
text={t("database_integrity_check.check_button")}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
toast.showMessage(t("database_integrity_check.checking_integrity"));
|
toast.showMessage(t("database_integrity_check.checking_integrity"));
|
||||||
|
|
||||||
const { results } = await server.get<DatabaseCheckIntegrityResponse>("database/check-integrity");
|
const { results } = await server.get<DatabaseCheckIntegrityResponse>("database/check-integrity");
|
||||||
|
|
||||||
if (results.length === 1 && results[0].integrity_check === "ok") {
|
if (results.length === 1 && results[0].integrity_check === "ok") {
|
||||||
toast.showMessage(t("database_integrity_check.integrity_check_succeeded"));
|
toast.showMessage(t("database_integrity_check.integrity_check_succeeded"));
|
||||||
} else {
|
} else {
|
||||||
@ -93,7 +97,7 @@ function DatabaseAnonymizationOptions() {
|
|||||||
buttonClick={async () => {
|
buttonClick={async () => {
|
||||||
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
||||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
|
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||||
} else {
|
} else {
|
||||||
@ -141,7 +145,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
|
|||||||
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
|
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="table table-stripped">
|
<table className="table table-stripped">
|
||||||
<thead>
|
<thead>
|
||||||
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
|
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
|
||||||
@ -172,4 +176,22 @@ function VacuumDatabaseOptions() {
|
|||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExperimentalOptions() {
|
||||||
|
const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson<string[]>("experimentalFeatures");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OptionsSection title={t("experimental_features.title")}>
|
||||||
|
<FormText>{t("experimental_features.disclaimer")}</FormText>
|
||||||
|
|
||||||
|
<CheckboxList
|
||||||
|
values={experimentalFeatures}
|
||||||
|
keyProperty="id"
|
||||||
|
titleProperty="name"
|
||||||
|
descriptionProperty="description"
|
||||||
|
currentValue={enabledExperimentalFeatures} onChange={setEnabledExperimentalFeatures}
|
||||||
|
/>
|
||||||
|
</OptionsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
|
import FormCheckbox from "../../../react/FormCheckbox";
|
||||||
|
|
||||||
interface CheckboxListProps<T> {
|
interface CheckboxListProps<T> {
|
||||||
values: T[];
|
values: T[];
|
||||||
keyProperty: keyof T;
|
keyProperty: keyof T;
|
||||||
titleProperty?: keyof T;
|
titleProperty?: keyof T;
|
||||||
disabledProperty?: keyof T;
|
disabledProperty?: keyof T;
|
||||||
|
descriptionProperty?: keyof T;
|
||||||
currentValue: string[];
|
currentValue: string[];
|
||||||
onChange: (newValues: string[]) => void;
|
onChange: (newValues: string[]) => void;
|
||||||
columnWidth?: string;
|
columnWidth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, descriptionProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||||
function toggleValue(value: string) {
|
function toggleValue(value: string) {
|
||||||
if (currentValue.includes(value)) {
|
if (currentValue.includes(value)) {
|
||||||
// Already there, needs removing.
|
// Already there, needs removing.
|
||||||
@ -22,20 +25,17 @@ export default function CheckboxList<T>({ values, keyProperty, titleProperty, di
|
|||||||
return (
|
return (
|
||||||
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
|
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
|
||||||
{values.map(value => (
|
{values.map(value => (
|
||||||
<li>
|
<li key={String(value[keyProperty])}>
|
||||||
<label className="tn-checkbox">
|
<FormCheckbox
|
||||||
<input
|
label={String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||||
type="checkbox"
|
name={String(value[keyProperty])}
|
||||||
className="form-check-input"
|
currentValue={currentValue.includes(String(value[keyProperty]))}
|
||||||
value={String(value[keyProperty])}
|
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||||
checked={currentValue.includes(String(value[keyProperty]))}
|
hint={value && (descriptionProperty ? String(value[descriptionProperty]) : undefined)}
|
||||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
onChange={() => toggleValue(String(value[keyProperty]))}
|
||||||
onChange={e => toggleValue((e.target as HTMLInputElement).value)}
|
/>
|
||||||
/>
|
|
||||||
{String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
|
||||||
</label>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
|||||||
"showLoginInShareTheme",
|
"showLoginInShareTheme",
|
||||||
"splitEditorOrientation",
|
"splitEditorOrientation",
|
||||||
"seenCallToActions",
|
"seenCallToActions",
|
||||||
|
"experimentalFeatures",
|
||||||
|
|
||||||
// AI/LLM integration options
|
// AI/LLM integration options
|
||||||
"aiEnabled",
|
"aiEnabled",
|
||||||
|
|||||||
@ -215,7 +215,8 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
{ name: "aiSystemPrompt", value: "", isSynced: true },
|
{ name: "aiSystemPrompt", value: "", isSynced: true },
|
||||||
{ name: "aiSelectedProvider", value: "openai", isSynced: true },
|
{ name: "aiSelectedProvider", value: "openai", isSynced: true },
|
||||||
|
|
||||||
{ name: "seenCallToActions", value: "[]", isSynced: true }
|
{ name: "seenCallToActions", value: "[]", isSynced: true },
|
||||||
|
{ name: "experimentalFeatures", value: "[]", isSynced: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -155,6 +155,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
|||||||
codeOpenAiModel: string;
|
codeOpenAiModel: string;
|
||||||
aiSelectedProvider: string;
|
aiSelectedProvider: string;
|
||||||
seenCallToActions: string;
|
seenCallToActions: string;
|
||||||
|
experimentalFeatures: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OptionNames = keyof OptionDefinitions;
|
export type OptionNames = keyof OptionDefinitions;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user