New layout: Integrate Basic properties (#8014)
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:
Elian Doran 2025-12-10 23:37:54 +02:00 committed by GitHub
commit 19b32dd3a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 499 additions and 209 deletions

View File

@ -52,6 +52,7 @@ import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx"; import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
import NoteStatusBar from "../widgets/NoteStatusBar.jsx";
export default class DesktopLayout { export default class DesktopLayout {
@ -176,7 +177,11 @@ 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 />) .optChild(isNewLayout, (
<Ribbon>
<NoteStatusBar />
</Ribbon>
))
) )
) )
.child(...this.customWidgets.get("center-pane")) .child(...this.customWidgets.get("center-pane"))

View File

@ -27,6 +27,16 @@ export function getEnabledExperimentalFeatureIds() {
return getEnabledFeatures().values(); return getEnabledFeatures().values();
} }
export async function toggleExperimentalFeature(featureId: ExperimentalFeatureId, enable: boolean) {
const features = new Set(getEnabledFeatures());
if (enable) {
features.add(featureId);
} else {
features.delete(featureId);
}
await options.save("experimentalFeatures", JSON.stringify(Array.from(features)));
}
function getEnabledFeatures() { function getEnabledFeatures() {
if (!enabledFeatures) { if (!enabledFeatures) {
let features: ExperimentalFeatureId[] = []; let features: ExperimentalFeatureId[] = [];

View File

@ -423,16 +423,16 @@ body.desktop .tabulator-popup-container,
pointer-events: none; pointer-events: none;
} }
.dropdown-menu .disabled .disabled-tooltip { .dropdown-menu .disabled .contextual-help {
pointer-events: all; pointer-events: all;
margin-inline-start: 8px; margin-inline-start: 8px;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--disabled-tooltip-icon-color); color: var(--contextual-help-icon-color);
cursor: help; cursor: help;
opacity: 0.75; opacity: 0.75;
} }
.dropdown-menu .disabled .disabled-tooltip:hover { .dropdown-menu .disabled .contextual-help:hover {
opacity: 1; opacity: 1;
} }
@ -1315,7 +1315,8 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
top: 0; top: 0;
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */ inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
margin-top: -10px; margin-top: -10px;
min-width: 15rem; min-width: max-content;
max-width: 300px;
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */ /* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
max-height: 600px; max-height: 600px;
overflow: auto; overflow: auto;

View File

@ -19,7 +19,7 @@
--dropdown-border-color: #555; --dropdown-border-color: #555;
--dropdown-shadow-opacity: 0.4; --dropdown-shadow-opacity: 0.4;
--dropdown-item-icon-destructive-color: #de6e5b; --dropdown-item-icon-destructive-color: #de6e5b;
--disabled-tooltip-icon-color: #7fd2ef; --contextual-help-icon-color: #7fd2ef;
--accented-background-color: #555; --accented-background-color: #555;
--more-accented-background-color: #777; --more-accented-background-color: #777;
@ -114,4 +114,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.use-note-color { .use-note-color {
--custom-color: var(--dark-theme-custom-color); --custom-color: var(--dark-theme-custom-color);
} }

View File

@ -23,7 +23,7 @@ html {
--dropdown-border-color: #ccc; --dropdown-border-color: #ccc;
--dropdown-shadow-opacity: 0.2; --dropdown-shadow-opacity: 0.2;
--dropdown-item-icon-destructive-color: #ec5138; --dropdown-item-icon-destructive-color: #ec5138;
--disabled-tooltip-icon-color: #004382; --contextual-help-icon-color: #004382;
--accented-background-color: #f5f5f5; --accented-background-color: #f5f5f5;
--more-accented-background-color: #ddd; --more-accented-background-color: #ddd;
@ -98,4 +98,4 @@ html {
.use-note-color { .use-note-color {
--custom-color: var(--light-theme-custom-color); --custom-color: var(--light-theme-custom-color);
} }

View File

@ -6,7 +6,7 @@
*/ */
:root { :root {
/* /*
* NOTICE: This theme is currently in the beta stage of development. * NOTICE: This theme is currently in the beta stage of development.
* The names and purposes of these CSS variables are subject to frequent changes. * The names and purposes of these CSS variables are subject to frequent changes.
*/ */
@ -22,7 +22,7 @@
--dropdown-border-color: #404040; --dropdown-border-color: #404040;
--dropdown-shadow-opacity: 0.6; --dropdown-shadow-opacity: 0.6;
--dropdown-item-icon-destructive-color: #de6e5b; --dropdown-item-icon-destructive-color: #de6e5b;
--disabled-tooltip-icon-color: #7fd2ef; --contextual-help-icon-color: #7fd2ef;
--accented-background-color: #555; --accented-background-color: #555;
@ -182,7 +182,7 @@
--tab-close-button-hover-background: #a45353; --tab-close-button-hover-background: #a45353;
--tab-close-button-hover-color: white; --tab-close-button-hover-color: white;
--active-tab-background-color: #ffffff1c; --active-tab-background-color: #ffffff1c;
--active-tab-hover-background-color: var(--active-tab-background-color); --active-tab-hover-background-color: var(--active-tab-background-color);
--active-tab-icon-color: #a9a9a9; --active-tab-icon-color: #a9a9a9;
@ -201,7 +201,7 @@
--promoted-attribute-card-background-color: #ffffff21; --promoted-attribute-card-background-color: #ffffff21;
--promoted-attribute-card-shadow: none; --promoted-attribute-card-shadow: none;
--floating-button-shadow-color: #00000080; --floating-button-shadow-color: #00000080;
--floating-button-background-color: #494949d2; --floating-button-background-color: #494949d2;
--floating-button-color: var(--button-text-color); --floating-button-color: var(--button-text-color);
@ -226,7 +226,7 @@
--scrollbar-border-color: unset; /* Deprecated */ --scrollbar-border-color: unset; /* Deprecated */
--selection-background-color: #3399FF70; --selection-background-color: #3399FF70;
--link-color: lightskyblue; --link-color: lightskyblue;
--mermaid-theme: dark; --mermaid-theme: dark;
@ -320,4 +320,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.use-note-color { .use-note-color {
--custom-color: var(--dark-theme-custom-color); --custom-color: var(--dark-theme-custom-color);
} }

View File

@ -6,7 +6,7 @@
*/ */
:root { :root {
/* /*
* NOTICE: This theme is currently in the beta stage of development. * NOTICE: This theme is currently in the beta stage of development.
* The names and purposes of these CSS variables are subject to frequent changes. * The names and purposes of these CSS variables are subject to frequent changes.
*/ */
@ -22,7 +22,7 @@
--dropdown-border-color: #ccc; --dropdown-border-color: #ccc;
--dropdown-shadow-opacity: 0.2; --dropdown-shadow-opacity: 0.2;
--dropdown-item-icon-destructive-color: #ec5138; --dropdown-item-icon-destructive-color: #ec5138;
--disabled-tooltip-icon-color: #004382; --contextual-help-icon-color: #004382;
--accented-background-color: #f5f5f5; --accented-background-color: #f5f5f5;
@ -138,7 +138,7 @@
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */ /* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
--launcher-pane-background-color: unset; --launcher-pane-background-color: unset;
--launcher-pane-text-color: unset; --launcher-pane-text-color: unset;
--launcher-pane-vert-background-color: #e8e8e8; --launcher-pane-vert-background-color: #e8e8e8;
--launcher-pane-vert-text-color: #000000bd; --launcher-pane-vert-text-color: #000000bd;
--launcher-pane-vert-button-hover-color: black; --launcher-pane-vert-button-hover-color: black;
@ -174,7 +174,7 @@
--tab-close-button-hover-background: #c95a5a; --tab-close-button-hover-background: #c95a5a;
--tab-close-button-hover-color: white; --tab-close-button-hover-color: white;
--active-tab-background-color: white; --active-tab-background-color: white;
--active-tab-hover-background-color: var(--active-tab-background-color); --active-tab-hover-background-color: var(--active-tab-background-color);
--active-tab-icon-color: gray; --active-tab-icon-color: gray;
@ -291,4 +291,4 @@
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%); --modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%); --modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%); --promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
} }

View File

@ -1962,8 +1962,9 @@
"unknown_widget": "Unknown widget for \"{{id}}\"." "unknown_widget": "Unknown widget for \"{{id}}\"."
}, },
"note_language": { "note_language": {
"not_set": "Not set", "not_set": "No language set",
"configure-languages": "Configure languages..." "configure-languages": "Configure languages...",
"help-on-languages": "Help on content languages..."
}, },
"content_language": { "content_language": {
"title": "Content languages", "title": "Content languages",

View File

@ -52,7 +52,7 @@ function ShareBadge() {
return (link && return (link &&
<Badge <Badge
icon={isSharedExternally ? "bx bx-world" : "bx bx-link"} icon={isSharedExternally ? "bx bx-world" : "bx bx-share-alt"}
text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")} text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")}
tooltip={isSharedExternally ? tooltip={isSharedExternally ?
t("breadcrumb_badges.shared_publicly_description", { link }) : t("breadcrumb_badges.shared_publicly_description", { link }) :

View File

@ -0,0 +1,13 @@
.note-status-bar {
display: flex;
align-items: center;
padding-inline: 1em;
.dropdown {
font-size: 0.85em;
.dropdown-toggle {
padding: 0.1em 0.25em;
}
}
}

View File

@ -0,0 +1,25 @@
import "./NoteStatusBar.css";
import { t } from "../services/i18n";
import { openInAppHelpFromUrl } from "../services/utils";
import { FormListItem } from "./react/FormList";
import { useNoteContext } from "./react/hooks";
import { NoteLanguageSelector } from "./ribbon/BasicPropertiesTab";
export default function NoteStatusBar() {
const { note } = useNoteContext();
return (
<div className="note-status-bar">
<NoteLanguageSelector
note={note}
extraChildren={(
<FormListItem
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
icon="bx bx-help-circle"
>{t("note_language.help-on-languages")}</FormListItem>
)}
/>
</div>
);
}

View File

@ -10,7 +10,8 @@ import { KeyboardActionNames } from "@triliumnext/commons";
import { ComponentChildren } from "preact"; import { ComponentChildren } from "preact";
import Component from "../../components/component"; import Component from "../../components/component";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils"; import utils, { dynamicRequire, isElectron, isMobile, reloadFrontendApp } from "../../services/utils";
import { isExperimentalFeatureEnabled, toggleExperimentalFeature } from "../../services/experimental_features";
interface MenuItemProps<T> { interface MenuItemProps<T> {
icon: string, icon: string,
@ -70,6 +71,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
</>} </>}
{!isElectron() && <BrowserOnlyOptions />} {!isElectron() && <BrowserOnlyOptions />}
{glob.isDev && <DevelopmentOptions />}
</Dropdown> </Dropdown>
) )
} }
@ -99,6 +101,21 @@ function BrowserOnlyOptions() {
</>; </>;
} }
function DevelopmentOptions() {
const newLayoutEnabled = isExperimentalFeatureEnabled("new-layout");
return <>
<FormDropdownDivider />
<FormListItem
icon={newLayoutEnabled ? "bx bx-layout" : "bx bxs-layout"}
onClick={async () => {
await toggleExperimentalFeature("new-layout", !newLayoutEnabled);
reloadFrontendApp();
}}
>{!newLayoutEnabled ? "Switch to new layout" : "Switch to old layout"}</FormListItem>
</>;
}
function SwitchToOptions() { function SwitchToOptions() {
if (isElectron()) { if (isElectron()) {
return; return;

View File

@ -1,9 +1,29 @@
.dropdown-item .description { .dropdown-item {
font-size: small; .description {
color: var(--muted-text-color); font-size: small;
white-space: normal; color: var(--muted-text-color);
} white-space: normal;
}
.dropdown-item span.bx { span.bx {
flex-shrink: 0; flex-shrink: 0;
} }
.switch-widget {
flex-grow: 1;
width: 100%;
--switch-track-width: 40px;
--switch-track-height: 20px;
--switch-thumb-width: 12px;
--switch-thumb-height: var(--switch-thumb-width);
.contextual-help {
margin-inline-start: 0.25em;
cursor: pointer;
}
.switch-spacer {
flex-grow: 1;
}
}
}

View File

@ -5,8 +5,9 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact
import "./FormList.css"; import "./FormList.css";
import { CommandNames } from "../../components/app_context"; import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks"; import { useStaticTooltip } from "./hooks";
import { handleRightToLeftPlacement, isMobile } from "../../services/utils"; import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
import clsx from "clsx"; import clsx from "clsx";
import FormToggle from "./FormToggle";
interface FormListOpts { interface FormListOpts {
children: ComponentChildren; children: ComponentChildren;
@ -94,12 +95,13 @@ interface FormListItemOpts {
description?: string; description?: string;
className?: string; className?: string;
rtl?: boolean; rtl?: boolean;
postContent?: ComponentChildren;
} }
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = { const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
placement: handleRightToLeftPlacement("right"), placement: handleRightToLeftPlacement("right"),
fallbackPlacements: [ handleRightToLeftPlacement("right") ] fallbackPlacements: [ handleRightToLeftPlacement("right") ]
} };
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) { export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
const itemRef = useRef<HTMLLIElement>(null); const itemRef = useRef<HTMLLIElement>(null);
@ -132,6 +134,49 @@ export function FormListItem({ className, icon, value, title, active, disabled,
); );
} }
export function FormListToggleableItem({ title, currentValue, onChange, disabled, helpPage, ...props }: Omit<FormListItemOpts, "onClick" | "children"> & {
title: string;
currentValue: boolean;
helpPage?: string;
onChange(newValue: boolean): void | Promise<void>;
}) {
const isWaiting = useRef(false);
return (
<FormListItem
{...props}
disabled={disabled}
onClick={async (e) => {
if ((e.target as HTMLElement | null)?.classList.contains("contextual-help")) {
return;
}
e.stopPropagation();
if (!disabled && !isWaiting.current) {
isWaiting.current = true;
await onChange(!currentValue);
isWaiting.current = false;
}
}}>
<FormToggle
switchOnName={title}
switchOffName={title}
currentValue={currentValue}
onChange={() => {}}
afterName={<>
{helpPage && (
<span
class="bx bx-help-circle contextual-help"
onClick={() => openInAppHelpFromUrl(helpPage)}
/>
)}
<span class="switch-spacer" />
</>}
/>
</FormListItem>
);
}
function FormListContent({ children, badges, description, disabled, disabledTooltip }: Pick<FormListItemOpts, "children" | "badges" | "description" | "disabled" | "disabledTooltip">) { function FormListContent({ children, badges, description, disabled, disabledTooltip }: Pick<FormListItemOpts, "children" | "badges" | "description" | "disabled" | "disabledTooltip">) {
return <> return <>
{children} {children}
@ -139,7 +184,7 @@ function FormListContent({ children, badges, description, disabled, disabledTool
<span className={`badge ${className ?? ""}`}>{text}</span> <span className={`badge ${className ?? ""}`}>{text}</span>
))} ))}
{disabled && disabledTooltip && ( {disabled && disabledTooltip && (
<span class="bx bx-info-circle disabled-tooltip" title={disabledTooltip} /> <span class="bx bx-info-circle contextual-help" title={disabledTooltip} />
)} )}
{description && <div className="description">{description}</div>} {description && <div className="description">{description}</div>}
</>; </>;

View File

@ -24,6 +24,14 @@
border-radius: 24px; border-radius: 24px;
background-color: var(--switch-off-track-background); background-color: var(--switch-off-track-background);
transition: background 200ms ease-in; transition: background 200ms ease-in;
&.disable-transitions {
transition: none !important;
&:after {
transition: none !important;
}
}
} }
.switch-widget .switch-button.on { .switch-widget .switch-button.on {
@ -103,4 +111,4 @@ body[dir=rtl] .switch-widget .switch-button.on:after {
.switch-widget .switch-help-button:hover { .switch-widget .switch-help-button:hover {
color: var(--main-text-color); color: var(--main-text-color);
} }

View File

@ -1,25 +1,39 @@
import clsx from "clsx";
import "./FormToggle.css"; import "./FormToggle.css";
import HelpButton from "./HelpButton"; import HelpButton from "./HelpButton";
import { useEffect, useState } from "preact/hooks";
import { ComponentChildren } from "preact";
interface FormToggleProps { interface FormToggleProps {
currentValue: boolean | null; currentValue: boolean | null;
onChange(newValue: boolean): void; onChange(newValue: boolean): void;
switchOnName: string; switchOnName: string;
switchOnTooltip: string; switchOnTooltip?: string;
switchOffName: string; switchOffName: string;
switchOffTooltip: string; switchOffTooltip?: string;
helpPage?: string; helpPage?: string;
disabled?: boolean; disabled?: boolean;
afterName?: ComponentChildren;
} }
export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled }: FormToggleProps) { export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled, afterName }: FormToggleProps) {
const [ disableTransition, setDisableTransition ] = useState(true);
useEffect(() => {
const timeout = setTimeout(() => {
setDisableTransition(false);
}, 100);
return () => clearTimeout(timeout);
}, []);
return ( return (
<div className="switch-widget"> <div className="switch-widget">
<span className="switch-name">{ currentValue ? switchOffName : switchOnName }</span> <span className="switch-name">{ currentValue ? switchOffName : switchOnName }</span>
{ afterName }
<label> <label>
<div <div
className={`switch-button ${currentValue ? "on" : ""} ${disabled ? "disabled" : ""}`} className={clsx("switch-button", { "on": currentValue, disabled, "disable-transitions": disableTransition })}
title={currentValue ? switchOffTooltip : switchOnTooltip } title={currentValue ? switchOffTooltip : switchOnTooltip }
> >
<input <input
@ -37,5 +51,5 @@ export default function FormToggle({ currentValue, helpPage, switchOnName, switc
{ helpPage && <HelpButton className="switch-help-button" helpPage={helpPage} />} { helpPage && <HelpButton className="switch-help-button" helpPage={helpPage} />}
</div> </div>
) );
} }

View File

@ -845,6 +845,8 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h
*/ */
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) { export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined); const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly");
const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
const enableEditing = useCallback((enabled = true) => { const enableEditing = useCallback((enabled = true) => {
if (noteContext?.viewScope) { if (noteContext?.viewScope) {
@ -859,7 +861,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
setIsReadOnly(readOnly); setIsReadOnly(readOnly);
}); });
} }
}, [ note, noteContext, noteContext?.viewScope ]); }, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => { useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) { if (noteContext?.ntxId === eventNoteContext.ntxId) {

View File

@ -1,26 +1,29 @@
import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; import { NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import Dropdown from "../react/Dropdown"; import { ComponentChildren } from "preact";
import { NOTE_TYPES } from "../../services/note_types"; import { createPortal } from "preact/compat";
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList"; import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { getAvailableLocales, t } from "../../services/i18n";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import mime_types from "../../services/mime_types";
import { Locale, LOCALES, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import server from "../../services/server";
import dialog from "../../services/dialog";
import FormToggle from "../react/FormToggle";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import protected_session from "../../services/protected_session";
import FormDropdownList from "../react/FormDropdownList";
import toast from "../../services/toast";
import branches from "../../services/branches"; import branches from "../../services/branches";
import dialog from "../../services/dialog";
import { getAvailableLocales, t } from "../../services/i18n";
import mime_types from "../../services/mime_types";
import { NOTE_TYPES } from "../../services/note_types";
import protected_session from "../../services/protected_session";
import server from "../../services/server";
import sync from "../../services/sync"; import sync from "../../services/sync";
import toast from "../../services/toast";
import Dropdown from "../react/Dropdown";
import FormDropdownList from "../react/FormDropdownList";
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import HelpButton from "../react/HelpButton"; import HelpButton from "../react/HelpButton";
import { TabContext } from "./ribbon-interface"; import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import Modal from "../react/Modal"; import Modal from "../react/Modal";
import { CodeMimeTypesList } from "../type_widgets/options/code_notes"; import { CodeMimeTypesList } from "../type_widgets/options/code_notes";
import { ContentLanguagesList } from "../type_widgets/options/i18n";
import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector"; import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector";
import { ContentLanguagesList } from "../type_widgets/options/i18n";
import { TabContext } from "./ribbon-interface";
export default function BasicPropertiesTab({ note }: TabContext) { export default function BasicPropertiesTab({ note }: TabContext) {
return ( return (
@ -37,18 +40,38 @@ export default function BasicPropertiesTab({ note }: TabContext) {
} }
function NoteTypeWidget({ note }: { note?: FNote | null }) { function NoteTypeWidget({ note }: { note?: FNote | null }) {
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
const mimeTypes = useMemo(() => {
mime_types.loadMimeTypes();
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled)
}, [ codeNotesMimeTypes ]);
const notSelectableNoteTypes = useMemo(() => NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type), []); const notSelectableNoteTypes = useMemo(() => NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type), []);
const currentNoteType = useNoteProperty(note, "type") ?? undefined; const currentNoteType = useNoteProperty(note, "type") ?? undefined;
const currentNoteMime = useNoteProperty(note, "mime"); const currentNoteMime = useNoteProperty(note, "mime");
const [ modalShown, setModalShown ] = useState(false); const [ modalShown, setModalShown ] = useState(false);
return (
<div className="note-type-container">
<span>{t("basic_properties.note_type")}:</span> &nbsp;
<Dropdown
dropdownContainerClassName="note-type-dropdown"
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
>
<NoteTypeDropdownContent currentNoteType={currentNoteType} currentNoteMime={currentNoteMime} note={note} setModalShown={setModalShown} />
</Dropdown>
{createPortal(
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</div>
);
}
export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note, setModalShown }: { currentNoteType?: NoteType, currentNoteMime?: string | null, note?: FNote | null, setModalShown: Dispatch<StateUpdater<boolean>> }) {
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
const mimeTypes = useMemo(() => {
mime_types.loadMimeTypes();
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled);
}, [ codeNotesMimeTypes ]);
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => { const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
if (!note || (type === currentNoteType && mime === currentNoteMime)) { if (!note || (type === currentNoteType && mime === currentNoteMime)) {
return; return;
@ -68,71 +91,68 @@ function NoteTypeWidget({ note }: { note?: FNote | null }) {
}, [ note, currentNoteType, currentNoteMime ]); }, [ note, currentNoteType, currentNoteMime ]);
return ( return (
<div className="note-type-container"> <>
<span>{t("basic_properties.note_type")}:</span> &nbsp; {noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
<Dropdown const badges: FormListBadge[] = [];
dropdownContainerClassName="note-type-dropdown" if (isNew) {
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>} badges.push({
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")} className: "new-note-type-badge",
> text: t("note_types.new-feature")
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => { });
const badges: FormListBadge[] = []; }
if (isNew) { if (isBeta) {
badges.push({ badges.push({
className: "new-note-type-badge", text: t("note_types.beta-feature")
text: t("note_types.new-feature") });
}); }
}
if (isBeta) {
badges.push({
text: t("note_types.beta-feature")
});
}
const checked = (type === currentNoteType); const checked = (type === currentNoteType);
if (type !== "code") { if (type !== "code") {
return ( return (
<FormListItem
checked={checked}
badges={badges}
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
<FormListItem <FormListItem
checked={checked} checked={checked}
badges={badges} disabled
onClick={() => changeNoteType(type, mime)} >
>{title}</FormListItem> <strong>{title}</strong>
); </FormListItem>
} else { </>
return ( );
<> }
<FormDropdownDivider /> })}
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
)
}
})}
{mimeTypes.map(({ title, mime }) => ( {mimeTypes.map(({ title, mime }) => (
<FormListItem onClick={() => changeNoteType("code", mime)}> <FormListItem onClick={() => changeNoteType("code", mime)}>
{title} {title}
</FormListItem> </FormListItem>
))} ))}
<FormDropdownDivider /> <FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem> <FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</Dropdown> </>
);
}
<Modal function NoteTypeOptionsModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
className="code-mime-types-modal" return (
title={t("code_mime_types.title")} <Modal
show={modalShown} onHidden={() => setModalShown(false)} className="code-mime-types-modal"
size="xl" scrollable title={t("code_mime_types.title")}
> show={modalShown} onHidden={() => setModalShown(false)}
<CodeMimeTypesList /> size="xl" scrollable
</Modal> >
</div> <CodeMimeTypesList />
) </Modal>
);
} }
function ProtectedNoteSwitch({ note }: { note?: FNote | null }) { function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
@ -187,22 +207,11 @@ function EditabilitySelect({ note }: { note?: FNote | null }) {
}} }}
/> />
</div> </div>
) );
} }
function BookmarkSwitch({ note }: { note?: FNote | null }) { function BookmarkSwitch({ note }: { note?: FNote | null }) {
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false); const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
const refreshState = useCallback(() => {
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
setIsBookmarked(!!isBookmarked);
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
});
return ( return (
<div className="bookmark-switch-container"> <div className="bookmark-switch-container">
@ -210,18 +219,36 @@ function BookmarkSwitch({ note }: { note?: FNote | null }) {
switchOnName={t("bookmark_switch.bookmark")} switchOnTooltip={t("bookmark_switch.bookmark_this_note")} switchOnName={t("bookmark_switch.bookmark")} switchOnTooltip={t("bookmark_switch.bookmark_this_note")}
switchOffName={t("bookmark_switch.bookmark")} switchOffTooltip={t("bookmark_switch.remove_bookmark")} switchOffName={t("bookmark_switch.bookmark")} switchOffTooltip={t("bookmark_switch.remove_bookmark")}
currentValue={isBookmarked} currentValue={isBookmarked}
onChange={async (shouldBookmark) => { onChange={setIsBookmarked}
if (!note) return;
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
if (!resp.success && "message" in resp) {
toast.showError(resp.message);
}
}}
disabled={["root", "_hidden"].includes(note?.noteId ?? "")} disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
/> />
</div> </div>
) );
}
export function useNoteBookmarkState(note: FNote | null | undefined) {
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
const refreshState = useCallback(() => {
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
setIsBookmarked(!!isBookmarked);
}, [ note ]);
const changeHandler = useCallback(async (shouldBookmark: boolean) => {
if (!note) return;
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
if (!resp.success && "message" in resp) {
toast.showError(resp.message);
}
}, [ note ]);
useEffect(() => refreshState(), [ refreshState ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
});
return [ isBookmarked, changeHandler ] as const;
} }
function TemplateSwitch({ note }: { note?: FNote | null }) { function TemplateSwitch({ note }: { note?: FNote | null }) {
@ -237,16 +264,33 @@ function TemplateSwitch({ note }: { note?: FNote | null }) {
currentValue={isTemplate} onChange={setIsTemplate} currentValue={isTemplate} onChange={setIsTemplate}
/> />
</div> </div>
) );
} }
function SharedSwitch({ note }: { note?: FNote | null }) { function SharedSwitch({ note }: { note?: FNote | null }) {
const [ isShared, switchShareState ] = useShareState(note);
return (
<div className="shared-switch-container">
<FormToggle
currentValue={isShared}
onChange={switchShareState}
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
</div>
);
}
export function useShareState(note: FNote | null | undefined) {
const [ isShared, setIsShared ] = useState(false); const [ isShared, setIsShared ] = useState(false);
const refreshState = useCallback(() => { const refreshState = useCallback(() => {
setIsShared(!!note?.hasAncestor("_share")); setIsShared(!!note?.hasAncestor("_share"));
}, [ note ]); }, [ note ]);
useEffect(() => refreshState(), [ note ]); useEffect(() => refreshState(), [ refreshState ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => { useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) { if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState(); refreshState();
@ -271,28 +315,29 @@ function SharedSwitch({ note }: { note?: FNote | null }) {
sync.syncNow(true); sync.syncNow(true);
}, [ note ]); }, [ note ]);
return ( return [ isShared, switchShareState ] as const;
<div className="shared-switch-container">
<FormToggle
currentValue={isShared}
onChange={switchShareState}
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
</div>
)
} }
function NoteLanguageSwitch({ note }: { note?: FNote | null }) { function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
return (
<div className="note-language-container">
<span>{t("basic_properties.language")}:</span>
&nbsp;
<NoteLanguageSelector note={note} />
<HelpButton helpPage="veGu4faJErEM" style={{ marginInlineStart: "4px" }} />
</div>
);
}
export function NoteLanguageSelector({ note, extraChildren }: { note: FNote | null | undefined, extraChildren?: ComponentChildren }) {
const [ modalShown, setModalShown ] = useState(false);
const [ languages ] = useTriliumOption("languages"); const [ languages ] = useTriliumOption("languages");
const DEFAULT_LOCALE = { const DEFAULT_LOCALE = {
id: "", id: "",
name: t("note_language.not_set") name: t("note_language.not_set")
}; };
const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language"); const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language");
const [ modalShown, setModalShown ] = useState(false);
const locales = useMemo(() => { const locales = useMemo(() => {
const enabledLanguages = JSON.parse(languages ?? "[]") as string[]; const enabledLanguages = JSON.parse(languages ?? "[]") as string[];
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id)); const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
@ -300,34 +345,37 @@ function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
}, [ languages ]); }, [ languages ]);
return ( return (
<div className="note-language-container"> <>
<span>{t("basic_properties.language")}:</span>
&nbsp;
<LocaleSelector <LocaleSelector
locales={locales} locales={locales}
defaultLocale={DEFAULT_LOCALE} defaultLocale={DEFAULT_LOCALE}
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage} currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
extraChildren={( extraChildren={<>
{extraChildren}
<FormListItem <FormListItem
onClick={() => setModalShown(true)} onClick={() => setModalShown(true)}
icon="bx bx-cog" icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem> >{t("note_language.configure-languages")}</FormListItem>
)} </>}
> />
{createPortal(
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
</LocaleSelector> function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
return (
<HelpButton helpPage="B0lcI9xz1r8K" style={{ marginInlineStart: "4px" }} /> <Modal
className="content-languages-modal"
<Modal title={t("content_language.title")}
className="content-languages-modal" show={modalShown} onHidden={() => setModalShown(false)}
title={t("content_language.title")} size="lg" scrollable
show={modalShown} onHidden={() => setModalShown(false)} >
size="lg" scrollable <ContentLanguagesList />
> </Modal>
<ContentLanguagesList />
</Modal>
</div>
); );
} }

View File

@ -1,5 +1,5 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons"; import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { useContext } from "preact/hooks"; import { useContext, useState } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context"; import appContext, { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
@ -13,10 +13,12 @@ import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/u
import ws from "../../services/ws"; import ws from "../../services/ws";
import ActionButton from "../react/ActionButton"; import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown"; import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteProperty, useTriliumOption } from "../react/hooks"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
import protected_session from "../../services/protected_session";
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); const isNewLayout = isExperimentalFeatureEnabled("new-layout");
@ -55,8 +57,10 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
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 isHelpPage = note.noteId.startsWith("_help");
const [syncServerHost] = useTriliumOption("syncServerHost"); const [syncServerHost] = useTriliumOption("syncServerHost");
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isNormalViewMode = noteContext?.viewScope?.viewMode === "default";
return ( return (
<Dropdown <Dropdown
@ -79,6 +83,11 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
{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 />
{isNewLayout && isNormalViewMode && !isHelpPage && <>
<NoteBasicProperties note={note} />
<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 })} />
@ -107,11 +116,82 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
<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 && <>
<FormDropdownDivider />
<DevelopmentActions note={note} noteContext={noteContext} />
</>}
</Dropdown> </Dropdown>
); );
} }
function NoteBasicProperties({ note }: { note: FNote }) {
const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
const [ isShared, switchShareState ] = useShareState(note);
const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template");
const isProtected = useNoteProperty(note, "isProtected");
return <>
<FormListToggleableItem
icon="bx bx-bookmark"
title={t("bookmark_switch.bookmark")}
currentValue={isBookmarked} onChange={setIsBookmarked}
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
/>
<FormListToggleableItem
icon="bx bx-copy-alt"
title={t("template_switch.template")}
currentValue={isTemplate} onChange={setIsTemplate}
helpPage="KC1HB96bqqHX"
disabled={note?.noteId.startsWith("_options")}
/>
<FormListToggleableItem
icon="bx bx-share-alt"
title={t("shared_switch.shared")}
currentValue={isShared} onChange={switchShareState}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
<EditabilityDropdown note={note} />
<FormListToggleableItem
icon="bx bx-lock-alt"
title={t("protect_note.toggle-on")}
currentValue={!!isProtected} onChange={shouldProtect => protected_session.protectNote(note.noteId, shouldProtect, false)}
/>
<FormDropdownDivider />
<NoteTypeDropdown note={note} />
</>;
}
function EditabilityDropdown({ note }: { note: FNote }) {
const [ readOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const [ autoReadOnlyDisabled, setAutoReadOnlyDisabled ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
function setState(readOnly: boolean, autoReadOnlyDisabled: boolean) {
setReadOnly(readOnly);
setAutoReadOnlyDisabled(autoReadOnlyDisabled);
}
return (
<FormDropdownSubmenu title={t("basic_properties.editable")} icon="bx bx-edit-alt" dropStart>
<FormListItem checked={!readOnly && !autoReadOnlyDisabled} onClick={() => setState(false, false)} description={t("editability_select.note_is_editable")}>{t("editability_select.auto")}</FormListItem>
<FormListItem checked={readOnly && !autoReadOnlyDisabled} onClick={() => setState(true, false)} description={t("editability_select.note_is_read_only")}>{t("editability_select.read_only")}</FormListItem>
<FormListItem checked={!readOnly && autoReadOnlyDisabled} onClick={() => setState(false, true)} description={t("editability_select.note_is_always_editable")}>{t("editability_select.always_editable")}</FormListItem>
</FormDropdownSubmenu>
);
}
function NoteTypeDropdown({ note }: { note: FNote }) {
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
const currentNoteMime = useNoteProperty(note, "mime");
const [ modalShown, setModalShown ] = useState(false);
return (
<FormDropdownSubmenu title={t("basic_properties.note_type")} icon="bx bx-file" dropStart>
<NoteTypeDropdownContent currentNoteType={currentNoteType} currentNoteMime={currentNoteMime} note={note} setModalShown={setModalShown} />
</FormDropdownSubmenu>
);
}
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
return ( return (
<FormDropdownSubmenu title="Development Actions" icon="bx bx-wrench" dropStart> <FormDropdownSubmenu title="Development Actions" icon="bx bx-wrench" dropStart>

View File

@ -1,22 +1,24 @@
import ScriptTab from "./ScriptTab";
import EditedNotesTab from "./EditedNotesTab";
import NotePropertiesTab from "./NotePropertiesTab";
import NoteInfoTab from "./NoteInfoTab";
import SimilarNotesTab from "./SimilarNotesTab";
import FilePropertiesTab from "./FilePropertiesTab";
import ImagePropertiesTab from "./ImagePropertiesTab";
import NotePathsTab from "./NotePathsTab";
import NoteMapTab from "./NoteMapTab";
import OwnedAttributesTab from "./OwnedAttributesTab";
import InheritedAttributesTab from "./InheritedAttributesTab";
import CollectionPropertiesTab from "./CollectionPropertiesTab";
import SearchDefinitionTab from "./SearchDefinitionTab";
import BasicPropertiesTab from "./BasicPropertiesTab";
import FormattingToolbar from "./FormattingToolbar";
import options from "../../services/options";
import { t } from "../../services/i18n";
import { TabConfiguration } from "./ribbon-interface";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { t } from "../../services/i18n";
import options from "../../services/options";
import BasicPropertiesTab from "./BasicPropertiesTab";
import CollectionPropertiesTab from "./CollectionPropertiesTab";
import EditedNotesTab from "./EditedNotesTab";
import FilePropertiesTab from "./FilePropertiesTab";
import FormattingToolbar from "./FormattingToolbar";
import ImagePropertiesTab from "./ImagePropertiesTab";
import InheritedAttributesTab from "./InheritedAttributesTab";
import NoteInfoTab from "./NoteInfoTab";
import NoteMapTab from "./NoteMapTab";
import NotePathsTab from "./NotePathsTab";
import NotePropertiesTab from "./NotePropertiesTab";
import OwnedAttributesTab from "./OwnedAttributesTab";
import { TabConfiguration } from "./ribbon-interface";
import ScriptTab from "./ScriptTab";
import SearchDefinitionTab from "./SearchDefinitionTab";
import SimilarNotesTab from "./SimilarNotesTab";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{ {
@ -28,7 +30,7 @@ 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: !isExperimentalFeatureEnabled("new-layout"), stayInDom: !isNewLayout,
avoidInNewLayout: true avoidInNewLayout: true
}, },
{ {
@ -85,11 +87,10 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
activate: true, activate: true,
}, },
{ {
// BasicProperties
title: t("basic_properties.basic_properties"), title: t("basic_properties.basic_properties"),
icon: "bx bx-slider", icon: "bx bx-slider",
content: BasicPropertiesTab, content: BasicPropertiesTab,
show: ({note}) => !note?.isLaunchBarConfig(), show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabBasicProperties" toggleCommand: "toggleRibbonTabBasicProperties"
}, },
{ {

View File

@ -1,8 +1,9 @@
import { Locale } from "@triliumnext/commons"; import { Locale } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { useMemo } from "preact/hooks";
import Dropdown from "../../../react/Dropdown"; import Dropdown from "../../../react/Dropdown";
import { FormDropdownDivider, FormListItem } from "../../../react/FormList"; import { FormDropdownDivider, FormListItem } from "../../../react/FormList";
import { ComponentChildren } from "preact";
import { useMemo, useState } from "preact/hooks";
export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: { export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: {
id?: string; id?: string;
@ -12,7 +13,7 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc
defaultLocale?: Locale, defaultLocale?: Locale,
extraChildren?: ComponentChildren extraChildren?: ComponentChildren
}) { }) {
const [ activeLocale, setActiveLocale ] = useState(defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue)); const activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue);
const processedLocales = useMemo(() => { const processedLocales = useMemo(() => {
const leftToRightLanguages = locales.filter((l) => !l.rtl); const leftToRightLanguages = locales.filter((l) => !l.rtl);
@ -48,7 +49,6 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc
rtl={locale.rtl} rtl={locale.rtl}
checked={locale.id === currentValue} checked={locale.id === currentValue}
onClick={() => { onClick={() => {
setActiveLocale(locale);
onChange(locale.id); onChange(locale.id);
}} }}
>{locale.name}</FormListItem> >{locale.name}</FormListItem>