Compare commits

..

3 Commits

193 changed files with 26411 additions and 8064 deletions

1
.envrc
View File

@@ -1 +0,0 @@
use flake

View File

@@ -86,12 +86,12 @@ jobs:
- name: Upload Playwright trace
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: Playwright trace (${{ matrix.dockerfile }})
path: test-output/playwright/output
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: Playwright report (${{ matrix.dockerfile }})
@@ -213,7 +213,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
path: /tmp/digests/*
@@ -227,7 +227,7 @@ jobs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
path: /tmp/digests
pattern: digests-*

View File

@@ -102,7 +102,7 @@ jobs:
name: Nightly Build
- name: Publish artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
if: ${{ github.event_name == 'pull_request' }}
with:
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}

View File

@@ -77,7 +77,7 @@ jobs:
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: e2e report ${{ matrix.arch }}
path: apps/server-e2e/test-output

View File

@@ -73,7 +73,7 @@ jobs:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Upload the artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
path: apps/desktop/upload/*.*
@@ -100,7 +100,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Upload the artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: release-server-linux-${{ matrix.arch }}
path: upload/*.*
@@ -120,7 +120,7 @@ jobs:
docs/Release Notes
- name: Download all artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
merge-multiple: true
pattern: release-*

3
.gitignore vendored
View File

@@ -44,10 +44,9 @@ upload
.rollup.cache
*.tsbuildinfo
/.direnv
/result
.svelte-kit
# docs
site/
apps/*/coverage
apps/*/coverage

2
.nvmrc
View File

@@ -1 +1 @@
24.12.0
24.11.1

View File

@@ -37,9 +37,6 @@
"apps/server/src/assets/doc_notes/**": true,
"apps/edit-docs/demo/**": true
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.rules.customizations": [
{ "rule": "*", "severity": "warn" }
]

View File

@@ -9,13 +9,13 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.25.0",
"packageManager": "pnpm@10.24.0",
"devDependencies": {
"@redocly/cli": "2.12.7",
"@redocly/cli": "2.12.3",
"archiver": "7.0.1",
"fs-extra": "11.3.2",
"react": "19.2.3",
"react-dom": "19.2.3",
"react": "19.2.1",
"react-dom": "19.2.1",
"typedoc": "0.28.15",
"typedoc-plugin-missing-exports": "4.1.2"
}

View File

@@ -44,7 +44,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"i18next": "25.7.2",
"i18next": "25.7.1",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -60,7 +60,7 @@
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.0",
"react-i18next": "16.5.0",
"react-i18next": "16.4.0",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",

View File

@@ -265,7 +265,7 @@ export type CommandMappings = {
reEvaluateRightPaneVisibility: CommandData;
runActiveNote: CommandData;
scrollContainerTo: CommandData & {
scrollContainerToCommand: CommandData & {
position: number;
};
scrollToEnd: CommandData;
@@ -498,10 +498,6 @@ type EventMappings = {
noteIds: string[];
};
refreshData: { ntxId: string | null | undefined };
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
}
};
export type EventListener<T extends EventNames> = {

View File

@@ -1,19 +1,18 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import type FNote from "../entities/fnote.js";
import { closeActiveDialog } from "../services/dialog.js";
import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js";
import type { ViewScope } from "../services/link.js";
import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import treeService from "../services/tree.js";
import Component from "./component.js";
import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js";
import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { closeActiveDialog } from "../services/dialog.js";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
export interface SetNoteOpts {
triggerSwitchEvent?: unknown;

View File

@@ -6,7 +6,7 @@ import openService from "../services/open.js";
import protectedSessionService from "../services/protected_session.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import utils, { openInReusableSplit } from "../services/utils.js";
import utils from "../services/utils.js";
import toastService from "../services/toast.js";
import noteCreateService from "../services/note_create.js";
@@ -193,16 +193,6 @@ export default class RootCommandExecutor extends Component {
appContext.triggerEvent("zenModeChanged", { isEnabled });
}
async toggleRibbonTabNoteMapCommand() {
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
if (!isNewLayout) return;
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext?.notePath) return;
openInReusableSplit(activeContext.notePath, "note-map");
}
firstTabCommand() {
this.#goToTab(1);
}

View File

@@ -1,57 +1,51 @@
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import { applyModals } from "./layout_commons.js";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import ApiLog from "../widgets/api_log.jsx";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import FindWidget from "../widgets/find.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import FindWidget from "../widgets/find.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StatusBar from "../widgets/layout/StatusBar.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import options from "../services/options.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TabRowWidget from "../widgets/tab_row.js";
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
import TocWidget from "../widgets/toc.js";
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import { applyModals } from "./layout_commons.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import Breadcrumb from "../widgets/Breadcrumb.jsx";
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
export default class DesktopLayout {
@@ -77,11 +71,10 @@ export default class DesktopLayout {
*/
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
.optChild(
fullWidthTabBar,
new FlexContainer("row")
@@ -117,7 +110,6 @@ export default class DesktopLayout {
.child(new TabRowWidget())
.optChild(customTitleBarButtons, <TitleBarButtons />)
.css("height", "40px"))
.optChild(isNewLayout, <FixedFormattingToolbar />)
.child(
new FlexContainer("row")
.filling()
@@ -131,31 +123,37 @@ export default class DesktopLayout {
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(new FlexContainer("row")
.class("title-row")
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.optChild(isNewLayout, <NoteBadges />)
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.optChild(!isNewLayout, <MovePaneButton direction="left" />)
.optChild(!isNewLayout, <MovePaneButton direction="right" />)
.optChild(!isNewLayout, <ClosePaneButton />)
.optChild(!isNewLayout, <CreatePaneButton />)
.optChild(isNewLayout, <NoteActions />))
.optChild(!isNewLayout, <Ribbon />)
.child(
new FlexContainer("row")
.class("breadcrumb-row")
.css("height", "30px")
.css("min-height", "30px")
.css("align-items", "center")
.css("padding", "10px")
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
.child(<Breadcrumb />)
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)
.child(<CreatePaneButton />)
)
.child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child(
new ScrollingContainer()
.filling()
.optChild(isNewLayout, <InlineTitle />)
.optChild(isNewLayout, <NoteTitleActions />)
.optChild(!isNewLayout, new ContentHeader()
.child(new ContentHeader()
.child(new FlexContainer("row")
.class("title-row")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
)
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />)
)
.optChild(!isNewLayout, <PromotedAttributes />)
.child(<Ribbon />)
.child(<PromotedAttributes />)
.child(<SqlTableSchemas />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
@@ -172,7 +170,6 @@ export default class DesktopLayout {
)
)
.child(...this.customWidgets.get("center-pane"))
)
.child(
new RightPaneContainer()
@@ -181,10 +178,8 @@ export default class DesktopLayout {
.child(...this.customWidgets.get("right-pane"))
)
)
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
)
)
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
.child(<CloseZenModeButton />)
// Desktop-specific dialogs.

View File

@@ -52,5 +52,5 @@ export function applyModals(rootContainer: RootContainer) {
.child(<IncorrectCpuArchDialog />)
.child(<PopupEditorDialog />)
.child(<CallToActionDialog />)
.child(<ToastContainer />);
.child(<ToastContainer />)
}

View File

@@ -1,51 +0,0 @@
import { t } from "./i18n";
import options from "./options";
export 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[];
export 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();
}
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() {
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;
}

View File

@@ -27,7 +27,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
return icon;
}
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
export type ViewMode = "default" | "source" | "attachments" | "contextual-help";
export interface ViewScope {
/**

View File

@@ -1,5 +1,5 @@
import { dayjs } from "@triliumnext/commons";
import type { ViewMode, ViewScope } from "./link.js";
import type { ViewScope } from "./link.js";
import FNote from "../entities/fnote";
import { snapdom } from "@zumer/snapdom";
@@ -439,20 +439,7 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
* @returns a promise that resolves once the help has been opened.
*/
export function openInAppHelpFromUrl(inAppHelpPage: string) {
return openInReusableSplit(`_help_${inAppHelpPage}`, "contextual-help");
}
/**
* Similar to opening a new note in a split, but re-uses an existing split if there is already one open with the same view mode.
*
* @param targetNoteId the note ID to open in the split.
* @param targetViewMode the view mode of the split to open the note in.
* @param openOpts additional options for opening the note.
*/
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
@@ -460,20 +447,23 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode:
return;
}
const subContexts = activeContext.getSubContexts();
const existingSubcontext = subContexts.find((s) => s.viewScope?.viewMode === targetViewMode);
const viewScope: ViewScope = { viewMode: targetViewMode };
if (!existingSubcontext) {
// The target split is not already open, open a new split with it.
const targetNote = `_help_${inAppHelpPage}`;
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
const viewScope: ViewScope = {
viewMode: "contextual-help",
};
if (!helpSubcontext) {
// The help is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,
notePath: targetNote,
hoistedNoteId: "_help",
viewScope
});
})
} else {
// There is already a target split open, make sure it opens on the right note.
existingSubcontext.setNote(targetNoteId, { viewScope });
// There is already a help window open, make sure it opens on the right note.
helpSubcontext.setNote(targetNote, { viewScope });
}
}

View File

@@ -423,16 +423,16 @@ body.desktop .tabulator-popup-container,
pointer-events: none;
}
.dropdown-menu .disabled .contextual-help {
.dropdown-menu .disabled .disabled-tooltip {
pointer-events: all;
margin-inline-start: 8px;
font-size: 0.75rem;
color: var(--contextual-help-icon-color);
color: var(--disabled-tooltip-icon-color);
cursor: help;
opacity: 0.75;
}
.dropdown-menu .disabled .contextual-help:hover {
.dropdown-menu .disabled .disabled-tooltip:hover {
opacity: 1;
}
@@ -521,7 +521,9 @@ body.mobile .dropdown .dropdown-submenu > span {
.cm-editor {
height: 100%;
outline: none !important;
border-radius: 6px;
overflow: hidden;
margin: 4px;
font-size: var(--monospace-font-size);
}
@@ -627,11 +629,6 @@ pre:not(.hljs) {
padding: var(--padding-size);
}
pre:has(> .cm-editor) {
padding: 0;
margin: 0;
}
pre > button.copy-button {
position: absolute;
top: var(--copy-button-margin-size);
@@ -1318,21 +1315,12 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
top: 0;
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;
min-width: 15rem;
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
max-height: 600px;
overflow: auto;
}
body.desktop .dropdown-submenu > .dropdown-menu {
min-width: max-content;
max-width: 300px;
}
.dropdown-submenu.dropstart > .dropdown-menu {
inset-inline-start: auto;
inset-inline-end: calc(100% - 2px);
}
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-start: calc(-100% + 10px);
}
@@ -2116,106 +2104,58 @@ body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-contai
/* Fixed formatting toolbar */
body.zen:not(.experimental-feature-new-layout) {
.note-split .ribbon-container {
position: fixed;
left: 0;
bottom: 20px;
width: 100%;
z-index: 1000;
opacity: 0; /* Hidden unless the current note split is focused */
pointer-events: none;
transition: opacity 100ms linear;
}
.note-split:focus-within .ribbon-container {
opacity: 1; /* Show when the note split is focused */
}
.note-split .ribbon-container .ribbon-body {
border: 0;
}
.note-split .ribbon-container .classic-toolbar-widget {
margin: auto;
width: fit-content;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
border-radius: 8px;
border: 1px solid var(--main-border-color);
padding: 4px;
background: var(--menu-background-color);
}
.note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
/* Hide the toolbar wrapper if the toolbar is missing */
display: none;
}
.note-split:focus-within .ribbon-container .classic-toolbar-widget {
pointer-events: all;
}
@media (max-width: 1300px) {
.note-split .ribbon-container .classic-toolbar-widget {
/* Set the toolbar to full with */
width: 100%;
}
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
/* Force toolbar items overflow dropdowns open upwards */
top: auto;
bottom: 100%;
}
}
body.zen .note-split .ribbon-container {
position: fixed;
left: 0;
bottom: 20px;
width: 100%;
z-index: 1000;
opacity: 0; /* Hidden unless the current note split is focused */
pointer-events: none;
transition: opacity 100ms linear;
}
body.zen.experimental-feature-new-layout {
.status-bar {
display: none;
body.zen .note-split:focus-within .ribbon-container {
opacity: 1; /* Show when the note split is focused */
}
body.zen .note-split .ribbon-container .ribbon-body {
border: 0;
}
body.zen .note-split .ribbon-container .classic-toolbar-widget {
margin: auto;
width: fit-content;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
border-radius: 8px;
border: 1px solid var(--main-border-color);
padding: 4px;
background: var(--menu-background-color);
}
body.zen .note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
/* Hide the toolbar wrapper if the toolbar is missing */
display: none;
}
body.zen .note-split:focus-within .ribbon-container .classic-toolbar-widget {
pointer-events: all;
}
@media (max-width: 1300px) {
body.zen .note-split .ribbon-container .classic-toolbar-widget {
/* Set the toolbar to full with */
width: 100%;
}
.classic-toolbar-widget {
position: fixed;
left: 50%;
bottom: 20px;
z-index: 1000;
opacity: 0; /* Hidden unless the current note split is focused */
pointer-events: none;
transition: opacity 100ms linear;
width: fit-content;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
border-radius: 8px;
border: 1px solid var(--main-border-color);
padding: 4px;
background: var(--menu-background-color);
transform: translateX(-50%);
}
#root-widget:has(.note-split.type-text:focus-within) .classic-toolbar-widget,
.classic-toolbar-widget:focus-within {
opacity: 1; /* Show when the note split is focused */
pointer-events: all;
}
@media (max-width: 1300px) {
.classic-toolbar-widget {
/* Set the toolbar to full with */
width: 100%;
}
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
.classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
/* Force toolbar items overflow dropdowns open upwards */
top: auto;
bottom: 100%;
}
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
/* Force toolbar items overflow dropdowns open upwards */
top: auto;
bottom: 100%;
}
}
@@ -2522,11 +2462,6 @@ footer.webview-footer button {
inset-inline-start: 10px;
}
.content-floating-buttons.top-right {
top: 10px;
inset-inline-end: 10px;
}
.content-floating-buttons.bottom-left {
bottom: 10px;
inset-inline-start: 10px;

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,13 +89,13 @@
* the color is adjusted based on the current color scheme (light or dark). The lightness
* component of the color represented in the CIELAB color space, will be
* constrained to a certain percentage defined below.
*
*
* Note: the tree background may vary when background effects are enabled, so it is recommended
* to maintain a higher contrast margin than on the usual note tree solid background. */
/* The maximum perceptual lightness for the custom color in the light theme (%): */
--tree-item-light-theme-max-color-lightness: 60;
/* The minimum perceptual lightness for the custom color in the dark theme (%): */
--tree-item-dark-theme-min-color-lightness: 65;
}
@@ -165,33 +165,15 @@ body.desktop .dropdown-submenu .dropdown-menu {
--menu-item-start-padding: 8px;
--menu-item-end-padding: 22px;
--menu-item-vertical-padding: 2px;
/* Note: the right padding should also accommodate the submenu arrow. */
border-radius: 6px;
cursor: default !important;
}
.dropdown-item:not(.dropdown-submenu),
body.desktop .dropdown-item.dropdown-submenu .dropdown-toggle,
.excalidraw .context-menu .context-menu-item {
padding-top: var(--menu-item-vertical-padding) !important;
padding-bottom: var(--menu-item-vertical-padding) !important;
padding-inline-start: var(--menu-item-start-padding) !important;
padding-inline-end: var(--menu-item-end-padding) !important;
}
.dropdown-item.dropdown-submenu {
padding: 0 !important;
.dropdown-toggle {
flex-grow: 1;
}
}
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item:not(.dropdown-submenu),
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item.dropdown-submenu .dropdown-toggle {
padding-inline-end: var(--menu-item-start-padding) !important;
padding-inline-start: var(--menu-item-end-padding) !important;
/* Note: the right padding should also accommodate the submenu arrow. */
border-radius: 6px;
cursor: default !important;
}
:root .dropdown-item:focus-visible {
@@ -267,8 +249,7 @@ html body .dropdown-item[disabled] {
}
/* Menu item arrow */
body.mobile .dropdown-submenu .dropdown-toggle::after,
body.desktop .dropdown-submenu:not(.dropstart) .dropdown-toggle::after {
.dropdown-menu .dropdown-toggle::after {
content: "\ed3b" !important;
position: absolute;
display: flex !important;
@@ -284,26 +265,6 @@ body.desktop .dropdown-submenu:not(.dropstart) .dropdown-toggle::after {
color: var(--menu-item-arrow-color) !important;
}
body.mobile .dropdown-submenu.dropstart .dropdown-toggle::before {
content: unset;
}
body.desktop .dropdown-submenu.dropstart .dropdown-toggle::before {
content: "\ea4d" !important;
position: absolute;
display: flex !important;
align-items: center;
justify-content: center;
top: 0;
inset-inline-start: 0;
margin: unset !important;
border: unset !important;
padding: 0 4px;
font-family: boxicons;
font-size: 1.2em;
color: var(--menu-item-arrow-color) !important;
}
body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after {
content: "\ea4d" !important;
}
@@ -378,7 +339,7 @@ body.mobile .dropdown-menu {
font-size: 1em !important;
backdrop-filter: var(--dropdown-backdrop-filter);
position: relative;
.dropdown-toggle::after {
top: 0.5em;
right: var(--dropdown-menu-padding-horizontal);
@@ -395,7 +356,7 @@ body.mobile .dropdown-menu {
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal) !important;
background: var(--card-background-color);
border-bottom: 1px solid var(--menu-item-delimiter-color) !important;
border-radius: 0;
border-radius: 0;
}
.dropdown-item:first-of-type,
@@ -406,9 +367,9 @@ body.mobile .dropdown-menu {
border-top-right-radius: 6px;
}
.dropdown-item:last-of-type,
.dropdown-item:last-of-type,
.dropdown-item:has(+ .dropdown-divider),
.dropdown-custom-item:last-of-type,
.dropdown-custom-item:last-of-type,
.dropdown-custom-item:has(+ .dropdown-divider) {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
@@ -431,10 +392,10 @@ body.mobile .dropdown-menu {
--menu-background-color: --menu-submenu-mobile-background-color;
--bs-dropdown-divider-margin-y: 0.25rem;
border-radius: 0;
max-height: 0;
max-height: 0;
transition: max-height 100ms ease-in;
display: block !important;
display: block !important;
&.show {
max-height: 1000px;
padding: 0.5rem 0.75rem !important;
@@ -444,7 +405,7 @@ body.mobile .dropdown-menu {
&.submenu-open {
.dropdown-toggle {
padding-bottom: var(--dropdown-menu-padding-vertical);
}
}
}
}
@@ -782,4 +743,4 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
.note-detail-empty .aa-suggestions div.aa-cursor {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
}

View File

@@ -154,7 +154,7 @@ button.btn.btn-success kbd {
color: var(--button-group-active-button-text-color);
}
/*
/*
* Input boxes
*/
@@ -399,8 +399,7 @@ button.select-button.dropdown-toggle.btn:active {
select:focus,
select.form-select:focus,
select.form-control:focus,
.select-button.dropdown-toggle.btn:focus,
.select-button.focus-outline:focus {
.select-button.dropdown-toggle.btn:focus {
box-shadow: unset;
outline: 3px solid var(--input-focus-outline-color);
outline-offset: 0;
@@ -423,7 +422,7 @@ optgroup {
line-height: 40px;
}
/*
/*
* File input
*
* <label class="tn-file-input tn-input-field">
@@ -785,4 +784,4 @@ input[type="range"] {
scrollbar-color: unset;
scrollbar-width: unset;
}
}
}

View File

@@ -168,7 +168,25 @@ ul.editability-dropdown li.dropdown-item > div {
* Note info
*/
:root .note-info-widget-table button.calculate-button {
min-width: 0;
padding: 4px 10px !important;
font-size: 0.8em;
}
/* Narrow width layout */
.note-info-widget {
container: info-section / inline-size;
}
/*
* Styling as a floating toolbar
*/
.ribbon-container {
position: sticky;
top: 0;
left: 0;
right: 0;
background: var(--main-background-color);
z-index: 997;
}

View File

@@ -160,11 +160,6 @@ span.fancytree-node.multiple-parents .fancytree-title::after {
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
}
body.experimental-feature-new-layout span.fancytree-node.multiple-parents .fancytree-title::after {
content: " \ed82";
opacity: 0.5;
}
span.fancytree-node.shared .fancytree-title::after {
font-family: "boxicons" !important;
font-size: smaller;

View File

@@ -1,12 +1,10 @@
import { NoteType } from "@triliumnext/commons";
import FAttribute from "../entities/fattribute.js";
import FBlob from "../entities/fblob.js";
import FBranch from "../entities/fbranch.js";
import utils from "../services/utils.js";
import FNote from "../entities/fnote.js";
import froca from "../services/froca.js";
import FAttribute from "../entities/fattribute.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
import utils from "../services/utils.js";
import FBranch from "../entities/fbranch.js";
import FBlob from "../entities/fblob.js";
type AttributeDefinitions = { [key in `#${string}`]: string; };
type RelationDefinitions = { [key in `~${string}`]: string; };
@@ -14,7 +12,6 @@ type RelationDefinitions = { [key in `~${string}`]: string; };
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
id?: string | undefined;
title: string;
type?: NoteType;
children?: NoteDefinition[];
content?: string;
}
@@ -48,7 +45,7 @@ export function buildNote(noteDef: NoteDefinition) {
const note = new FNote(froca, {
noteId: noteDef.id ?? utils.randomString(12),
title: noteDef.title,
type: noteDef.type ?? "text",
type: "text",
mime: "text/html",
isProtected: false,
blobId: ""

View File

@@ -693,10 +693,7 @@
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?",
"print_pdf": "导出为 PDF...",
"open_note_on_server": "在服务器上打开笔记",
"view_revisions": "笔记修订...",
"note_map": "笔记地图",
"advanced": "高级"
"open_note_on_server": "在服务器上打开笔记"
},
"onclick_button": {
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
@@ -1580,13 +1577,7 @@
"printing_pdf": "正在导出为PDF…"
},
"note_title": {
"placeholder": "请输入笔记标题...",
"created_on": "建立于 <Value />",
"last_modified": "最后修改于 <Value />",
"note_type_switcher_label": "从 {{type}} 切换到:",
"note_type_switcher_others": "更多笔记类型",
"note_type_switcher_templates": "模板",
"note_type_switcher_collection": "集合"
"placeholder": "请输入笔记标题..."
},
"search_result": {
"no_notes_found": "没有找到符合搜索条件的笔记。",
@@ -1948,9 +1939,8 @@
"unknown_widget": "未知组件:\"{{id}}\"."
},
"note_language": {
"not_set": "设置语言",
"configure-languages": "设置语言...",
"help-on-languages": "内容语言帮助..."
"not_set": "设置",
"configure-languages": "设置语言..."
},
"content_language": {
"title": "内容语言",
@@ -2017,7 +2007,7 @@
"book_properties_config": {
"hide-weekends": "隐藏周末",
"display-week-numbers": "显示周数",
"map-style": "地图样式",
"map-style": "地图样式",
"max-nesting-depth": "最大嵌套深度:",
"raster": "栅格",
"vector_light": "矢量(浅色)",
@@ -2126,43 +2116,5 @@
"unknown_http_error_title": "与服务器通讯错误",
"unknown_http_error_content": "状态码: {{statusCode}}\n地址: {{method}} {{url}}\n信息: {{message}}",
"traefik_blocks_requests": "如果您使用 Traefik 反向代理,它引入了一项影响与服务器的通信重大更改。"
},
"experimental_features": {
"title": "实验选项",
"disclaimer": "这些选项处于实验阶段,可能导致系统不稳定。请谨慎使用。",
"new_layout_name": "新布局",
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。"
},
"tab_history_navigation_buttons": {
"go-back": "返回前一笔记",
"go-forward": "前往下一笔记"
},
"breadcrumb_badges": {
"read_only_explicit": "只读",
"read_only_auto": "自动只读",
"shared_publicly": "公开共享",
"shared_locally": "本地共享",
"read_only_explicit_description": "此笔记已被手动设置为只读。\n点击可临时编辑。",
"read_only_auto_description": "出于性能原因,此笔记已被自动设置为只读模式。此自动限制可以在设置中调整。\n\n点击可临时编辑。",
"read_only_temporarily_disabled": "临时编辑",
"read_only_temporarily_disabled_description": "此笔记当前可编辑,但通常是只读的。一旦你切换到其他笔记,该笔记将恢复为只读模式。\n\n点击以重新启用只读模式。",
"shared_publicly_description": "此笔记已在网上发布,链接为 {{- link}},并且可公开访问。\n\n点击以导航到共享笔记或右键点击查看更多选项。",
"shared_locally_description": "此笔记仅在本地网络共享,链接为 {{- link}}。\n\n点击以导航到共享笔记或右键点击查看更多选项。",
"clipped_note": "网页剪辑",
"clipped_note_description": "此笔记最初来自 {{url}}。\n\n点击即可跳转至源网页。",
"execute_script": "运行脚本",
"execute_script_description": "这是一篇脚本笔记。点击即可执行脚本。",
"execute_sql": "运行SQL",
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。"
},
"status_bar": {
"language_title": "更改所有内容的语言",
"note_info_title": "查看有关此笔记的信息,例如创建/修改日期或笔记大小。",
"backlinks_title_other": "此笔记由 {{count}} 个其他笔记链接而来。\n\n点击查看反向链接列表。",
"attachments_title_other": "此笔记包含 {{count}} 个附件。点击即可在新标签页中打开附件列表。",
"attributes_other": "{{count}} 个属性",
"attributes_title": "单击以打开专用窗格,编辑此笔记拥有的属性,以及查看继承属性列表。",
"note_paths_title": "点击查看此笔记在树状图中的位置路径。",
"code_note_switcher": "更改语言模式"
}
}

View File

@@ -108,11 +108,6 @@
"cloned_note_prefix_title": "Klonovaná poznámka se zobrazí ve stromu poznámek s danou předponou",
"clone_to_selected_note": "Klonovat vybranou poznámku",
"no_path_to_clone_to": "Žádná cest pro klonování.",
"note_cloned": "Poznámka „{{clonedTitle}}“ bylo naklonována do „{{targetTitle}}“"
},
"zpetne_odkazy": {
"backlink_one": "{{count}} zpětný odkaz",
"backlink_few": "{{count}} zpětné odkazy",
"backlink_other": "{{count}} zpětných odkazů"
"note_cloned": "Poznámka: „{{clonedTitle}}“ bylo naklonováno do „{{targetTitle}}“"
}
}

View File

@@ -689,17 +689,11 @@
"export_note": "Export note",
"delete_note": "Delete note",
"print_note": "Print note",
"view_revisions": "Note revisions...",
"save_revision": "Save revision",
"advanced": "Advanced",
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?",
"print_pdf": "Export as PDF...",
"export_as_image": "Export as image",
"export_as_image_png": "PNG (raster)",
"export_as_image_svg": "SVG (vector)",
"note_map": "Note map"
"print_pdf": "Export as PDF..."
},
"onclick_button": {
"no_click_handler": "Button widget '{{componentId}}' has no defined click handler"
@@ -798,7 +792,7 @@
"file_type": "File type",
"file_size": "File size",
"download": "Download",
"open": "Open externally",
"open": "Open",
"upload_new_revision": "Upload new revision",
"upload_success": "New file revision has been uploaded.",
"upload_failed": "Upload of a new file revision failed.",
@@ -829,8 +823,7 @@
"note_size_info": "Note size provides rough estimate of storage requirements for this note. It takes into account note's content and content of its note revisions.",
"calculate": "calculate",
"subtree_size": "(subtree size: {{size}} in {{count}} notes)",
"title": "Note Info",
"show_similar_notes": "Show similar notes"
"title": "Note Info"
},
"note_map": {
"open_full": "Expand to full",
@@ -893,8 +886,7 @@
"search_parameters": "Search Parameters",
"unknown_search_option": "Unknown search option {{searchOptionName}}",
"search_note_saved": "Search note has been saved into {{- notePathTitle}}",
"actions_executed": "Actions have been executed.",
"view_options": "View options:"
"actions_executed": "Actions have been executed."
},
"similar_notes": {
"title": "Similar Notes",
@@ -1104,12 +1096,6 @@
"vacuuming_database": "Vacuuming database...",
"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": {
"theme_defined": "Theme defined",
"fonts": "Fonts",
@@ -1757,14 +1743,7 @@
"printing_pdf": "Exporting to PDF in progress..."
},
"note_title": {
"placeholder": "type note's title here...",
"created_on": "Created on <Value />",
"last_modified": "Modified on <Value />",
"note_type_switcher_label": "Switch from {{type}} to:",
"note_type_switcher_others": "Other note type",
"note_type_switcher_templates": "Template",
"note_type_switcher_collection": "Collection",
"edited_notes": "Edited notes"
"placeholder": "type note's title here..."
},
"search_result": {
"no_notes_found": "No notes have been found for given search parameters.",
@@ -1974,9 +1953,8 @@
"unknown_widget": "Unknown widget for \"{{id}}\"."
},
"note_language": {
"not_set": "No language set",
"configure-languages": "Configure languages...",
"help-on-languages": "Help on content languages..."
"not_set": "Not set",
"configure-languages": "Configure languages..."
},
"content_language": {
"title": "Content languages",
@@ -2043,7 +2021,7 @@
"book_properties_config": {
"hide-weekends": "Hide weekends",
"display-week-numbers": "Display week numbers",
"map-style": "Map style",
"map-style": "Map style:",
"max-nesting-depth": "Max nesting depth:",
"raster": "Raster",
"vector_light": "Vector (Light)",
@@ -2143,49 +2121,5 @@
"tab_history_navigation_buttons": {
"go-back": "Go back to previous note",
"go-forward": "Go forward to next note"
},
"breadcrumb": {
"hoisted_badge": "Hoisted",
"hoisted_badge_title": "Unhoist",
"workspace_badge": "Workspace",
"scroll_to_top_title": "Jump to the beginning of the note"
},
"breadcrumb_badges": {
"read_only_explicit": "Read-only",
"read_only_explicit_description": "This note has been manually set to read-only.\nClick to edit it temporarily.",
"read_only_auto": "Auto read-only",
"read_only_auto_description": "This note was set automatically to read-only mode for performance reasons. This automatic limit is adjustable from settings.\n\nClick to edit it temporarily.",
"read_only_temporarily_disabled": "Temporarily editable",
"read_only_temporarily_disabled_description": "This note is currently editable, but it is normally read-only. The note will go back to being read-only as soon as you navigate to another note.\n\nClick to re-enable read-only mode.",
"shared_publicly": "Shared publicly",
"shared_locally": "Shared locally",
"shared_copy_to_clipboard": "Copy link to clipboard",
"shared_open_in_browser": "Open link in browser",
"shared_unshare": "Remove share",
"clipped_note": "Web clip",
"clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.",
"execute_script": "Run script",
"execute_script_description": "This note is a script note. Click to execute the script.",
"execute_sql": "Run SQL",
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
},
"status_bar": {
"language_title": "Change content language",
"note_info_title": "View note info (e.g., dates, note size)",
"backlinks_one": "{{count}} backlink",
"backlinks_other": "{{count}} backlinks",
"backlinks_title_one": "View backlink",
"backlinks_title_other": "View backlinks",
"attachments_one": "{{count}} attachment",
"attachments_other": "{{count}} attachments",
"attachments_title_one": "View attachment in a new tab",
"attachments_title_other": "View attachments in a new tab",
"attributes_one": "{{count}} attribute",
"attributes_other": "{{count}} attributes",
"attributes_title": "Owned attributes and inherited attributes",
"note_paths_one": "{{count}} path",
"note_paths_other": "{{count}} paths",
"note_paths_title": "Note paths",
"code_note_switcher": "Change language mode"
}
}

View File

@@ -94,8 +94,7 @@
"info": {
"okButton": "OK",
"closeButton": "Chiudi",
"modalTitle": "Messaggio informativo",
"copy_to_clipboard": "Copia negli appunti"
"modalTitle": "Messaggio informativo"
},
"export": {
"close": "Chiudi",
@@ -315,7 +314,7 @@
"import-into-note": "Importa nella nota",
"apply-bulk-actions": "Applica azioni in blocco",
"converted-to-attachments": "{{count}} note sono state convertite in allegati.",
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note principali? Questa operazione si applica solo alle note immagine, le altre note verranno ignorate.",
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note padre?",
"open-in-popup": "Modifica rapida"
},
"electron_context_menu": {
@@ -1261,8 +1260,7 @@
"convert_into_attachment_successful": "Nota '{{title}}' è stato convertito in allegato.",
"convert_into_attachment_prompt": "Sei sicuro di voler convertire la nota '{{title}}' in un allegato della nota padre?",
"print_pdf": "Esporta come PDF...",
"open_note_on_server": "Apri una nota sul server",
"view_revisions": "Revisioni..."
"open_note_on_server": "Apri una nota sul server"
},
"onclick_button": {
"no_click_handler": "Il widget pulsante '{{componentId}}' non ha un gestore di clic definito"
@@ -1495,12 +1493,7 @@
"editable_text": {
"placeholder": "Digita qui il contenuto della tua nota...",
"auto-detect-language": "Rilevato automaticamente",
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug.",
"editor_crashed_title": "L'editor di testo si è bloccato",
"editor_crashed_content": "I tuoi contenuti sono stati recuperati con successo, ma alcune delle modifiche più recenti potrebbero non essere state salvate.",
"editor_crashed_details_button": "Visualizza ulteriori dettagli...",
"editor_crashed_details_intro": "Se questo errore si verifica più volte, valuta la possibilità di segnalarlo su GitHub incollando le informazioni riportate di seguito.",
"editor_crashed_details_title": "Informazioni tecniche"
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
},
"empty": {
"open_note_instruction": "Apri una nota digitandone il titolo nel campo sottostante oppure scegli una nota nell'albero.",
@@ -1874,9 +1867,7 @@
"printing_pdf": "Esportazione in PDF in corso..."
},
"note_title": {
"placeholder": "scrivi qui il titolo della nota...",
"created_on": "Creato il <Value />",
"last_modified": "Ultima modifica il <Value />"
"placeholder": "scrivi qui il titolo della nota..."
},
"search_result": {
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
@@ -2012,9 +2003,8 @@
"unknown_widget": "Widget sconosciuto per \"{{id}}\"."
},
"note_language": {
"not_set": "Nessuna lingua impostata",
"configure-languages": "Configura le lingue...",
"help-on-languages": "Aiuto sulle lingue dei contenuti..."
"not_set": "Non impostato",
"configure-languages": "Configura le lingue..."
},
"content_language": {
"title": "Lingue dei contenuti",
@@ -2032,8 +2022,7 @@
"button_title": "Esporta diagramma come PNG"
},
"svg": {
"export_to_png": "Non è stato possibile esportare il diagramma in formato PNG.",
"export_to_svg": "Il diagramma non può essere esportato in formato SVG."
"export_to_png": "Non è stato possibile esportare il diagramma in formato PNG."
},
"code_theme": {
"title": "Aspetto",
@@ -2117,32 +2106,5 @@
},
"popup-editor": {
"maximize": "Passa all'editor completo"
},
"experimental_features": {
"title": "Opzioni sperimentali",
"disclaimer": "Queste opzioni sono sperimentali e potrebbero causare instabilità. Usare con cautela.",
"new_layout_name": "Nuovo layout",
"new_layout_description": "Prova il nuovo layout per un look più moderno e una maggiore usabilità. Soggetto a modifiche significative nelle prossime versioni."
},
"server": {
"unknown_http_error_title": "Errore di comunicazione con il server",
"unknown_http_error_content": "Codice di stato: {{statusCode}}\nURL: {{method}} {{url}}\nMessaggio: {{message}}",
"traefik_blocks_requests": "Se si utilizza il proxy inverso Traefik, è stata introdotta una modifica sostanziale che influisce sulla comunicazione con il server."
},
"tab_history_navigation_buttons": {
"go-back": "Torna alla nota precedente",
"go-forward": "Passa alla nota successiva"
},
"breadcrumb_badges": {
"read_only_explicit": "Sola lettura",
"read_only_explicit_description": "Questa nota è stata impostata manualmente come di sola lettura.\nClicca per modificarla temporaneamente.",
"read_only_auto": "Solo lettura automatica",
"read_only_auto_description": "Questa nota è stata impostata automaticamente in modalità di sola lettura per motivi di prestazioni. Questo limite automatico è modificabile dalle impostazioni.\n\nClicca per modificarla temporaneamente.",
"read_only_temporarily_disabled": "Modificabile temporaneamente",
"read_only_temporarily_disabled_description": "Questa nota è attualmente modificabile, ma normalmente è di sola lettura. La nota tornerà ad essere di sola lettura non appena passerai a un'altra nota.\n\nClicca per riattivare la modalità di sola lettura.",
"shared_publicly": "Condiviso pubblicamente",
"shared_publicly_description": "Questa nota è stata pubblicata online all'indirizzo {{- link}} ed è accessibile al pubblico.\n\nClicca per visualizzare la nota condivisa o clicca con il tasto destro del mouse per ulteriori opzioni.",
"shared_locally": "Condiviso localmente",
"shared_locally_description": "Questa nota è condivisa sulla rete locale solo all'indirizzo {{- link}}.\n\nClicca per accedere alla nota condivisa o clicca con il tasto destro del mouse per ulteriori opzioni."
}
}

View File

@@ -218,8 +218,7 @@
"unknown_search_option": "不明な検索オプション {{searchOptionName}}",
"search_note_saved": "検索ノートが {{- notePathTitle}} に保存されました",
"actions_executed": "アクションが実行されました。",
"ancestor": "祖先:",
"view_options": "表示オプション:"
"ancestor": "祖先:"
},
"shortcuts": {
"multiple_shortcuts": "同じアクションに対して複数のショートカットを設定する場合、カンマで区切ることができます。",
@@ -259,7 +258,7 @@
"export_in_progress": "エクスポート処理中: {{progressCount}}",
"export_finished_successfully": "エクスポートが正常に完了しました。",
"format_pdf": "PDF - 印刷または共有目的に。",
"share-format": "web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 web サイトとして公開できます。"
"share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
},
"help": {
"title": "チートシート",
@@ -459,13 +458,7 @@
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
"note_attachments": "ノートの添付ファイル",
"open_note_on_server": "サーバー上のノートを開く",
"view_revisions": "ノートの変更履歴...",
"note_map": "ノートマップ",
"advanced": "高度",
"export_as_image": "画像としてエクスポート",
"export_as_image_png": "PNG (raster)",
"export_as_image_svg": "SVG (vector)"
"open_note_on_server": "サーバー上のノートを開く"
},
"command_palette": {
"export_note_title": "ノートをエクスポート",
@@ -590,7 +583,7 @@
"file_type": "ファイルタイプ",
"file_size": "ファイルサイズ",
"download": "ダウンロード",
"open": "外部で開く",
"open": "開く",
"title": "ファイル",
"upload_new_revision": "編集履歴をアップロード",
"original_file_name": "元のファイル名",
@@ -606,8 +599,7 @@
"calculate": "計算",
"subtree_size": "(サブツリーサイズ: {{size}}、ノード数: {{count}}",
"title": "ノート情報",
"note_size_info": "ノートのサイズは、このノートに必要なストレージの概算を示します。これは、ノートの内容とそのノートの編集履歴の内容を考慮したものです。",
"show_similar_notes": "類似のノートを表示"
"note_size_info": "ノートのサイズは、このノートに必要なストレージの概算を示します。これは、ノートの内容とそのノートの編集履歴の内容を考慮したものです。"
},
"image_properties": {
"file_type": "ファイルタイプ",
@@ -807,7 +799,7 @@
},
"web_view": {
"web_view": "Web ビュー",
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
"embed_websites": "Web ビュータイプでは、ウェブサイトをTriliumに埋め込むことができます。",
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
@@ -969,7 +961,7 @@
"password": {
"wiki": "wiki",
"heading": "パスワード",
"alert_message": "新しいパスワードは大切に保管してください。パスワードは web インターフェースへのログインや、保護されたノートの暗号化に使用されます。パスワードを忘れると、保護されたノートはすべて永久に失われます。",
"alert_message": "新しいパスワードは大切に保管してください。パスワードはウェブインターフェースへのログインや、保護されたノートの暗号化に使用されます。パスワードを忘れると、保護されたノートはすべて永久に失われます。",
"reset_link": "リセットするにはここをクリック。",
"old_password": "旧パスワード",
"new_password": "新パスワード",
@@ -1115,7 +1107,7 @@
"sql_console_home": "SQLコンソールートのデフォルトの場所",
"bookmark_folder": "このラベルの付いたノートは、ブックマークにフォルダとして表示されます(子フォルダへのアクセスを許可します)",
"share_hidden_from_tree": "このートは左側のナビゲーションツリーには表示されていませんが、URL からアクセスできます",
"share_external_link": "ノートは共有ツリー内で外部 web サイトへのリンクとして機能します",
"share_external_link": "ノートは共有ツリー内で外部ウェブサイトへのリンクとして機能します",
"share_alias": "https://your_trilium_host/share/[your_alias] でノートを利用できるようにエイリアスを定義します",
"share_omit_default_css": "デフォルトの共有ページのCSSは省略されます。スタイルを大幅に変更する場合に使用してください。",
"share_root": "/share root で提供されるノートをマークする。",
@@ -1241,14 +1233,7 @@
"none_yet": "アクションを上のリストからクリックして追加。"
},
"note_title": {
"placeholder": "ここにノートのタイトルを入力...",
"created_on": "作成日 <Value />",
"last_modified": "最終更新日 <Value />",
"note_type_switcher_label": "{{type}} から切り替え:",
"note_type_switcher_others": "その他のノートタイプ",
"note_type_switcher_templates": "テンプレート",
"note_type_switcher_collection": "コレクション",
"edited_notes": "編集済みノート"
"placeholder": "ここにノートのタイトルを入力..."
},
"search_result": {
"no_notes_found": "指定された検索パラメータに該当するノートは見つかりませんでした。",
@@ -1345,9 +1330,8 @@
"minimum_input": "入力された時間値は {{minimumSeconds}} 秒以上である必要があります。"
},
"note_language": {
"not_set": "言語が設定されていません",
"configure-languages": "言語を設定...",
"help-on-languages": "コンテンツの言語に関するヘルプ..."
"not_set": "未設定",
"configure-languages": "言語を設定..."
},
"content_language": {
"title": "コンテンツの言語",
@@ -1636,7 +1620,7 @@
"remove_this_attribute": "この属性を削除",
"remove_color": "このカラーラベルを削除",
"promoted_attributes": "プロモート属性",
"url_placeholder": "http://web サイト..."
"url_placeholder": "http://ウェブサイト..."
},
"relation_map": {
"open_in_new_tab": "新しいタブで開く",
@@ -1795,7 +1779,7 @@
"placeholder": "ここにノートの内容を入力...",
"auto-detect-language": "自動検出",
"keeps-crashing": "編集コンポーネントがクラッシュし続けます。Trilium を再起動してください。問題が解決しない場合は、バグレポートの作成をご検討ください。",
"editor_crashed_title": "テキストエディタがクラッシュしました",
"editor_crashed_title": "テキストエディタがクラッシュしました",
"editor_crashed_content": "コンテンツは正常に復元されましたが、最近の変更の一部が保存されていない可能性があります。",
"editor_crashed_details_button": "詳細を見る...",
"editor_crashed_details_intro": "このエラーが何度も発生する場合は、以下の情報を貼り付けて GitHub に報告することを検討してください。",
@@ -1990,7 +1974,7 @@
"book_properties_config": {
"hide-weekends": "週末を非表示",
"display-week-numbers": "週番号を表示",
"map-style": "マップスタイル",
"map-style": "マップスタイル:",
"max-nesting-depth": "最大階層の深さ:",
"show-scale": "スケールを表示",
"raster": "Raster",
@@ -2085,7 +2069,7 @@
"recovery_keys_used": "使用日: {{date}}",
"recovery_keys_unused": "回復コード {{index}} は未使用です",
"oauth_title": "OAuth/OpenID",
"oauth_description": "OpenIDは、Googleなどの他のサービスのアカウントを使用して web サイトにログインし、本人確認を行うための標準化された方法です。デフォルトの発行者はGoogleですが、他のOpenIDプロバイダに変更できます。詳しくは<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">こちら</a>をご覧ください。Google経由でOpenIDサービスを設定するには、<a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">こちらの手順</a>に従ってください。",
"oauth_description": "OpenIDは、Googleなどの他のサービスのアカウントを使用してウェブサイトにログインし、本人確認を行うための標準化された方法です。デフォルトの発行者はGoogleですが、他のOpenIDプロバイダに変更できます。詳しくは<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">こちら</a>をご覧ください。Google経由でOpenIDサービスを設定するには、<a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">こちらの手順</a>に従ってください。",
"oauth_description_warning": "OAuth/OpenIDを有効にするには、config.iniファイルにOAuth/OpenIDのベースURL、クライアントID、クライアントシークレットを設定し、アプリケーションを再起動する必要があります。環境変数から設定する場合は、TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID and TRILIUM_OAUTH_CLIENT_SECRET を設定してください。",
"oauth_missing_vars": "設定がありません: {{-variables}}",
"oauth_user_account": "ユーザーアカウント: ",
@@ -2132,46 +2116,5 @@
"unknown_http_error_title": "サーバーとの通信エラー",
"unknown_http_error_content": "ステータスコード: {{statusCode}}\nURL: {{method}} {{url}}\nメッセージ: {{message}}",
"traefik_blocks_requests": "Traefik リバース プロキシを使用している場合、サーバーとの通信に影響する重大な変更が導入されました。"
},
"tab_history_navigation_buttons": {
"go-back": "前のノートに戻る",
"go-forward": "次のノートに進む"
},
"experimental_features": {
"title": "実験オプション",
"disclaimer": "これらのオプションは試験的なもので、動作が不安定になる可能性があります。注意してご使用ください。",
"new_layout_name": "新しいレイアウト",
"new_layout_description": "よりモダンな外観と使いやすさが向上した新しいレイアウトをお試しください。今後のリリースで大幅な変更が加えられる可能性があります。"
},
"breadcrumb_badges": {
"read_only_explicit": "読み取り専用",
"read_only_auto": "自動的に読み取り専用",
"shared_publicly": "公開で共有",
"shared_locally": "ローカルで共有",
"read_only_explicit_description": "このノートは手動で読み取り専用に設定されています。\nクリックすると一時的に編集できます。",
"read_only_temporarily_disabled": "一時的に編集可能",
"read_only_auto_description": "このノートはパフォーマンス上の理由により、自動的に読み取り専用モードに設定されました。この自動制限は設定から調整できます。\n\n一時的に編集するにはクリックしてください。",
"read_only_temporarily_disabled_description": "このノートは現在編集可能ですが、通常は読み取り専用です。別のノートに移動すると読み取り専用に戻ります。\n\nクリックすると読み取り専用モードが再度有効になります。",
"shared_publicly_description": "このノートは {{- link}} でオンライン公開されており、誰でも閲覧可能です。\n\n共有ートに移動するにはクリックするか、右クリックしてその他のオプションを選択してください。",
"shared_locally_description": "このノートは、{{- link}} でローカルネットワークのみで共有されています。\n\nクリックして共有ートに移動するか、右クリックしてその他のオプションを選択してください。",
"clipped_note": "Web クリップ",
"clipped_note_description": "このノートは {{url}} から取得されました。\n\nクリックすると元の web ページに移動します。",
"execute_script": "スクリプトを実行",
"execute_script_description": "このノートはスクリプトノートです。クリックするとスクリプトが実行されます。",
"execute_sql": "SQL を実行",
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。"
},
"status_bar": {
"language_title": "コンテンツの言語を変更",
"note_info_title": "ノート情報を表示(例: 日付、ノートのサイズなど)",
"backlinks_title_other": "バックリンクを表示",
"attachments_title_other": "添付ファイルを新しいタブで表示",
"attributes_other": "{{count}} 個の属性",
"attributes_title": "所有属性と継承属性",
"note_paths_title": "ノートパス",
"code_note_switcher": "言語モードを変更",
"backlinks_other": "{{count}} バックリンク",
"attachments_other": "{{count}} 件の添付ファイル",
"note_paths_other": "{{count}} 個のパス"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -493,12 +493,7 @@
"editable_text": {
"placeholder": "Scrieți conținutul notiței aici...",
"auto-detect-language": "Automat",
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă.",
"editor_crashed_title": "Editorul text a avut o eroare",
"editor_crashed_content": "Conținutul a fost recuperat cu succes, dar este posibil ca o parte din cele mai recente modificări ale dvs. să nu se fi salvat.",
"editor_crashed_details_button": "Mai multe detalii...",
"editor_crashed_details_intro": "Dacă întâmpinați frecvent această eroare, considerați să o raportați pe GitHub copiând informația de mai jos.",
"editor_crashed_details_title": "Informații tehnice"
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
},
"edited_notes": {
"deleted": "(șters)",
@@ -790,8 +785,7 @@
"info": {
"closeButton": "Închide",
"modalTitle": "Mesaj informativ",
"okButton": "OK",
"copy_to_clipboard": "Copiază în clipboard"
"okButton": "OK"
},
"inherited_attribute_list": {
"no_inherited_attributes": "Niciun atribut moștenit.",
@@ -873,14 +867,12 @@
"print_note": "Imprimare notiță",
"re_render_note": "Reinterpretare notiță",
"save_revision": "Salvează o nouă revizie",
"advanced": "Advansat",
"search_in_note": "Caută în notiță",
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?",
"print_pdf": "Exportare ca PDF...",
"open_note_on_server": "Deschide notița pe server",
"view_revisions": "Revizii ale notițelor..."
"open_note_on_server": "Deschide notița pe server"
},
"note_erasure_timeout": {
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
@@ -1415,7 +1407,7 @@
"hoist-note": "Focalizează notița",
"unhoist-note": "Defocalizează notița",
"converted-to-attachments": "{{count}} notițe au fost convertite în atașamente.",
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte? Această operațiune se aplică doar notițelor de tip imagine, celelalte vor fi ignorate.",
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte?",
"open-in-popup": "Editare rapidă",
"archive": "Arhivează",
"unarchive": "Dezarhivează"
@@ -1534,9 +1526,7 @@
"printing_pdf": "Exportare ca PDF în curs..."
},
"note_title": {
"placeholder": "introduceți titlul notiței aici...",
"created_on": "Creată la <Value />",
"last_modified": "Modificată la <Value />"
"placeholder": "introduceți titlul notiței aici..."
},
"revisions_snapshot_limit": {
"erase_excess_revision_snapshots": "Șterge acum reviziile excesive",
@@ -1768,8 +1758,7 @@
},
"note_language": {
"configure-languages": "Configurează limbile...",
"not_set": "Nicio limbă setată",
"help-on-languages": "Informații despre limba conținutului..."
"not_set": "Nedefinită"
},
"png_export_button": {
"button_title": "Exportă diagrama ca PNG"
@@ -1965,8 +1954,7 @@
"oauth_user_not_logged_in": "Neautentificat!"
},
"svg": {
"export_to_png": "Diagrama nu a putut fi exportată în PNG.",
"export_to_svg": "Diagrama nu a putut fi exportată în SVG."
"export_to_png": "Diagrama nu a putut fi exportată în PNG."
},
"code_theme": {
"title": "Afișare",
@@ -2118,32 +2106,5 @@
},
"popup-editor": {
"maximize": "Comută la editorul principal"
},
"experimental_features": {
"title": "Opțiuni experimentale",
"disclaimer": "Aceste opțiuni sunt experimentale și pot cauza instabilitate. Folosiți cu prudență.",
"new_layout_name": "Aspect nou",
"new_layout_description": "Încercați noul aspect pentru un design mai modern și mai ușor de utilizat. Poate surveni modificări semnificative în următoarele release-uri."
},
"server": {
"unknown_http_error_title": "Eroare de comunicare cu server-ul",
"unknown_http_error_content": "Cod: {{statusCode}}\nURL: {{method}} {{url}}\nMesaj: {{message}}",
"traefik_blocks_requests": "Dacă utilizați reverse proxy-ul Traefik, acesta a introdus o schimbare majoră ce afectează comunicarea cu server-ul."
},
"tab_history_navigation_buttons": {
"go-back": "Înapoi la notița anterioară",
"go-forward": "Înainte către notița următoare"
},
"breadcrumb_badges": {
"read_only_explicit": "Mod citire",
"read_only_explicit_description": "Această notiță a fost setată explicit să fie doar în citire.\nClick pentru a o edita temporar.",
"read_only_auto": "Mod citire auto",
"read_only_auto_description": "Această notița a fost setată automată să fie în mod doar de citire din motive de performanță. Această limită automată este ajustabilă din setări.\n\nClick pentru a o edita temporar.",
"read_only_temporarily_disabled": "Editabilă temporar",
"read_only_temporarily_disabled_description": "Această notiță se poate modifica, deși în mod normal ea este doar în citire. Notița va reveni la modul doar în citire imediat ce navigați către altă notiță.\n\nClick pentru a re-activa modul doar în citire.",
"shared_publicly": "Partajată public",
"shared_publicly_description": "Această notiță este publicată online la {{- link}} și este acesibilă public.\n\nClic pentru a naviga la pagina partajată sau click dreapta pentru mai multe opțiuni.",
"shared_locally": "Partajată local",
"shared_locally_description": "Această notiță este partajată doar pe rețeaua locală la {{- link}}.\n\nClic pentru a naviga la pagina partajată sau click dreapta pentru mai multe opțiuni."
}
}

View File

@@ -2055,21 +2055,5 @@
"pagination": {
"total_notes": "{{count}} заметок",
"page_title": "Страница {{startIndex}} - {{endIndex}}"
},
"status_bar": {
"attributes_one": "{{count}} атрибут",
"attributes_few": "{{count}} атрибута",
"attributes_many": "{{count}} атрибутов",
"note_info_title": "Просмотр информации об этой заметке, например, даты создания/изменения или размер.",
"language_title": "Изменить язык всего содержимого"
},
"breadcrumb_badges": {
"execute_sql_description": "Эта заметка - SQL-запрос. Нажмите, чтобы выполнить его.",
"execute_sql": "Выполнить SQL",
"execute_script_description": "Это заметка содержит скрипт. Нажмите, чтобы выполнить его.",
"execute_script": "Выполнить скрипт",
"clipped_note_description": "Эта заметка первоначально взята с сайта {{url}}.\n\nНажмите, чтобы перейти на исходную веб-страницу.",
"shared_publicly": "Доступно публично",
"shared_locally": "Доступно локально"
}
}

View File

@@ -205,8 +205,7 @@
"info": {
"modalTitle": "資訊消息",
"closeButton": "關閉",
"okButton": "確定",
"copy_to_clipboard": "複製到剪貼簿"
"okButton": "確定"
},
"jump_to_note": {
"search_button": "全文搜尋",
@@ -987,12 +986,7 @@
"editable_text": {
"placeholder": "在這裡輸入您的筆記內容…",
"auto-detect-language": "自動檢測",
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在請考慮提交錯誤報告。",
"editor_crashed_title": "文字編輯器崩潰",
"editor_crashed_content": "您的內容已成功恢復,但最近的幾項變更可能未被儲存。",
"editor_crashed_details_button": "檢視更多資訊⋯",
"editor_crashed_details_intro": "若您多次遇到此錯誤,請考慮在 GitHub 回報以下資訊。",
"editor_crashed_details_title": "技術資訊"
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在請考慮提交錯誤報告。"
},
"empty": {
"open_note_instruction": "透過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
@@ -1537,9 +1531,7 @@
"printing_pdf": "正在匯出為 PDF…"
},
"note_title": {
"placeholder": "請輸入筆記標題...",
"created_on": "建立於 {{date}}",
"last_modified": "最後修改於 {{date}}"
"placeholder": "請輸入筆記標題..."
},
"search_result": {
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
@@ -2114,26 +2106,5 @@
},
"popup-editor": {
"maximize": "切換至完整編輯器"
},
"experimental_features": {
"title": "實驗性選項",
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
"new_layout_name": "新版面配置",
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
},
"server": {
"unknown_http_error_title": "與伺服器通訊錯誤",
"unknown_http_error_content": "狀態碼:{{statusCode}}\n網址{{method}} {{url}}\n訊息{{message}}",
"traefik_blocks_requests": "若您正在使用 Traefik 反向代理,該代理已引入一項重大變更影響與伺服器的通訊。"
},
"tab_history_navigation_buttons": {
"go-back": "返回前一筆記",
"go-forward": "前往下一筆記"
},
"breadcrumb_badges": {
"read_only_explicit": "唯讀",
"read_only_auto": "自動唯讀",
"shared_publicly": "公開分享",
"shared_locally": "本地分享"
}
}

View File

@@ -1,6 +1,9 @@
.breadcrumb {
.breadcrumb-row {
position: relative;
align-items: center;
}
.component.breadcrumb {
contain: none;
display: flex;
margin: 0;
align-items: center;
@@ -8,12 +11,7 @@
gap: 0.25em;
flex-wrap: nowrap;
overflow: hidden;
--badge-radius: 6px;
.badge-hoisted {
--color: var(--input-background-color);
color: var(--main-text-color);
}
max-width: 85%;
> span,
> span > span {
@@ -21,10 +19,6 @@
align-items: center;
min-width: 0;
.bx {
margin-inline: 6px;
}
a {
color: inherit;
text-decoration: none;
@@ -51,24 +45,11 @@
}
.dropdown-item span,
.dropdown-item strong,
.breadcrumb-last-item {
.dropdown-item strong {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: block;
max-width: 300px;
}
a.breadcrumb-last-item,
a.breadcrumb-last-item:visited {
text-decoration: none;
color: currentColor;
font-weight: 600;
}
input {
padding: 0 10px;
width: 200px;
}
}

View File

@@ -0,0 +1,166 @@
import "./Breadcrumb.css";
import { useMemo } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import NoteContext from "../components/note_context";
import froca from "../services/froca";
import ActionButton from "./react/ActionButton";
import Dropdown from "./react/Dropdown";
import { FormListItem } from "./react/FormList";
import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./react/hooks";
import Icon from "./react/Icon";
import NoteLink from "./react/NoteLink";
import link_context_menu from "../menus/link_context_menu";
const COLLAPSE_THRESHOLD = 5;
const INITIAL_ITEMS = 2;
const FINAL_ITEMS = 2;
export default function Breadcrumb() {
const { note, noteContext } = useNoteContext();
const notePath = buildNotePaths(noteContext?.notePathArray);
return (
<div className="breadcrumb">
{notePath.length > COLLAPSE_THRESHOLD ? (
<>
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
<Fragment key={item}>
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem notePath={item} />
}
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
</Fragment>
))}
<BreadcrumbCollapsed items={notePath.slice(INITIAL_ITEMS, -FINAL_ITEMS)} noteContext={noteContext} />
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
<Fragment key={item}>
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
<BreadcrumbItem notePath={item} />
</Fragment>
))}
</>
) : (
notePath.map((item, index) => (
<Fragment key={item}>
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem notePath={item} />
}
{(index < notePath.length - 1 || note?.hasChildren()) &&
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
</Fragment>
))
)}
</div>
);
}
function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) {
const note = useMemo(() => froca.getNoteFromCache("root"), []);
useNoteLabel(note, "iconClass");
const title = useNoteProperty(note, "title");
return (note &&
<ActionButton
icon={note.getIcon()}
text={title ?? ""}
onClick={() => noteContext?.setNote("root")}
onContextMenu={(e) => {
e.preventDefault();
link_context_menu.openContextMenu(note.noteId, e);
}}
/>
);
}
function BreadcrumbItem({ notePath }: { notePath: string }) {
return (
<NoteLink
notePath={notePath}
noPreview
/>
);
}
function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
return (
<Dropdown
text={<Icon icon="bx bx-chevron-right" />}
noSelectButtonStyle
buttonClassName="icon-action"
hideToggleArrow
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
>
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
</Dropdown>
);
}
function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
const notePathComponents = notePath.split("/");
const parentNoteId = notePathComponents.at(-1);
const childNotes = useChildNotes(parentNoteId);
return (
<ul className="breadcrumb-child-list">
{childNotes.map((note) => {
const childNotePath = `${notePath}/${note.noteId}`;
return <li key={note.noteId}>
<FormListItem
icon={note.getIcon()}
onClick={() => noteContext?.setNote(childNotePath)}
>
{childNotePath !== activeNotePath
? <span>{note.title}</span>
: <strong>{note.title}</strong>}
</FormListItem>
</li>;
})}
</ul>
);
}
function BreadcrumbCollapsed({ items, noteContext }: { items: string[], noteContext: NoteContext | undefined }) {
return (
<Dropdown
text={<Icon icon="bx bx-dots-horizontal-rounded" />}
noSelectButtonStyle
buttonClassName="icon-action"
hideToggleArrow
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
>
<ul className="breadcrumb-child-list">
{items.map((notePath) => {
const notePathComponents = notePath.split("/");
const noteId = notePathComponents[notePathComponents.length - 1];
const note = froca.getNoteFromCache(noteId);
if (!note) return null;
return <li key={note.noteId}>
<FormListItem
icon={note.getIcon()}
onClick={() => noteContext?.setNote(notePath)}
>
<span>{note.title}</span>
</FormListItem>
</li>;
})}
</ul>
</Dropdown>
);
}
function buildNotePaths(notePathArray: string[] | undefined) {
if (!notePathArray) return [];
let prefix = "";
const output: string[] = [];
for (const notePath of notePathArray) {
output.push(`${prefix}${notePath}`);
prefix += `${notePath}/`;
}
return output;
}

View File

@@ -6,11 +6,12 @@
.floating-buttons-children,
.show-floating-buttons {
position: absolute;
top: var(--floating-buttons-vert-offset, 14px);
top: calc(var(--floating-buttons-vert-offset, 14px) + var(--ribbon-height, 0px) + var(--content-header-height, 0px));
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
display: flex;
flex-direction: row;
z-index: 100;
transition: top 0.3s ease;
}
.note-split.rtl .floating-buttons-children,

View File

@@ -48,12 +48,6 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
const [ visible, setVisible ] = useState(true);
useEffect(() => setVisible(true), [ note ]);
useTriliumEvent("contentSafeMarginChanged", (e) => {
if (e.noteContext === noteContext) {
setTop(e.top);
}
});
return (
<div className="floating-buttons no-print" style={{top}}>
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
@@ -93,9 +87,9 @@ function CloseFloatingButton({ setVisible }: { setVisible(visible: boolean): voi
className="close-floating-buttons-button"
icon="bx bx-chevrons-right"
text={t("hide_floating_buttons_button.button_title")}
onClick={() => setVisible(false)}
onClick={() => setVisible(false)}
noIconActionClass
/>
</div>
);
}
}

View File

@@ -1,27 +1,25 @@
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
import { VNode } from "preact";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import appContext, { EventData, EventNames } from "../components/app_context";
import Component from "../components/component";
import NoteContext from "../components/note_context";
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
import froca from "../services/froca";
import { t } from "../services/i18n";
import { copyImageReferenceToClipboard } from "../services/image";
import { getHelpUrlForNote } from "../services/in_app_help";
import LoadResults from "../services/load_results";
import server from "../services/server";
import toast from "../services/toast";
import tree from "../services/tree";
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
import { ViewTypeOptions } from "./collections/interface";
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
import server from "../services/server";
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
import toast from "../services/toast";
import { t } from "../services/i18n";
import { copyImageReferenceToClipboard } from "../services/image";
import tree from "../services/tree";
import { getHelpUrlForNote } from "../services/in_app_help";
import froca from "../services/froca";
import NoteLink from "./react/NoteLink";
import RawHtml from "./react/RawHtml";
import { ViewTypeOptions } from "./collections/interface";
import attributes from "../services/attributes";
import LoadResults from "../services/load_results";
export interface FloatingButtonContext {
parentComponent: Component;
@@ -39,7 +37,7 @@ function FloatingButton({ className, ...props }: ActionButtonProps) {
className={`floating-button ${className ?? ""}`}
noIconActionClass
{...props}
/>;
/>
}
export type FloatingButtonsList = ((context: FloatingButtonContext) => false | VNode)[];
@@ -78,19 +76,17 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
ToggleReadOnlyButton
];
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = !isNewLayout && (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
return isEnabled && <FloatingButton
text={t("backend_log.refresh")}
icon="bx bx-refresh"
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
/>;
/>
}
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = !isNewLayout && note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
@@ -98,19 +94,19 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
/>;
/>
}
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = !isNewLayout && ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <FloatingButton
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
onClick={() => setReadOnly(!isReadOnly)}
/>;
/>
}
function EditButton({ note, noteContext }: FloatingButtonContext) {
@@ -133,7 +129,7 @@ function EditButton({ note, noteContext }: FloatingButtonContext) {
icon="bx bx-pencil"
className={animationClass}
onClick={() => enableEditing()}
/>;
/>
}
function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
@@ -151,7 +147,7 @@ function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingB
appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId });
}
}}
/>;
/>
}
function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
@@ -169,51 +165,47 @@ function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }
appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId });
}
}}
/>;
/>
}
function RunActiveNoteButton({ note }: FloatingButtonContext) {
const isEnabled = !isNewLayout && (note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium");
const isEnabled = note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium";
return isEnabled && <FloatingButton
icon="bx bx-play"
text={t("code_buttons.execute_button_title")}
triggerCommand="runActiveNote"
/>;
/>
}
function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
const isEnabled = !isNewLayout && note.mime.startsWith("application/javascript;env=");
const isEnabled = note.mime.startsWith("application/javascript;env=");
return isEnabled && <FloatingButton
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
/>;
/>
}
function SaveToNoteButton({ note }: FloatingButtonContext) {
const isEnabled = !isNewLayout && note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely();
const isEnabled = note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely();
return isEnabled && <FloatingButton
icon="bx bx-save"
text={t("code_buttons.save_to_note_button_title")}
onClick={buildSaveSqlToNoteHandler(note)}
/>;
}
export function buildSaveSqlToNoteHandler(note: FNote) {
return async (e: MouseEvent) => {
e.preventDefault();
const { notePath } = await server.post<SaveSqlConsoleResponse>("special-notes/save-sql-console", { sqlConsoleNoteId: note.noteId });
if (notePath) {
toast.showMessage(t("code_buttons.sql_console_saved_message", { "note_path": await tree.getNotePathTitle(notePath) }));
// TODO: This hangs the navigation, for some reason.
//await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
}
};
onClick={async (e) => {
e.preventDefault();
const { notePath } = await server.post<SaveSqlConsoleResponse>("special-notes/save-sql-console", { sqlConsoleNoteId: note.noteId });
if (notePath) {
toast.showMessage(t("code_buttons.sql_console_saved_message", { "note_path": await tree.getNotePathTitle(notePath) }));
// TODO: This hangs the navigation, for some reason.
//await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
}
}}
/>
}
function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) {
const isEnabled = (!isNewLayout && note.type === "relationMap" && isDefaultViewMode);
const isEnabled = (note.type === "relationMap" && isDefaultViewMode);
return isEnabled && (
<>
<FloatingButton
@@ -242,11 +234,11 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
/>
</div>
</>
);
)
}
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
const isEnabled = !isNewLayout && viewType === "geoMap" && !isReadOnly;
const isEnabled = viewType === "geoMap" && !isReadOnly;
return isEnabled && (
<FloatingButton
icon="bx bx-plus-circle"
@@ -258,11 +250,8 @@ function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonCon
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
const isEnabled = (
!isNewLayout
&& ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode
);
const isEnabled = ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode;
return isEnabled && (
<>
@@ -283,11 +272,11 @@ function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonCon
position: "absolute" // Take out of the the hidden image from flexbox to prevent the layout being affected
}} />
</>
);
)
}
function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = !isNewLayout && ["mermaid", "mindMap"].includes(note?.type ?? "")
const isEnabled = ["mermaid", "mindMap"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode;
return isEnabled && (
<>
@@ -303,26 +292,38 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
onClick={() => triggerEvent("exportPng")}
/>
</>
);
)
}
function InAppHelpButton({ note }: FloatingButtonContext) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl && !isNewLayout;
return isEnabled && (
return !!helpUrl && (
<FloatingButton
icon="bx bx-help-circle"
text={t("help-button.title")}
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
/>
);
)
}
function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
const [ popupOpen, setPopupOpen ] = useState(false);
let [ backlinkCount, setBacklinkCount ] = useState(0);
let [ popupOpen, setPopupOpen ] = useState(false);
const backlinksContainerRef = useRef<HTMLDivElement>(null);
const backlinkCount = useBacklinkCount(note, isDefaultViewMode);
function refresh() {
if (!isDefaultViewMode) return;
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
setBacklinkCount(resp.count);
});
}
useEffect(() => refresh(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (needsRefresh(note, loadResults)) refresh();
});
// Determine the max height of the container.
const { windowHeight } = useWindowSize();
@@ -335,7 +336,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
}
}, [ popupOpen, windowHeight ]);
const isEnabled = !isNewLayout && isDefaultViewMode && backlinkCount > 0;
const isEnabled = isDefaultViewMode && backlinkCount > 0;
return (isEnabled &&
<div className="backlinks-widget has-overflow">
<div
@@ -354,34 +355,15 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
);
}
export function useBacklinkCount(note: FNote | null | undefined, isDefaultViewMode: boolean) {
const [ backlinkCount, setBacklinkCount ] = useState(0);
const refresh = useCallback(() => {
if (!note || !isDefaultViewMode) return;
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
setBacklinkCount(resp.count);
});
}, [ isDefaultViewMode, note ]);
useEffect(() => refresh(), [ refresh ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && needsRefresh(note, loadResults)) refresh();
});
return backlinkCount;
}
export function BacklinksList({ note }: { note: FNote }) {
function BacklinksList({ note }: { note: FNote }) {
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
function refresh() {
server.get<BacklinksResponse>(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => {
// prefetch all
const noteIds = backlinks
.filter(bl => "noteId" in bl)
.map((bl) => bl.noteId);
.filter(bl => "noteId" in bl)
.map((bl) => bl.noteId);
await froca.getNotes(noteIds);
setBacklinks(backlinks);
});
@@ -393,7 +375,7 @@ export function BacklinksList({ note }: { note: FNote }) {
});
return backlinks.map(backlink => (
<li>
<div>
<NoteLink
notePath={backlink.noteId}
showNotePath showNoteIcon
@@ -407,7 +389,7 @@ export function BacklinksList({ note }: { note: FNote }) {
<RawHtml html={excerpt} />
))
)}
</li>
</div>
));
}

View File

@@ -1,18 +1,16 @@
import "./NoteDetail.css";
import { isValidElement, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
import { useNoteContext, useTriliumEvent } from "./react/hooks"
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import { t } from "../services/i18n";
import protected_session_holder from "../services/protected_session_holder";
import toast from "../services/toast.js";
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
import { useNoteContext, useTriliumEvent } from "./react/hooks";
import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
import { isValidElement, VNode } from "preact";
import { TypeWidgetProps } from "./type_widgets/type_widget";
import "./NoteDetail.css";
import attributes from "../services/attributes";
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
import toast from "../services/toast.js";
import { t } from "../services/i18n";
/**
* The note detail is in charge of rendering the content of a note, by determining its type (e.g. text, code) and using the appropriate view widget.
@@ -82,7 +80,7 @@ export default function NoteDetail() {
parentComponent.handleEvent("noteTypeMimeChanged", { noteId: note.noteId });
} else if (note.noteId
&& loadResults.isNoteReloaded(note.noteId, parentComponent.componentId)
&& (type !== (await getExtendedWidgetType(note, noteContext)) || mime !== note?.mime)) {
&& (type !== (await getWidgetType(note, noteContext)) || mime !== note?.mime)) {
// this needs to have a triggerEvent so that e.g., note type (not in the component subtree) is updated
parentComponent.triggerEvent("noteTypeMimeChanged", { noteId: note.noteId });
} else {
@@ -214,7 +212,7 @@ export default function NoteDetail() {
isVisible={type === itemType}
isFullHeight={isFullHeight}
props={props}
/>;
/>
})}
</div>
);
@@ -256,7 +254,7 @@ function useNoteInfo() {
const [ mime, setMime ] = useState<string>();
function refresh() {
getExtendedWidgetType(actualNote, noteContext).then(type => {
getWidgetType(actualNote, noteContext).then(type => {
setNote(actualNote);
setType(type);
setMime(actualNote?.mime);
@@ -284,12 +282,12 @@ async function getCorrespondingWidget(type: ExtendedNoteType): Promise<null | Ty
} else if (isValidElement(result)) {
// Direct VNode provided.
return result;
} else {
return result;
}
return result;
}
export async function getExtendedWidgetType(note: FNote | null | undefined, noteContext: NoteContext | undefined): Promise<ExtendedNoteType | undefined> {
async function getWidgetType(note: FNote | null | undefined, noteContext: NoteContext | undefined): Promise<ExtendedNoteType | undefined> {
if (!noteContext) return undefined;
if (!note) {
// If the note is null, then it's a new tab. If it's undefined, then it's not loaded yet.
@@ -301,10 +299,8 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
if (noteContext?.viewScope?.viewMode === "source") {
resultingType = "readOnlyCode";
} else if (noteContext.viewScope?.viewMode === "attachments") {
} else if (noteContext?.viewScope && noteContext.viewScope.viewMode === "attachments") {
resultingType = noteContext.viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (noteContext.viewScope?.viewMode === "note-map") {
resultingType = "noteMap";
} else if (type === "text" && (await noteContext?.isReadOnly())) {
resultingType = "readOnlyText";
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
@@ -326,7 +322,7 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
return resultingType;
}
export function checkFullHeight(noteContext: NoteContext | undefined, type: ExtendedNoteType | undefined) {
function checkFullHeight(noteContext: NoteContext | undefined, type: ExtendedNoteType | undefined) {
if (!noteContext) return false;
// https://github.com/zadam/trilium/issues/2522

View File

@@ -1,21 +1,19 @@
import "./PromotedAttributes.css";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import FAttribute from "../entities/fattribute";
import FNote from "../entities/fnote";
import "./PromotedAttributes.css";
import { useNoteContext, useNoteLabel, useTriliumEvent, useUniqueName } from "./react/hooks";
import { Attribute } from "../services/attribute_parser";
import attributes from "../services/attributes";
import debounce from "../services/debounce";
import FAttribute from "../entities/fattribute";
import clsx from "clsx";
import { t } from "../services/i18n";
import { DefinitionObject, extractAttributeDefinitionTypeAndName, LabelType } from "../services/promoted_attribute_definition_parser";
import server from "../services/server";
import ws from "../services/ws";
import { useNoteContext, useNoteLabel, useTriliumEvent, useUniqueName } from "./react/hooks";
import FNote from "../entities/fnote";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import NoteAutocomplete from "./react/NoteAutocomplete";
import ws from "../services/ws";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import attributes from "../services/attributes";
import debounce from "../services/debounce";
interface Cell {
uniqueId: string;
@@ -41,15 +39,6 @@ type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
export default function PromotedAttributes() {
const { note, componentId } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
}
export function PromotedAttributesContent({ note, componentId, cells, setCells }: {
note: FNote | null | undefined;
componentId: string;
cells: Cell[] | undefined;
setCells: Dispatch<StateUpdater<Cell[] | undefined>>;
}) {
const [ cellToFocus, setCellToFocus ] = useState<Cell>();
return (
@@ -73,7 +62,7 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells }
*
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
*/
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
const [ viewType ] = useNoteLabel(note, "viewType");
const [ cells, setCells ] = useState<Cell[]>();
@@ -167,7 +156,7 @@ function PromotedAttributeCell(props: CellProps) {
{correspondingInput}
<MultiplicityCell {...props} />
</div>
);
)
}
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = {
@@ -230,29 +219,29 @@ function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) {
<label className="tn-checkbox">{inputNode}</label>
</div>
<label for={inputId}>{definition.promotedAlias ?? valueName}</label>
</>;
</>
} else {
return (
<div className="input-group">
{inputNode}
{ definition.labelType === "color" && <ColorPicker {...props} onChange={onChangeListener} inputId={inputId} />}
{ definition.labelType === "url" && (
<InputButton
className="open-external-link-button"
icon="bx bx-window-open"
title={t("promoted_attributes.open_external_link")}
onClick={(e) => {
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
const url = inputEl?.value;
if (url) {
window.open(url, "_blank");
}
}}
/>
)}
</div>
);
}
return (
<div className="input-group">
{inputNode}
{ definition.labelType === "color" && <ColorPicker {...props} onChange={onChangeListener} inputId={inputId} />}
{ definition.labelType === "url" && (
<InputButton
className="open-external-link-button"
icon="bx bx-window-open"
title={t("promoted_attributes.open_external_link")}
onClick={(e) => {
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
const url = inputEl?.value;
if (url) {
window.open(url, "_blank");
}
}}
/>
)}
</div>
);
}
@@ -293,7 +282,7 @@ function ColorPicker({ cell, onChange, inputId }: CellProps & {
}}
/>
</>
);
)
}
function RelationInput({ inputId, ...props }: CellProps & { inputId: string }) {
@@ -306,7 +295,7 @@ function RelationInput({ inputId, ...props }: CellProps & { inputId: string }) {
await updateAttribute(note, cell, componentId, value, setCells);
}}
/>
);
)
}
function MultiplicityCell({ cell, cells, setCells, setCellToFocus, note, componentId }: CellProps) {
@@ -357,13 +346,13 @@ function MultiplicityCell({ cell, cells, setCells, setCellToFocus, note, compone
name: cell.valueName,
value: ""
}
});
})
}
setCells(cells.toSpliced(index, 1, ...newOnesToInsert));
}}
/>
</td>
);
)
}
function PromotedActionButton({ icon, title, onClick }: {
@@ -377,7 +366,7 @@ function PromotedActionButton({ icon, title, onClick }: {
title={title}
onClick={onClick}
/>
);
)
}
function InputButton({ icon, className, title, onClick }: {
@@ -392,7 +381,7 @@ function InputButton({ icon, className, title, onClick }: {
title={title}
onClick={onClick}
/>
);
)
}
function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, onChangeListener: OnChangeListener) {
@@ -417,7 +406,7 @@ function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute,
[
{
displayKey: "value",
source (term, cb) {
source: function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));

View File

@@ -12,7 +12,6 @@ import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js";
import type { Attribute } from "../../services/attribute_parser.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
const TPL = /*html*/`
<div class="attr-detail tn-tool-dialog">
@@ -310,8 +309,6 @@ interface SearchRelatedResponse {
count: number;
}
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default class AttributeDetailWidget extends NoteContextAwareWidget {
private $title!: JQuery<HTMLElement>;
private $inputName!: JQuery<HTMLElement>;
@@ -582,13 +579,6 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
.css("top", y - offset.top + 70)
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
if (isNewLayout) {
this.$widget
.css("top", "unset")
.css("bottom", 70)
.css("max-height", "80vh");
}
if (focus === "name") {
this.$inputName.trigger("focus").trigger("select");
}

View File

@@ -1,19 +1,16 @@
import Dropdown from "../react/Dropdown";
import "./global_menu.css";
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool, useTriliumOptionInt } from "../react/hooks";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem } from "../react/FormList";
import { CommandNames } from "../../components/app_context";
import KeyboardShortcut from "../react/KeyboardShortcut";
import { KeyboardActionNames } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import Component from "../../components/component";
import { ExperimentalFeature, ExperimentalFeatureId, experimentalFeatures, isExperimentalFeatureEnabled, toggleExperimentalFeature } from "../../services/experimental_features";
import { t } from "../../services/i18n";
import utils, { dynamicRequire, isElectron, isMobile, reloadFrontendApp } from "../../services/utils";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem } from "../react/FormList";
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool, useTriliumOptionInt } from "../react/hooks";
import KeyboardShortcut from "../react/KeyboardShortcut";
import { ParentComponent } from "../react/react_utils";
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils";
interface MenuItemProps<T> {
icon: string,
@@ -38,7 +35,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
text={<>
{isVerticalLayout && <VerticalLayoutIcon />}
{isUpdateAvailable && <div class="global-menu-button-update-available">
<span className="bx bxs-down-arrow-alt global-menu-button-update-available-button" title={t("update_available.update_available")} />
<span className="bx bxs-down-arrow-alt global-menu-button-update-available-button" title={t("update_available.update_available")}></span>
</div>}
</>}
noDropdownListStyle
@@ -57,7 +54,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
<SwitchToOptions />
<MenuItem command="showLaunchBarSubtree" icon={`bx ${isMobile() ? "bx-mobile" : "bx-sidebar"}`} text={t("global_menu.configure_launchbar")} />
<AdvancedMenu dropStart={!isVerticalLayout} />
<AdvancedMenu />
<MenuItem command="showOptions" icon="bx bx-cog" text={t("global_menu.options")} />
<FormDropdownDivider />
@@ -68,19 +65,18 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
{isUpdateAvailable && <>
<FormListHeader text={t("global_menu.new-version-available")} />
<MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")}
icon="bx bx-download"
text={t("global_menu.download-update", {latestVersion})} />
icon="bx bx-download"
text={t("global_menu.download-update", {latestVersion})} />
</>}
{!isElectron() && <BrowserOnlyOptions />}
{glob.isDev && <DevelopmentOptions dropStart={!isVerticalLayout} />}
</Dropdown>
);
)
}
function AdvancedMenu({ dropStart }: { dropStart: boolean }) {
function AdvancedMenu() {
return (
<FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")} dropStart={dropStart}>
<FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")}>
<MenuItem command="showHiddenSubtree" icon="bx bx-hide" text={t("global_menu.show_hidden_subtree")} />
<MenuItem command="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} />
<FormDropdownDivider />
@@ -93,7 +89,7 @@ function AdvancedMenu({ dropStart }: { dropStart: boolean }) {
{isElectron() && <MenuItem command="openDevTools" icon="bx bx-bug-alt" text={t("global_menu.open_dev_tools")} />}
<KeyboardActionMenuItem command="reloadFrontendApp" icon="bx bx-refresh" text={t("global_menu.reload_frontend")} title={t("global_menu.reload_hint")} />
</FormDropdownSubmenu>
);
)
}
function BrowserOnlyOptions() {
@@ -103,41 +99,14 @@ function BrowserOnlyOptions() {
</>;
}
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
return <>
<FormDropdownDivider />
<FormListItem disabled>Development Options</FormListItem>
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
{experimentalFeatures.map((feature) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
))}
</FormDropdownSubmenu>
</>;
}
function ExperimentalFeatureToggle({ experimentalFeature }: { experimentalFeature: ExperimentalFeature }) {
const featureEnabled = isExperimentalFeatureEnabled(experimentalFeature.id as ExperimentalFeatureId);
return (
<FormListItem
checked={featureEnabled}
title={experimentalFeature.description}
onClick={async () => {
await toggleExperimentalFeature(experimentalFeature.id as ExperimentalFeatureId, !featureEnabled);
reloadFrontendApp();
}}
>{experimentalFeature.name}</FormListItem>
);
}
function SwitchToOptions() {
if (isElectron()) {
return;
} else if (!isMobile()) {
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />;
}
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />;
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />
} else {
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />
}
}
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
@@ -148,7 +117,7 @@ function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProp
onClick={typeof command === "function" ? command : undefined}
disabled={disabled}
active={active}
>{text}</FormListItem>;
>{text}</FormListItem>
}
function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) {
@@ -156,7 +125,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<Keybo
{...props}
command={command}
text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>}
/>;
/>
}
function VerticalLayoutIcon() {
@@ -179,7 +148,7 @@ function VerticalLayoutIcon() {
<path className="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>
);
)
}
function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) {
@@ -203,7 +172,7 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null
}}
className={`dropdown-item-button ${icon}`}
>{children}</a>
);
)
}
return isElectron() ? (
@@ -244,7 +213,7 @@ function ToggleWindowOnTop() {
setIsAlwaysOnTop(newState);
}}
/>
);
)
}
function useTriliumUpdateStatus() {
@@ -255,7 +224,7 @@ function useTriliumUpdateStatus() {
async function updateVersionStatus() {
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
let latestVersion: string | undefined;
let latestVersion: string | undefined = undefined;
try {
const resp = await fetch(RELEASES_API_URL);
const data = await resp.json();

View File

@@ -243,7 +243,7 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
currentValue?: string;
placeholder?: string;
save: (newValue: string) => void | Promise<void>;
save: (newValue: string) => void;
dismiss: () => void;
isNewItem?: boolean;
mode?: "normal" | "multiline" | "relation";

View File

@@ -1 +1,15 @@
/** Intentionally left empty for now **/
.content-header-widget {
position: relative;
z-index: 998;
background-color: var(--main-background-color);
}
.note-split.bgfx .content-header-widget {
background-color: transparent;
}
.content-header-widget.floating {
position: sticky;
top: 0;
background-color: var(--main-background-color, #fff) !important;
}

View File

@@ -2,15 +2,19 @@ import { EventData } from "../../components/app_context";
import BasicWidget from "../basic_widget";
import Container from "./container";
import NoteContext from "../../components/note_context";
import "./content_header.css";
export default class ContentHeader extends Container<BasicWidget> {
noteContext?: NoteContext;
thisElement?: HTMLElement;
parentElement?: HTMLElement;
resizeObserver: ResizeObserver;
currentHeight: number = 0;
currentSafeMargin: number = NaN;
previousScrollTop: number = 0;
isFloating: boolean = false;
scrollThreshold: number = 10; // pixels before triggering float
constructor() {
super();
@@ -35,19 +39,44 @@ export default class ContentHeader extends Container<BasicWidget> {
this.thisElement = this.$widget.get(0)!;
this.resizeObserver.observe(this.thisElement);
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
this.parentElement.addEventListener("scroll", this.updateScrollState.bind(this), { passive: true });
}
updateScrollState() {
const currentScrollTop = this.parentElement!.scrollTop;
const isScrollingUp = currentScrollTop < this.previousScrollTop;
const hasDropdownOpen = this.thisElement!.querySelector(".dropdown-menu.show") !== null;
const hasMovedEnough = Math.abs(currentScrollTop - this.previousScrollTop) > this.scrollThreshold;
if (hasDropdownOpen) {
this.setFloating(true);
} else if (currentScrollTop === 0) {
this.setFloating(false);
} else if (hasMovedEnough) {
this.setFloating(isScrollingUp);
}
this.previousScrollTop = currentScrollTop;
this.updateSafeMargin();
}
setFloating(shouldFloat: boolean) {
if (shouldFloat !== this.isFloating) {
this.isFloating = shouldFloat;
if (shouldFloat) {
this.$widget.addClass("floating");
} else {
this.$widget.removeClass("floating");
}
}
}
updateSafeMargin() {
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
if (newSafeMargin !== this.currentSafeMargin) {
this.currentSafeMargin = newSafeMargin;
this.triggerEvent("contentSafeMarginChanged", {
top: newSafeMargin,
noteContext: this.noteContext!
});
const parentEl = this.parentElement?.closest<HTMLDivElement>(".note-split");
if (this.isFloating || this.parentElement!.scrollTop === 0) {
parentEl!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
} else {
parentEl!.style.removeProperty("--content-header-height");
}
}
@@ -60,4 +89,4 @@ export default class ContentHeader extends Container<BasicWidget> {
}
}
}
}

View File

@@ -5,7 +5,6 @@ import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.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.
@@ -38,7 +37,6 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.#setBackdropEffects();
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
this.#setExperimentalFeatures();
return super.render();
}
@@ -58,7 +56,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
if (loadResults.isOptionReloaded("maxContentWidth")
|| loadResults.isOptionReloaded("centerContent")) {
this.#setMaxContentWidth();
}
}
@@ -101,12 +99,6 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
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) {
const correspondingLocale = LOCALES.find(l => l.id === locale);
document.body.lang = locale;

View File

@@ -49,7 +49,7 @@ export default class ScrollingContainer extends Container<BasicWidget> {
}
}
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerTo">) {
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerToCommand">) {
this.$widget.scrollTop(position);
}
}

View File

@@ -31,7 +31,6 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
flex-grow: 1;
display: flex;
align-items: center;
margin-block: 0;
}
.modal.popup-editor-dialog .modal-header .note-title-widget {

View File

@@ -1,32 +1,26 @@
import "./PopupEditor.css";
import { ComponentChildren } from "preact";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import tree from "../../services/tree";
import utils from "../../services/utils";
import NoteList from "../collections/NoteList";
import FloatingButtons from "../FloatingButtons";
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import NoteDetail from "../NoteDetail";
import PromotedAttributes from "../PromotedAttributes";
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
import Modal from "../react/Modal";
import "./PopupEditor.css";
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
import NoteTitleWidget from "../note_title";
import NoteIcon from "../note_icon";
import NoteContext from "../../components/note_context";
import { NoteContextContext, ParentComponent } from "../react/react_utils";
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
import NoteDetail from "../NoteDetail";
import { ComponentChildren } from "preact";
import NoteList from "../collections/NoteList";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar";
import PromotedAttributes from "../PromotedAttributes";
import FloatingButtons from "../FloatingButtons";
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import utils from "../../services/utils";
import tree from "../../services/tree";
import froca from "../../services/froca";
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
import NoteBadges from "../layout/NoteBadges";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
import { t } from "../../services/i18n";
import appContext from "../../components/app_context";
export default function PopupEditor() {
const [ shown, setShown ] = useState(false);
@@ -67,10 +61,7 @@ export default function PopupEditor() {
<NoteContextContext.Provider value={noteContext}>
<DialogWrapper>
<Modal
title={<>
<TitleRow />
{isNewLayout && <NoteBadges />}
</>}
title={<TitleRow />}
customTitleBarButtons={[{
iconClassName: "bx-expand-alt",
title: t("popup-editor.maximize"),
@@ -84,17 +75,19 @@ export default function PopupEditor() {
className="popup-editor-dialog"
size="lg"
show={shown}
onShown={() => parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId })}
onShown={() => {
parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId });
}}
onHidden={() => setShown(false)}
keepInDom // needed for faster loading
noFocus // automatic focus breaks block popup
>
{!isNewLayout && <ReadOnlyNoteInfoBar />}
<ReadOnlyNoteInfoBar />
<PromotedAttributes />
{isMobile
? <MobileEditorToolbar inPopupEditor />
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
? <MobileEditorToolbar inPopupEditor />
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
<FloatingButtons items={items} />
<NoteDetail />
@@ -102,7 +95,7 @@ export default function PopupEditor() {
</Modal>
</DialogWrapper>
</NoteContextContext.Provider>
);
)
}
export function DialogWrapper({ children }: { children: ComponentChildren }) {
@@ -114,7 +107,7 @@ export function DialogWrapper({ children }: { children: ComponentChildren }) {
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
{children}
</div>
);
)
}
export function TitleRow() {
@@ -123,5 +116,5 @@ export function TitleRow() {
<NoteIcon />
<NoteTitleWidget />
</div>
);
)
}

View File

@@ -8,7 +8,7 @@ import note_types from "../../services/note_types";
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
import { TreeCommandNames } from "../../menus/tree_context_menu";
import { Suggestion } from "../../services/note_autocomplete";
import SimpleBadge from "../react/Badge";
import Badge from "../react/Badge";
import { useTriliumEvent } from "../react/hooks";
export interface ChooseNoteTypeResponse {
@@ -108,7 +108,7 @@ export default function NoteTypeChooserDialogComponent() {
value={[ item.type, item.templateNoteId ].join(",") }
icon={item.uiIcon}>
{item.title}
{item.badges && item.badges.map((badge) => <SimpleBadge {...badge} />)}
{item.badges && item.badges.map((badge) => <Badge {...badge} />)}
</FormListItem>;
}
})}

View File

@@ -2102,8 +2102,8 @@ const icons: Icon[] = [
type_of_icon: "REGULAR"
},
{
name: "border-left",
slug: "border-left-regular",
name: "border-inline-start",
slug: "border-inline-start-regular",
category_id: 111,
type_of_icon: "REGULAR"
},
@@ -10259,9 +10259,9 @@ function getIconClass(icon: Icon) {
return `bxl-${icon.name}`;
} else if (icon.type_of_icon === "SOLID") {
return `bxs-${icon.name}`;
}
return `bx-${icon.name}`;
} else {
return `bx-${icon.name}`;
}
}
for (const icon of icons) {

View File

@@ -1,249 +0,0 @@
import "./Breadcrumb.css";
import { useRef, useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import link_context_menu from "../../menus/link_context_menu";
import { getReadableTextColor } from "../../services/css_class_manager";
import froca from "../../services/froca";
import hoisted_note from "../../services/hoisted_note";
import { t } from "../../services/i18n";
import ActionButton from "../react/ActionButton";
import { Badge } from "../react/Badge";
import Dropdown from "../react/Dropdown";
import { FormListItem } from "../react/FormList";
import { useChildNotes, useNote, useNoteIcon, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useStaticTooltip } from "../react/hooks";
import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink";
const COLLAPSE_THRESHOLD = 5;
const INITIAL_ITEMS = 2;
const FINAL_ITEMS = 2;
export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) {
const notePath = buildNotePaths(noteContext);
return (
<div className="breadcrumb">
{notePath.length > COLLAPSE_THRESHOLD ? (
<>
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
<Fragment key={item}>
<BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
</Fragment>
))}
<BreadcrumbCollapsed items={notePath.slice(INITIAL_ITEMS, -FINAL_ITEMS)} noteContext={noteContext} />
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
<Fragment key={item}>
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
<BreadcrumbItem index={notePath.length - FINAL_ITEMS + index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
</Fragment>
))}
</>
) : (
notePath.map((item, index) => (
<Fragment key={item}>
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
}
{(index < notePath.length - 1 || note?.hasChildren()) &&
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
</Fragment>
))
)}
</div>
);
}
function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) {
const noteId = noteContext?.hoistedNoteId ?? "root";
if (noteId !== "root") {
return <BreadcrumbHoistedNoteRoot noteId={noteId} />;
}
// Root note is icon only.
const note = froca.getNoteFromCache("root");
return (note &&
<ActionButton
className="root-note"
icon={note.getIcon()}
text={""}
onClick={() => noteContext?.setNote(note.noteId)}
onContextMenu={(e) => {
e.preventDefault();
link_context_menu.openContextMenu(note.noteId, e);
}}
/>
);
}
function BreadcrumbHoistedNoteRoot({ noteId }: { noteId: string }) {
const note = useNote(noteId);
const noteIcon = useNoteIcon(note);
const [ workspace ] = useNoteLabelBoolean(note, "workspace");
const [ workspaceIconClass ] = useNoteLabel(note, "workspaceIconClass");
const [ workspaceColor ] = useNoteLabel(note, "workspaceTabBackgroundColor");
// Hoisted workspace shows both text and icon and a way to exit easily out of the hoisting.
return (note &&
<>
<Badge
className="badge-hoisted"
icon={workspace ? (workspaceIconClass || noteIcon) : "bx bxs-chevrons-up"}
text={workspace ? t("breadcrumb.workspace_badge") : t("breadcrumb.hoisted_badge")}
tooltip={t("breadcrumb.hoisted_badge_title")}
onClick={() => hoisted_note.unhoist()}
style={workspaceColor ? {
"--color": workspaceColor,
"color": getReadableTextColor(workspaceColor)
} : undefined}
/>
<NoteLink
notePath={noteId}
showNoteIcon
noPreview
/>
</>
);
}
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
const linkRef = useRef<HTMLAnchorElement>(null);
const noteId = notePath.split("/").at(-1);
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
const title = useNoteProperty(note, "title");
useStaticTooltip(linkRef, {
placement: "top",
title: t("breadcrumb.scroll_to_top_title")
});
if (!note) return null;
return (
<a
ref={linkRef}
href="#"
className="breadcrumb-last-item tn-link"
onClick={() => {
const activeNtxId = appContext.tabManager.activeNtxId;
const scrollingContainer = document.querySelector(`[data-ntx-id="${activeNtxId}"] .scrolling-container`);
scrollingContainer?.scrollTo({ top: 0, behavior: "smooth" });
}}
>{title}</a>
);
}
function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) {
if (index === 0) {
return <BreadcrumbRoot noteContext={noteContext} />;
}
if (index === notePathLength - 1) {
return <>
<BreadcrumbLastItem notePath={notePath} />
</>;
}
return <NoteLink notePath={notePath} />;
}
function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
return (
<Dropdown
text={<Icon icon="bx bx-chevron-right" />}
noSelectButtonStyle
buttonClassName="icon-action"
hideToggleArrow
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}
>
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
</Dropdown>
);
}
function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
const notePathComponents = notePath.split("/");
const parentNoteId = notePathComponents.at(-1);
const childNotes = useChildNotes(parentNoteId);
return (
<ul className="breadcrumb-child-list">
{childNotes.map((note) => {
if (note.noteId === "_hidden") return;
const childNotePath = `${notePath}/${note.noteId}`;
return <li key={note.noteId}>
<FormListItem
icon={note.getIcon()}
onClick={() => noteContext?.setNote(childNotePath)}
>
{childNotePath !== activeNotePath
? <span>{note.title}</span>
: <strong>{note.title}</strong>}
</FormListItem>
</li>;
})}
</ul>
);
}
function BreadcrumbCollapsed({ items, noteContext }: { items: string[], noteContext: NoteContext | undefined }) {
return (
<Dropdown
text={<Icon icon="bx bx-dots-horizontal-rounded" />}
noSelectButtonStyle
buttonClassName="icon-action"
hideToggleArrow
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
>
<ul className="breadcrumb-child-list">
{items.map((notePath) => {
const notePathComponents = notePath.split("/");
const noteId = notePathComponents[notePathComponents.length - 1];
const note = froca.getNoteFromCache(noteId);
if (!note) return null;
return <li key={note.noteId}>
<FormListItem
icon={note.getIcon()}
onClick={() => noteContext?.setNote(notePath)}
>
<span>{note.title}</span>
</FormListItem>
</li>;
})}
</ul>
</Dropdown>
);
}
function buildNotePaths(noteContext: NoteContext) {
const notePathArray = noteContext.notePathArray;
if (!notePathArray) return [];
let prefix = "";
let output: string[] = [];
let pos = 0;
let hoistedNotePos = -1;
for (const notePath of notePathArray) {
if (noteContext.hoistedNoteId !== "root" && notePath === noteContext.hoistedNoteId) {
hoistedNotePos = pos;
}
output.push(`${prefix}${notePath}`);
prefix += `${notePath}/`;
pos++;
}
// When hoisted, display only the path starting with the hoisted note.
if (noteContext.hoistedNoteId !== "root" && hoistedNotePos > -1) {
output = output.slice(hoistedNotePos);
}
return output;
}

View File

@@ -1,113 +0,0 @@
:root {
--title-transition: opacity 200ms ease-in;
}
.component.inline-title {
contain: none;
}
.inline-title {
max-width: var(--max-content-width);
padding-inline-start: 24px;
& > .inline-title-row {
display: flex;
align-items: center;
transition: var(--title-transition);
&.hidden {
opacity: 0;
pointer-events: none;
}
}
&.hidden {
display: none;
}
.note-icon-widget {
padding: 0;
}
.inline-title-row {
border-bottom: 2px solid gray;
}
}
.title-row {
&.note-icon-widget,
&.note-title-widget {
transition: var(--title-transition);
}
&.hide-title .note-icon-widget,
&.hide-title .note-title-widget {
opacity: 0;
pointer-events: none;
}
}
.note-split.type-code:not(.mime-text-x-sqlite) .inline-title {
background-color: var(--main-background-color);
}
body.prefers-centered-content .inline-title {
margin-inline: auto;
}
.title-details {
display: flex;
gap: 0.25em;
margin: 0;
margin-top: 4px;
list-style-type: none;
opacity: .5;
flex-wrap: wrap;
span.value {
font-weight: 500;
}
}
.note-type-switcher {
padding: .25em 0;
display: flex;
align-items: center;
overflow-x: auto;
min-width: 0;
gap: 5px;
min-height: 40px;
--badge-radius: 12px;
>* {
flex-shrink: 0;
}
.ext-badge {
--color: var(--input-background-color);
color: var(--main-text-color);
font-size: 0.9rem;
flex-shrink: 0;
}
}
.edited-notes {
padding: 1.5em 0;
.collapsible-inner-body {
display: flex;
flex-wrap: wrap;
gap: 0.3em;
.badge {
margin: 0;
a.tn-link {
color: inherit;
text-transform: none;
text-decoration: none;
display: inline-block;
}
}
}
}

View File

@@ -1,345 +0,0 @@
import "./InlineTitle.css";
import { NoteType } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChild } from "preact";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { Trans } from "react-i18next";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { ViewScope } from "../../services/link";
import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types";
import server from "../../services/server";
import { formatDateTime } from "../../utils/formatters";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import SimpleBadge, { Badge, BadgeWithDropdown } from "../react/Badge";
import Collapsible from "../react/Collapsible";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteBlob, useNoteContext, useNoteLabel, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils";
import { useEditedNotes } from "../ribbon/EditedNotesTab";
import { useNoteMetadata } from "../ribbon/NoteInfoTab";
import { onWheelHorizontalScroll } from "../widget_utils";
const supportedNoteTypes = new Set<NoteType>([
"text", "code"
]);
export default function InlineTitle() {
const { note, parentComponent, viewScope } = useNoteContext();
const type = useNoteProperty(note, "type");
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
const containerRef = useRef<HTMLDivElement>(null);
const [ titleHidden, setTitleHidden ] = useState(false);
useLayoutEffect(() => {
setShown(shouldShow(note?.noteId, type, viewScope));
}, [ note, type, viewScope ]);
useLayoutEffect(() => {
if (!shown) return;
const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector(":scope > .title-row");
if (!titleRow) return;
titleRow.classList.toggle("hide-title", true);
const observer = new IntersectionObserver((entries) => {
titleRow.classList.toggle("hide-title", entries[0].isIntersecting);
setTitleHidden(!entries[0].isIntersecting);
}, {
threshold: 0.85
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
titleRow.classList.remove("hide-title");
observer.disconnect();
};
}, [ shown, parentComponent ]);
return (
<div
ref={containerRef}
className={clsx("inline-title", !shown && "hidden")}
>
<div class={clsx("inline-title-row", titleHidden && "hidden")}>
<NoteIcon />
<NoteTitleWidget />
</div>
<NoteTitleDetails />
<EditedNotes />
<NoteTypeSwitcher />
</div>
);
}
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
if (viewScope?.viewMode !== "default") return false;
if (noteId?.startsWith("_options")) return true;
return type && supportedNoteTypes.has(type);
}
//#region Title details
export function NoteTitleDetails() {
const { note } = useNoteContext();
const { metadata } = useNoteMetadata(note);
const isHiddenNote = note?.noteId.startsWith("_");
const items: ComponentChild[] = [
(!isHiddenNote && metadata?.dateCreated &&
<TextWithValue
i18nKey="note_title.created_on"
value={formatDateTime(metadata.dateCreated, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateCreated, "full", "long")}
/>),
(!isHiddenNote && metadata?.dateModified &&
<TextWithValue
i18nKey="note_title.last_modified"
value={formatDateTime(metadata.dateModified, "medium", "none")}
valueTooltip={formatDateTime(metadata.dateModified, "full", "long")}
/>)
].filter(item => !!item);
return items.length > 0 && (
<div className="title-details">
{joinElements(items, " • ")}
</div>
);
}
function TextWithValue({ i18nKey, value, valueTooltip }: {
i18nKey: string;
value: string;
valueTooltip: string;
}) {
const listItemRef = useRef<HTMLLIElement>(null);
useStaticTooltip(listItemRef, {
selector: "span.value",
title: valueTooltip,
popperConfig: { placement: "bottom" }
});
return (
<li ref={listItemRef}>
<Trans
i18nKey={i18nKey}
components={{
Value: <span className="value">{value}</span> as React.ReactElement
}}
/>
</li>
);
}
//#endregion
//#region Note type switcher
const SWITCHER_PINNED_NOTE_TYPES = new Set<NoteType>([ "text", "code", "book", "canvas" ]);
function NoteTypeSwitcher() {
const { note } = useNoteContext();
const blob = useNoteBlob(note);
const currentNoteType = useNoteProperty(note, "type");
const { pinnedNoteTypes, restNoteTypes } = useMemo(() => {
const pinnedNoteTypes: NoteTypeMapping[] = [];
const restNoteTypes: NoteTypeMapping[] = [];
for (const noteType of NOTE_TYPES) {
if (noteType.reserved || noteType.static || noteType.type === "book") continue;
if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) {
pinnedNoteTypes.push(noteType);
} else {
restNoteTypes.push(noteType);
}
}
return { pinnedNoteTypes, restNoteTypes };
}, []);
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
<div
className="note-type-switcher"
onWheel={onWheelHorizontalScroll}
>
{note && blob?.contentLength === 0 && (
<>
<div className="intro">{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}</div>
{pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && (
<Badge
key={noteType.type}
text={noteType.title}
icon={`bx ${noteType.icon}`}
onClick={() => switchNoteType(note.noteId, noteType)}
/>
))}
{collectionTemplates.length > 0 && <CollectionNoteTypes noteId={note.noteId} collectionTemplates={collectionTemplates} />}
{builtinTemplates.length > 0 && <TemplateNoteTypes noteId={note.noteId} builtinTemplates={builtinTemplates} />}
{restNoteTypes.length > 0 && <MoreNoteTypes noteId={note.noteId} restNoteTypes={restNoteTypes} />}
</>
)}
</div>
);
}
function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) {
return (
<BadgeWithDropdown
text={t("note_title.note_type_switcher_others")}
icon="bx bx-dots-vertical-rounded"
>
{restNoteTypes.map(noteType => (
<FormListItem
key={noteType.type}
icon={`bx ${noteType.icon}`}
onClick={() => switchNoteType(noteId, noteType)}
>{noteType.title}</FormListItem>
))}
</BadgeWithDropdown>
);
}
function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) {
return (
<BadgeWithDropdown
text={t("note_title.note_type_switcher_collection")}
icon="bx bx-book"
>
{collectionTemplates.map(collectionTemplate => (
<FormListItem
key={collectionTemplate.noteId}
icon={collectionTemplate.getIcon()}
onClick={() => setTemplate(noteId, collectionTemplate.noteId)}
>{collectionTemplate.title}</FormListItem>
))}
</BadgeWithDropdown>
);
}
function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) {
const [ userTemplates, setUserTemplates ] = useState<FNote[]>([]);
async function refreshTemplates() {
const templateNoteIds = await server.get<string[]>("search-templates");
const templateNotes = await froca.getNotes(templateNoteIds);
setUserTemplates(templateNotes);
}
// First load.
useEffect(() => {
refreshTemplates();
}, []);
// React to external changes.
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) {
refreshTemplates();
}
});
return (
<BadgeWithDropdown
text={t("note_title.note_type_switcher_templates")}
icon="bx bx-copy-alt"
>
{userTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
{userTemplates.length > 0 && <FormDropdownDivider />}
{builtinTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
</BadgeWithDropdown>
);
}
function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) {
return (
<FormListItem
icon={template.getIcon()}
onClick={() => setTemplate(noteId, template.noteId)}
>{template.title}</FormListItem>
);
}
function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) {
return server.put(`notes/${noteId}/type`, { type, mime });
}
function setTemplate(noteId: string, templateId: string) {
return attributes.setRelation(noteId, "template", templateId);
}
function useBuiltinTemplates() {
const [ templates, setTemplates ] = useState<{
builtinTemplates: FNote[];
collectionTemplates: FNote[];
}>({
builtinTemplates: [],
collectionTemplates: []
});
async function loadBuiltinTemplates() {
const templatesRoot = await froca.getNote("_templates");
if (!templatesRoot) return;
const childNotes = await templatesRoot.getChildNotes();
const builtinTemplates: FNote[] = [];
const collectionTemplates: FNote[] = [];
for (const childNote of childNotes) {
if (!childNote.hasLabel("template")) continue;
if (childNote.hasLabel("collection")) {
collectionTemplates.push(childNote);
} else {
builtinTemplates.push(childNote);
}
}
setTemplates({ builtinTemplates, collectionTemplates });
}
useEffect(() => {
loadBuiltinTemplates();
}, []);
return templates;
}
//#endregion
//#region Edited Notes
function EditedNotes() {
const { note } = useNoteContext();
const [ dateNote ] = useNoteLabel(note, "dateNote");
const [ editedNotesOpenInRibbon ] = useTriliumOptionBool("editedNotesOpenInRibbon");
return (note && dateNote &&
<Collapsible
className="edited-notes"
title={t("note_title.edited_notes")}
initiallyExpanded={editedNotesOpenInRibbon}
>
<EditedNotesContent note={note} />
</Collapsible>
);
}
function EditedNotesContent({ note }: { note: FNote }) {
const editedNotes = useEditedNotes(note);
return (
<>
{editedNotes?.map(editedNote => (
<SimpleBadge
key={editedNote.noteId}
title={(
<NoteLink
notePath={editedNote.noteId}
showNoteIcon
/>
)}
/>
))}
</>
);
}
//#endregion

View File

@@ -1,27 +0,0 @@
.component.note-badges {
contain: none;
}
.note-badges {
display: flex;
gap: 5px;
min-width: 0;
flex-shrink: 1;
overflow: hidden;
--badge-radius: 12px;
.ext-badge {
&.temporarily-editable-badge { --color: #4fa52b; }
&.read-only-badge { --color: #e33f3b; }
&.share-badge { --color: #3b82f6; }
&.clipped-note-badge { --color: #57a2a5; }
&.execute-badge { --color: #f59e0b; }
}
.dropdown-badge {
&.dropdown-backlinks-badge .dropdown-menu {
min-width: 500px;
}
}
}

View File

@@ -1,108 +0,0 @@
import "./NoteBadges.css";
import { copyTextWithToast } from "../../services/clipboard_ext";
import { t } from "../../services/i18n";
import { goToLinkExt } from "../../services/link";
import { Badge, BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
export default function NoteBadges() {
return (
<div className="note-badges">
<ReadOnlyBadge />
<ShareBadge />
<ClippedNoteBadge />
<ExecuteBadge />
</div>
);
}
function ReadOnlyBadge() {
const { note, noteContext } = useNoteContext();
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
const isTemporarilyEditable = noteContext?.ntxId !== "_popup-editor" && noteContext?.viewScope?.readOnlyTemporarilyDisabled;
if (isTemporarilyEditable) {
return <Badge
icon="bx bx-lock-open-alt"
text={t("breadcrumb_badges.read_only_temporarily_disabled")}
tooltip={t("breadcrumb_badges.read_only_temporarily_disabled_description")}
className="temporarily-editable-badge"
onClick={() => enableEditing(false)}
/>;
} else if (isReadOnly) {
return <Badge
icon="bx bx-lock-alt"
text={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")}
tooltip={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit_description") : t("breadcrumb_badges.read_only_auto_description")}
className="read-only-badge"
onClick={() => enableEditing()}
/>;
}
}
function ShareBadge() {
const { note } = useNoteContext();
const [ , switchShareState ] = useShareState(note);
const { isSharedExternally, linkHref } = useShareInfo(note);
return (linkHref &&
<BadgeWithDropdown
icon={isSharedExternally ? "bx bx-world" : "bx bx-share-alt"}
text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")}
className="share-badge"
>
<FormListItem
icon="bx bx-copy"
onClick={() => copyTextWithToast(linkHref)}
>{t("breadcrumb_badges.shared_copy_to_clipboard")}</FormListItem>
<FormListItem
icon="bx bx-link-external"
onClick={(e) => goToLinkExt(e, linkHref)}
>{t("breadcrumb_badges.shared_open_in_browser")}</FormListItem>
<FormDropdownDivider />
<FormListItem
icon="bx bx-unlink"
onClick={() => switchShareState(false)}
>{t("breadcrumb_badges.shared_unshare")}</FormListItem>
</BadgeWithDropdown>
);
}
function ClippedNoteBadge() {
const { note } = useNoteContext();
const [ pageUrl ] = useNoteLabel(note, "pageUrl");
return (pageUrl &&
<Badge
className="clipped-note-badge"
icon="bx bx-globe"
text={t("breadcrumb_badges.clipped_note")}
tooltip={t("breadcrumb_badges.clipped_note_description", { url: pageUrl })}
href={pageUrl}
/>
);
}
function ExecuteBadge() {
const { note, parentComponent } = useNoteContext();
const isScript = note?.isTriliumScript();
const isSql = note?.isTriliumSqlite();
const isExecutable = isScript || isSql;
const [ executeDescription ] = useNoteLabel(note, "executeDescription");
const [ executeButton ] = useNoteLabelBoolean(note, "executeButton");
return (note && isExecutable && (executeDescription || executeButton) &&
<Badge
className="execute-badge"
icon="bx bx-play"
text={isScript ? t("breadcrumb_badges.execute_script") : t("breadcrumb_badges.execute_sql")}
tooltip={executeDescription || (isScript ? t("breadcrumb_badges.execute_script_description") : t("breadcrumb_badges.execute_sql_description"))}
onClick={() => parentComponent.triggerCommand("runActiveNote")}
/>
);
}

View File

@@ -1,11 +0,0 @@
body.experimental-feature-new-layout {
.component.title-actions {
contain: none;
}
.title-actions {
&.visible {
padding: 0.75em 15px;
}
}
}

View File

@@ -1,70 +0,0 @@
import "./NoteTitleActions.css";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import CollectionProperties from "../note_bars/CollectionProperties";
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
export default function NoteTitleActions() {
const { note, ntxId, componentId, noteContext } = useNoteContext();
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
const noteType = useNoteProperty(note, "type");
const items = [
note && <PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />,
note && noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />,
note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />
].filter(Boolean);
return (
<div className={clsx("title-actions", items.length > 0 && "visible")}>
{items}
</div>
);
}
function SearchProperties({ note, ntxId }: { note: FNote, ntxId: string | null | undefined }) {
return (note &&
<Collapsible
title={t("search_definition.search_parameters")}
initiallyExpanded={note.isInHiddenSubtree()} // not saved searches
>
<SearchDefinitionTab note={note} ntxId={ntxId} hidden={false} />
</Collapsible>
);
}
function PromotedAttributes({ note, componentId, noteContext }: {
note: FNote | null | undefined,
componentId: string,
noteContext: NoteContext | undefined
}) {
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
const [ expanded, setExpanded ] = useState(false);
useEffect(() => {
getExtendedWidgetType(note, noteContext).then(extendedNoteType => {
const fullHeight = checkFullHeight(noteContext, extendedNoteType);
setExpanded(!fullHeight);
});
}, [ note, noteContext ]);
if (!cells?.length) return false;
return (note && (
<ExternallyControlledCollapsible
key={note.noteId}
title={t("promoted_attributes.promoted_attributes")}
expanded={expanded} setExpanded={setExpanded}
>
<PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />
</ExternallyControlledCollapsible>
));
}

View File

@@ -1,263 +0,0 @@
.component.status-bar {
contain: none;
border-top: 1px solid var(--main-border-color);
background-color: var(--left-pane-background-color);
> .status-bar-main-row {
min-height: 28px;
display: flex;
align-items: center;
padding-inline: 0.25em;
font-size: 0.85em;
> .breadcrumb {
flex-grow: 1;
--icon-button-size: 23px;
}
> .actions-row {
padding: 0.1em;
display: flex;
gap: 0.1em;
.btn {
padding: 0 0.5em !important;
background: transparent;
display: flex;
align-items: center;
border: 0;
span:first-of-type {
font-size: 1rem;
}
&.active,
&.dropdown-toggle.show,
&:focus,
&:hover {
background: var(--input-background-color);
}
}
.status-bar-dropdown-button {
&:after {
content: unset;
}
}
}
.dropdown {
.dropdown-toggle {
padding: 0.1em 0.25em;
}
.dropdown-menu {
width: max-content;
}
}
.dropdown-note-info {
padding: 1em !important;
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
.dropdown-note-paths {
.note-paths-widget {
padding: 0.5em;
}
.note-path-intro {
color: var(--muted-text-color);
}
.note-path-list {
margin: 12px 0;
padding: 0;
list-style: none;
/* Note path card */
li {
--border-radius: 6px;
position: relative;
background: var(--card-background-color);
padding: 8px 20px 8px 25px;
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
& + li {
margin-top: 2px;
}
/* Current path arrow */
&.path-current::before {
position: absolute;
display: flex;
justify-content: flex-end;
align-items: center;
content: "\ee8f";
top: 0;
left: 0;
width: 20px;
bottom: 0;
font-family: "boxicons";
font-size: .75em;
color: var(--menu-item-icon-color);
}
}
/* Note path segment */
a {
margin-inline: 2px;
padding-inline: 2px;
color: currentColor;
font-weight: normal;
text-decoration: none;
/* The last segment of the note path */
&.basename {
color: var(--muted-text-color);
}
}
}
}
.dropdown-code-note-switcher {
max-height: 90vh;
overflow: scroll;
}
.backlinks-widget > .dropdown-menu {
--menu-padding-size: .9em;
max-height: 60vh;
overflow-y: scroll;
/* Backlink card */
li {
--border-radius: 8px;
max-width: 600px;
padding: 10px 20px;
background: var(--card-background-color);
& + li {
margin-top: 2px;
}
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* Card header */
& > span:first-child {
display: block;
> span {
display: flex;
flex-wrap: wrap;
align-items: center;
/* Note path */
> small {
flex: 100%;
order: -1;
font-size: .65rem;
.note-path {
padding: 0;
}
}
/* Note icon */
> .bx {
color: var(--menu-item-icon-color);
}
/* Note title */
> a {
margin-inline-start: 4px;
color: currentColor;
font-weight: 500;
}
}
}
/* Card content - excerpt */
& > span:nth-child(2) > div {
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
display: block;
margin: 8px 0;
border-radius: 4px;
background: var(--quick-search-result-content-background);
padding: 8px;
font-size: .75rem;
a {
background: transparent;
color: var(--quick-search-result-highlight-color);
text-decoration: underline;
}
p {
margin: 0;
}
}
}
}
}
> .attribute-list {
font-size: 0.9em;
padding: 0.5em 0.75em;
.inherited-attributes-widget > div {
padding: 0;
font-size: 0.9em;
}
.attribute-list-editor {
padding: 0 !important;
}
}
}

View File

@@ -1,422 +0,0 @@
import "./StatusBar.css";
import { Locale } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { ViewScope } from "../../services/link";
import server from "../../services/server";
import { openInAppHelpFromUrl } from "../../services/utils";
import { formatDateTime } from "../../utils/formatters";
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
import Dropdown, { DropdownProps } from "../react/Dropdown";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteLabel, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
import Icon from "../react/Icon";
import LinkButton from "../react/LinkButton";
import { ParentComponent } from "../react/react_utils";
import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
import InheritedAttributesTab from "../ribbon/InheritedAttributesTab";
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
import { useAttachments } from "../type_widgets/Attachment";
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
import Breadcrumb from "./Breadcrumb";
interface StatusBarContext {
note: FNote;
notePath: string | null | undefined;
noteContext: NoteContext;
viewScope?: ViewScope;
hoistedNoteId?: string;
}
export default function StatusBar() {
const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext();
const [ activePane, setActivePane ] = useState<"attributes" | "similar-notes" | false>(false);
const context: StatusBarContext | undefined | null = note && noteContext && { note, notePath, noteContext, viewScope, hoistedNoteId };
const attributesContext: AttributesProps | undefined | null = context && {
...context,
attributesShown: activePane === "attributes",
setAttributesShown: (shown) => setActivePane(shown && "attributes")
};
const noteInfoContext: NoteInfoContext | undefined | null = context && {
...context,
similarNotesShown: activePane === "similar-notes",
setSimilarNotesShown: (shown) => setActivePane(shown && "similar-notes")
};
const isHiddenNote = note?.isInHiddenSubtree();
return (
<div className="status-bar">
{attributesContext && <AttributesPane {...attributesContext} />}
{noteInfoContext && <SimilarNotesPane {...noteInfoContext} />}
<div className="status-bar-main-row">
{context && attributesContext && noteInfoContext && <>
<Breadcrumb {...context} />
<div className="actions-row">
<CodeNoteSwitcher {...context} />
<LanguageSwitcher {...context} />
{!isHiddenNote && <NotePaths {...context} />}
<AttributesButton {...attributesContext} />
<AttachmentCount {...context} />
<BacklinksBadge {...context} />
<NoteInfoBadge {...noteInfoContext} />
</div>
</>}
</div>
</div>
);
}
function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions, dropdownOptions, ...dropdownProps }: Omit<DropdownProps, "hideToggleArrow" | "title" | "titlePosition"> & {
title: string;
icon?: string;
}) {
return (
<Dropdown
buttonClassName={clsx("status-bar-dropdown-button", buttonClassName)}
titlePosition="top"
titleOptions={{
popperConfig: {
...titleOptions?.popperConfig,
strategy: "fixed"
},
animation: false,
...titleOptions
}}
dropdownOptions={{
popperConfig: {
strategy: "fixed",
placement: "top"
},
...dropdownOptions
}}
text={<>
{icon && (<><Icon icon={icon} />&nbsp;</>)}
<span className="text">{text}</span>
</>}
{...dropdownProps}
>
{children}
</Dropdown>
);
}
interface StatusBarButtonBaseProps {
className?: string;
icon: string;
title: string;
text: string | number;
disabled?: boolean;
active?: boolean;
}
type StatusBarButtonWithCommand = StatusBarButtonBaseProps & { triggerCommand: CommandNames; };
type StatusBarButtonWithClick = StatusBarButtonBaseProps & { onClick: () => void; };
function StatusBarButton({ className, icon, text, title, active, ...restProps }: StatusBarButtonWithCommand | StatusBarButtonWithClick) {
const parentComponent = useContext(ParentComponent);
const buttonRef = useRef<HTMLButtonElement>(null);
useStaticTooltip(buttonRef, {
placement: "top",
fallbackPlacements: [ "top" ],
popperConfig: { strategy: "fixed" },
animation: false,
title
});
return (
<button
ref={buttonRef}
className={clsx("btn select-button focus-outline", className, active && "active")}
type="button"
onClick={() => {
if ("triggerCommand" in restProps) {
parentComponent?.triggerCommand(restProps.triggerCommand);
} else {
restProps.onClick();
}
}}
>
<Icon icon={icon} />&nbsp;<span className="text">{text}</span>
</button>
);
}
//#region Language Switcher
function LanguageSwitcher({ note }: StatusBarContext) {
const [ modalShown, setModalShown ] = useState(false);
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
return (
<>
{note.type === "text" && <StatusBarDropdown
icon="bx bx-globe"
title={t("status_bar.language_title")}
text={<span dir={activeLocale?.rtl ? "rtl" : "ltr"}>{getLocaleName(activeLocale)}</span>}
>
{processedLocales.map((locale, index) =>
(typeof locale === "object") ? (
<FormListItem
key={locale.id}
rtl={locale.rtl}
checked={locale.id === currentNoteLanguage}
onClick={() => setCurrentNoteLanguage(locale.id)}
>{locale.name}</FormListItem>
) : (
<FormDropdownDivider key={`divider-${index}`} />
)
)}
<FormDropdownDivider />
<FormListItem
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
icon="bx bx-help-circle"
>{t("note_language.help-on-languages")}</FormListItem>
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
</StatusBarDropdown>}
{createPortal(
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
export function getLocaleName(locale: Locale | null | undefined) {
if (!locale) return "";
if (!locale.id) return "-";
if (locale.name.length <= 4 || locale.rtl) return locale.name; // Some locales like Japanese and Chinese look better than their ID.
return locale.id
.replace("_", "-")
.toLocaleUpperCase();
}
//#endregion
//#region Note info & Similar
interface NoteInfoContext extends StatusBarContext {
similarNotesShown: boolean;
setSimilarNotesShown: (value: boolean) => void;
}
export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) {
const dropdownRef = useRef<BootstrapDropdown>(null);
const { metadata, ...sizeProps } = useNoteMetadata(note);
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
return (note &&
<StatusBarDropdown
icon="bx bx-info-circle"
title={t("status_bar.note_info_title")}
dropdownRef={dropdownRef}
dropdownContainerClassName="dropdown-note-info"
dropdownOptions={{ autoClose: "outside" }}
>
<ul>
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
</ul>
<LinkButton
text={t("note_info_widget.show_similar_notes")}
onClick={() => {
dropdownRef.current?.hide();
setSimilarNotesShown(true);
}}
/>
</StatusBarDropdown>
);
}
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
return (
<li>
<strong title={title}>{text}{": "}</strong>
<span>{value}</span>
</li>
);
}
function SimilarNotesPane({ note, similarNotesShown }: NoteInfoContext) {
return (similarNotesShown &&
<div className="similar-notes-pane">
<SimilarNotesTab note={note} />
</div>
);
}
//#endregion
//#region Backlinks
function BacklinksBadge({ note, viewScope }: StatusBarContext) {
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
return (note && count > 0 &&
<StatusBarDropdown
className="backlinks-badge backlinks-widget"
icon="bx bx-link"
text={t("status_bar.backlinks", { count })}
title={t("status_bar.backlinks_title", { count })}
dropdownContainerClassName="backlinks-items"
>
<BacklinksList note={note} />
</StatusBarDropdown>
);
}
//#endregion
//#region Attachment count
function AttachmentCount({ note }: StatusBarContext) {
const attachments = useAttachments(note);
const count = attachments.length;
return (note && count > 0 &&
<StatusBarButton
className="attachment-count-button"
icon="bx bx-paperclip"
text={t("status_bar.attachments", { count })}
title={t("status_bar.attachments_title", { count })}
triggerCommand="showAttachments"
/>
);
}
//#endregion
//#region Attributes
interface AttributesProps extends StatusBarContext {
attributesShown: boolean;
setAttributesShown: (shown: boolean) => void;
}
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
// React to note changes.
useEffect(() => {
setCount(note.attributes.length);
}, [ note ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
}
}));
return (
<StatusBarButton
className="attributes-button"
icon="bx bx-list-check"
title={t("status_bar.attributes_title")}
text={t("status_bar.attributes", { count })}
active={attributesShown}
onClick={() => setAttributesShown(!attributesShown)}
/>
);
}
function AttributesPane({ note, noteContext, attributesShown, setAttributesShown }: AttributesProps) {
const parentComponent = useContext(ParentComponent);
const api = useRef<AttributeEditorImperativeHandlers>(null);
const context = parentComponent && {
componentId: parentComponent.componentId,
note,
hidden: !note
};
// Show on keyboard shortcuts.
useTriliumEvents([ "addNewLabel", "addNewRelation" ], () => setAttributesShown(true));
// Interaction with the attribute editor.
useLegacyImperativeHandlers(useMemo(() => ({
saveAttributesCommand: () => api.current?.save(),
reloadAttributesCommand: () => api.current?.refresh(),
updateAttributeListCommand: ({ attributes }) => api.current?.renderOwnedAttributes(attributes)
}), [ api ]));
return (context &&
<div className={clsx("attribute-list", !attributesShown && "hidden-ext")}>
<InheritedAttributesTab {...context} />
<AttributeEditor
{...context}
api={api}
ntxId={noteContext.ntxId}
/>
</div>
);
}
//#endregion
//#region Note paths
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
const count = sortedNotePaths?.length ?? 0;
return (count > 1 &&
<StatusBarDropdown
title={t("status_bar.note_paths_title")}
dropdownContainerClassName="dropdown-note-paths"
icon="bx bx-directions"
text={t("status_bar.note_paths", { count })}
>
<NotePathsWidget
sortedNotePaths={sortedNotePaths}
currentNotePath={notePath}
/>
</StatusBarDropdown>
);
}
//#endregion
//#region Code note switcher
function CodeNoteSwitcher({ note }: StatusBarContext) {
const [ modalShown, setModalShown ] = useState(false);
const currentNoteMime = useNoteProperty(note, "mime");
const mimeTypes = useMimeTypes();
const correspondingMimeType = useMemo(() => (
mimeTypes.find(m => m.mime === currentNoteMime)
), [ mimeTypes, currentNoteMime ]);
return (note.type === "code" &&
<>
<StatusBarDropdown
icon="bx bx-code-curly"
text={correspondingMimeType?.title}
title={t("status_bar.code_note_switcher")}
dropdownContainerClassName="dropdown-code-note-switcher"
>
<NoteTypeCodeNoteList
currentMimeType={currentNoteMime}
mimeTypes={mimeTypes}
changeNoteType={(type, mime) => server.put(`notes/${note.noteId}/type`, { type, mime })}
setModalShown={() => setModalShown(true)}
/>
</StatusBarDropdown>
{createPortal(
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
//#endregion

View File

@@ -1,20 +0,0 @@
.collection-properties {
padding: 0;
display: flex;
gap: 0.25em;
align-items: center;
width: 100%;
max-width: unset;
font-size: 0.8em;
.dropdown-menu {
input.form-control {
padding: 2px 8px;
margin-left: 1em;
}
}
.spacer {
flex-grow: 1;
}
}

View File

@@ -1,222 +0,0 @@
import "./CollectionProperties.css";
import { t } from "i18next";
import { useContext } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import FNote from "../../entities/fnote";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { openInAppHelpFromUrl } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: "bx bxs-grid",
list: "bx bx-list-ul",
calendar: "bx bx-calendar",
table: "bx bx-table",
geoMap: "bx bx-map-alt",
board: "bx bx-columns",
presentation: "bx bx-rectangle"
};
export default function CollectionProperties({ note }: { note: FNote }) {
const [ viewType, setViewType ] = useViewType(note);
return (
<div className="collection-properties">
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
<ViewOptions note={note} viewType={viewType} />
<div className="spacer" />
<HelpButton note={note} />
</div>
);
}
function ViewTypeSwitcher({ viewType, setViewType }: { viewType: ViewTypeOptions, setViewType: (newValue: ViewTypeOptions) => void }) {
return (
<Dropdown
text={<>
<Icon icon={ICON_MAPPINGS[viewType]} />&nbsp;
{VIEW_TYPE_MAPPINGS[viewType]}
</>}
>
{Object.entries(VIEW_TYPE_MAPPINGS).map(([ key, label ]) => (
<FormListItem
key={key}
onClick={() => setViewType(key as ViewTypeOptions)}
selected={viewType === key}
disabled={viewType === key}
icon={ICON_MAPPINGS[key as ViewTypeOptions]}
>{label}</FormListItem>
))}
</Dropdown>
);
}
function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOptions }) {
const properties = bookPropertiesConfig[viewType].properties;
return (
<Dropdown
buttonClassName="bx bx-cog icon-action"
hideToggleArrow
>
{properties.map(property => (
<ViewProperty key={property.label} note={note} property={property} />
))}
{properties.length > 0 && <FormDropdownDivider />}
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-archive",
label: t("book_properties.include_archived_notes"),
bindToLabel: "includeArchived"
} as CheckBoxProperty} />
</Dropdown>
);
}
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}
function HelpButton({ note }: { note: FNote }) {
const helpUrl = getHelpUrlForNote(note);
return (helpUrl && (
<ActionButton
icon="bx bx-help-circle"
onClick={(() => openInAppHelpFromUrl(helpUrl))}
text={t("help-button.title")}
/>
));
}

View File

@@ -28,45 +28,3 @@ body.mobile .note-title-widget input.note-title {
body.desktop .note-title-widget input.note-title {
font-size: 180%;
}
body.experimental-feature-new-layout {
.title-row {
container-type: size;
border-bottom: 1px solid var(--main-border-color);
transition: border 400ms ease-out;
&.hide-title {
border-bottom-color: transparent;
transition: none;
}
@container (max-width: 700px) {
.note-icon-widget .note-icon {
font-size: 1.3em;
}
.note-title-widget {
display: flex;
align-items: center;
.note-title {
font-size: 1em;
}
}
.note-title-widget:focus-within + .note-badges,
.ext-badge .text {
display: none;
}
}
}
.note-title-widget {
input.note-title {
--input-focus-background: transparent;
--input-focus-outline-color: transparent;
--input-hover-background: transparent;
--input-hover-color: initial;
--input-focus-color: initial;
}
}
}

View File

@@ -62,7 +62,6 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getNoteTypeClass(note.type));
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("protected", note.isProtected);

View File

@@ -1,59 +0,0 @@
.ext-badge {
display: flex;
align-items: center;
padding: 2px 6px;
border-radius: var(--badge-radius);
font-size: 0.75em;
background-color: var(--color, transparent);
color: white;
min-width: 0;
flex-shrink: 1;
&.clickable {
cursor: pointer;
&:hover {
background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black);
}
}
a {
color: inherit !important;
text-decoration: none;
}
> * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.dropdown-badge {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: var(--badge-radius);
.ext-badge {
border-radius: 0;
.text {
display: inline-flex;
align-items: center;
.arrow {
font-size: 1.3em;
margin-left: 0.25em;
}
}
}
.btn {
border: 0;
margin: 0;
padding: 0;
}
}

View File

@@ -1,81 +1,8 @@
import "./Badge.css";
import clsx from "clsx";
import { ComponentChildren, HTMLAttributes } from "preact";
import { useRef } from "preact/hooks";
import Dropdown, { DropdownProps } from "./Dropdown";
import { useStaticTooltip } from "./hooks";
import Icon from "./Icon";
interface SimpleBadgeProps {
interface BadgeProps {
className?: string;
title: ComponentChildren;
title: string;
}
interface BadgeProps extends Pick<HTMLAttributes<HTMLDivElement>, "onClick" | "style"> {
text?: ComponentChildren;
icon?: string;
className?: string;
tooltip?: string;
href?: string;
}
export default function SimpleBadge({ title, className }: SimpleBadgeProps) {
return <span class={`badge ${className ?? ""}`}>{title}</span>;
}
export function Badge({ icon, className, text, tooltip, href, ...containerProps }: BadgeProps) {
const containerRef = useRef<HTMLDivElement>(null);
useStaticTooltip(containerRef, {
placement: "bottom",
fallbackPlacements: [ "bottom" ],
animation: false,
html: true,
title: tooltip
});
const content = <>
{icon && <><Icon icon={icon} />&nbsp;</>}
<span class="text">{text}</span>
</>;
return (
<div
ref={containerRef}
className={clsx("ext-badge", className, { "clickable": !!containerProps.onClick })}
{...containerProps}
>
{href ? <a href={href}>{content}</a> : <span>{content}</span>}
</div>
);
}
export function BadgeWithDropdown({ text, children, tooltip, className, dropdownOptions, ...props }: BadgeProps & {
children: ComponentChildren,
dropdownOptions?: Partial<DropdownProps>
}) {
return (
<Dropdown
className={`dropdown-badge dropdown-${className}`}
text={<Badge
text={<>{text} <Icon className="arrow" icon="bx bx-chevron-down" /></>}
className={className}
{...props}
/>}
noDropdownListStyle
noSelectButtonStyle
hideToggleArrow
title={tooltip}
titlePosition="bottom"
{...dropdownOptions}
dropdownOptions={{
...dropdownOptions?.dropdownOptions,
popperConfig: {
...dropdownOptions?.dropdownOptions?.popperConfig,
placement: "bottom", strategy: "fixed"
}
}}
>{children}</Dropdown>
);
}
export default function Badge({ title, className }: BadgeProps) {
return <span class={`badge ${className ?? ""}`}>{title}</span>
}

View File

@@ -1,37 +0,0 @@
.collapsible {
.collapsible-title {
line-height: 1em;
display: flex;
align-items: center;
appearance: none;
background: transparent;
border: 0;
color: inherit;
.arrow {
font-size: 1.3em;
transition: transform 250ms ease-in;
}
}
.collapsible-body {
height: 0;
overflow: hidden;
}
.collapsible-inner-body {
padding-top: 0.5em;
}
&.expanded {
.collapsible-title .arrow {
transform: rotate(90deg);
}
}
&.with-transition {
.collapsible-body {
transition: height 250ms ease-in;
}
}
}

View File

@@ -1,69 +0,0 @@
import "./Collapsible.css";
import clsx from "clsx";
import { ComponentChildren, HTMLAttributes } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { useElementSize, useUniqueName } from "./hooks";
import Icon from "./Icon";
interface CollapsibleProps extends Pick<HTMLAttributes<HTMLDivElement>, "className"> {
title: string;
children: ComponentChildren;
initiallyExpanded?: boolean;
}
export default function Collapsible({ initiallyExpanded, ...restProps }: CollapsibleProps) {
const [ expanded, setExpanded ] = useState(initiallyExpanded);
return <ExternallyControlledCollapsible {...restProps} expanded={expanded} setExpanded={setExpanded} />;
}
export function ExternallyControlledCollapsible({ title, children, className, expanded, setExpanded }: Omit<CollapsibleProps, "initiallyExpanded"> & {
expanded: boolean | undefined;
setExpanded: (expanded: boolean) => void
}) {
const bodyRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const { height } = useElementSize(innerRef) ?? {};
const contentId = useUniqueName();
const [ transitionEnabled, setTransitionEnabled ] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
setTransitionEnabled(true);
}, 200);
return () => clearTimeout(timeout);
}, []);
return (
<div className={clsx("collapsible", className, {
expanded,
"with-transition": transitionEnabled
})}>
<button
className="collapsible-title"
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
aria-controls={contentId}
>
<Icon className="arrow" icon="bx bx-chevron-right" />&nbsp;
{title}
</button>
<div
id={contentId}
ref={bodyRef}
className="collapsible-body"
style={{ height: expanded ? height : "0" }}
aria-hidden={!expanded}
>
<div
ref={innerRef}
className="collapsible-inner-body"
>
{children}
</div>
</div>
</div>
);
}

View File

@@ -117,8 +117,8 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
aria-expanded="false"
id={id ?? ariaId}
disabled={disabled}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
onMouseOver={() => showTooltip()}
onMouseLeave={() => hideTooltip()}
{...buttonProps}
>
{text}

View File

@@ -1,8 +1,6 @@
import { Ref } from "preact";
import { useEffect, useRef } from "preact/hooks";
import ActionButton, { ActionButtonProps } from "./ActionButton";
import Button, { ButtonProps } from "./Button";
import { useEffect, useRef } from "preact/hooks";
interface FormFileUploadProps {
name?: string;
@@ -28,7 +26,7 @@ export default function FormFileUpload({ inputRef, name, onChange, multiple, hid
multiple={multiple}
onChange={e => onChange((e.target as HTMLInputElement).files)} />
</label>
);
)
}
/**
@@ -51,27 +49,5 @@ export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonPr
onChange={onChange}
/>
</>
);
}
/**
* Similar to {@link FormFileUploadButton}, but uses an {@link ActionButton} instead of a normal {@link Button}.
* @param param the change listener for the file upload and the properties for the button.
*/
export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit<ActionButtonProps, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<ActionButton
{...buttonProps}
onClick={() => inputRef.current?.click()}
/>
<FormFileUpload
inputRef={inputRef}
hidden
onChange={onChange}
/>
</>
);
)
}

View File

@@ -1,29 +1,9 @@
.dropdown-item {
.description {
font-size: small;
color: var(--muted-text-color);
white-space: normal;
}
span.bx {
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;
}
}
.dropdown-item .description {
font-size: small;
color: var(--muted-text-color);
white-space: normal;
}
.dropdown-item span.bx {
flex-shrink: 0;
}

View File

@@ -1,15 +1,12 @@
import "./FormList.css";
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
import clsx from "clsx";
import { ComponentChildren } from "preact";
import { type CSSProperties,useEffect, useMemo, useRef, useState } from "preact/compat";
import { CommandNames } from "../../components/app_context";
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
import FormToggle from "./FormToggle";
import { useStaticTooltip } from "./hooks";
import Icon from "./Icon";
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact/compat";
import "./FormList.css";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks";
import { handleRightToLeftPlacement, isMobile } from "../../services/utils";
import clsx from "clsx";
interface FormListOpts {
children: ComponentChildren;
@@ -35,7 +32,7 @@ export default function FormList({ children, onSelect, style, fullHeight, wrappe
return () => {
$wrapperRef.off("hide.bs.dropdown");
dropdown.dispose();
};
}
}, [ triggerRef, wrapperRef ]);
const builtinStyles = useMemo(() => {
@@ -53,7 +50,8 @@ export default function FormList({ children, onSelect, style, fullHeight, wrappe
<button
ref={triggerRef}
type="button" style="display: none;"
data-bs-toggle="dropdown" data-bs-display="static" />
data-bs-toggle="dropdown" data-bs-display="static">
</button>
<div class="dropdown-menu static show" style={{
...style ?? {},
@@ -96,13 +94,12 @@ interface FormListItemOpts {
description?: string;
className?: string;
rtl?: boolean;
postContent?: ComponentChildren;
}
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
placement: handleRightToLeftPlacement("right"),
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
};
}
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
const itemRef = useRef<HTMLLIElement>(null);
@@ -135,49 +132,6 @@ 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">) {
return <>
{children}
@@ -185,7 +139,7 @@ function FormListContent({ children, badges, description, disabled, disabledTool
<span className={`badge ${className ?? ""}`}>{text}</span>
))}
{disabled && disabledTooltip && (
<span class="bx bx-info-circle contextual-help" title={disabledTooltip} />
<span class="bx bx-info-circle disabled-tooltip" title={disabledTooltip} />
)}
{description && <div className="description">{description}</div>}
</>;
@@ -200,24 +154,18 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
<li>
<h6 className="dropdown-header">{text}</h6>
</li>
);
)
}
export function FormDropdownDivider() {
return <div className="dropdown-divider" />;
}
export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdownToggleClicked }: {
icon: string,
title: ComponentChildren,
children: ComponentChildren,
onDropdownToggleClicked?: () => void,
dropStart?: boolean
}) {
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
const [ openOnMobile, setOpenOnMobile ] = useState(false);
return (
<li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}>
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
<span
className="dropdown-toggle"
onClick={(e) => {
@@ -225,8 +173,6 @@ export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdo
if (isMobile()) {
setOpenOnMobile(!openOnMobile);
} else if (onDropdownToggleClicked) {
onDropdownToggleClicked();
}
}}
>
@@ -238,5 +184,5 @@ export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdo
{children}
</ul>
</li>
);
)
}

View File

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

View File

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

View File

@@ -17,7 +17,6 @@
.info-bar-subtle {
color: var(--muted-text-color);
background: var(--main-background-color);
border-bottom: 1px solid var(--main-border-color);
margin-block: 0;
padding-inline: 22px;
}
}

View File

@@ -1,26 +1,16 @@
import { ComponentChild } from "preact";
import { CommandNames } from "../../components/app_context";
interface LinkButtonProps {
onClick?: () => void;
onClick: () => void;
text: ComponentChild;
triggerCommand?: CommandNames;
}
export default function LinkButton({ onClick, text, triggerCommand }: LinkButtonProps) {
export default function LinkButton({ onClick, text }: LinkButtonProps) {
return (
<a class="tn-link" href="#"
data-trigger-command={triggerCommand}
role="button"
onKeyDown={(e)=> {
if (e.code === "Space") {
onClick?.();
}
}}
onClick={(e) => {
e.preventDefault();
onClick?.();
}}>
<a class="tn-link" href="javascript:" onClick={(e) => {
e.preventDefault();
onClick();
}}>
{text}
</a>
)

View File

@@ -1,30 +1,29 @@
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
import { Tooltip } from "bootstrap";
import Mark from "mark.js";
import { RefObject, VNode } from "preact";
import { CSSProperties } from "preact/compat";
import { DragData } from "../note_tree";
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils";
import { RefObject, VNode } from "preact";
import { Tooltip } from "bootstrap";
import { ViewMode, ViewScope } from "../../services/link";
import appContext, { EventData, EventNames } from "../../components/app_context";
import attributes from "../../services/attributes";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import FBlob from "../../entities/fblob";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import froca from "../../services/froca";
import keyboard_actions from "../../services/keyboard_actions";
import { ViewScope } from "../../services/link";
import Mark from "mark.js";
import NoteContext from "../../components/note_context";
import NoteContextAwareWidget from "../note_context_aware_widget";
import options, { type OptionValue } from "../../services/options";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts";
import SpacedUpdate from "../../services/spaced_update";
import toast, { ToastOptions } from "../../services/toast";
import utils, { escapeRegExp, randomString, reloadFrontendApp } from "../../services/utils";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { DragData } from "../note_tree";
import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils";
import server from "../../services/server";
import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts";
import froca from "../../services/froca";
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentComponent = useContext(ParentComponent);
@@ -43,7 +42,7 @@ export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler:
for (const eventName of eventNames) {
handlers.push({ eventName, callback: (data) => {
handler(data, eventName);
}});
}})
}
for (const { eventName, callback } of handlers) {
@@ -112,8 +111,8 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha
await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId);
dataSaved?.(data);
};
}, [ note, getData, dataSaved ]);
}
}, [ note, getData, dataSaved ])
const spacedUpdate = useSpacedUpdate(callback);
// React to note/blob changes.
@@ -138,7 +137,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
await spacedUpdate.updateNowIfNecessary();
});
})
// Save if needed upon window/browser closing.
useEffect(() => {
@@ -171,7 +170,7 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st
if (needsRefresh) {
reloadFrontendApp(`option change: ${name}`);
}
};
}
}, [ name, needsRefresh ]);
useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => {
@@ -179,14 +178,14 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st
const newValue = options.get(name);
setValue(newValue);
}
}, [ name, setValue ]));
}, [ name, setValue ]));
useDebugValue(name);
return [
value,
wrappedSetValue
];
]
}
/**
@@ -202,7 +201,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
return [
(value === "true"),
(newValue) => setValue(newValue ? "true" : "false")
];
]
}
/**
@@ -218,18 +217,17 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
return [
(parseInt(value, 10)),
(newValue) => setValue(newValue)
];
]
}
/**
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
*
* @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
export function useTriliumOptionJson<T>(name: OptionNames, needsRefresh?: boolean): [ T, (newValue: T) => Promise<void> ] {
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
const [ value, setValue ] = useTriliumOption(name);
useDebugValue(name);
return [
(JSON.parse(value) as T),
@@ -267,7 +265,7 @@ export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
* @returns a name with the given prefix and a random alpanumeric string appended to it.
*/
export function useUniqueName(prefix?: string) {
return useMemo(() => (prefix ? `${prefix}-` : "") + utils.randomString(10), [ prefix ]);
return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]);
}
export function useNoteContext() {
@@ -275,7 +273,6 @@ export function useNoteContext() {
const [ noteContext, setNoteContext ] = useState<NoteContext | undefined>(noteContextContext ?? undefined);
const [ notePath, setNotePath ] = useState<string | null | undefined>();
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ hoistedNoteId, setHoistedNoteId ] = useState(noteContext?.hoistedNoteId);
const [ , setViewScope ] = useState<ViewScope>();
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
const [ refreshCounter, setRefreshCounter ] = useState(0);
@@ -283,7 +280,6 @@ export function useNoteContext() {
useEffect(() => {
if (!noteContextContext) return;
setNoteContext(noteContextContext);
setHoistedNoteId(noteContextContext.hoistedNoteId);
setNote(noteContextContext.note);
setNotePath(noteContextContext.notePath);
setViewScope(noteContextContext.viewScope);
@@ -297,7 +293,6 @@ export function useNoteContext() {
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({ noteContext }) => {
if (noteContextContext) return;
setNoteContext(noteContext);
setHoistedNoteId(noteContext.hoistedNoteId);
setNotePath(noteContext.notePath);
setViewScope(noteContext.viewScope);
});
@@ -315,20 +310,15 @@ export function useNoteContext() {
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
}
});
useTriliumEvent("hoistedNoteChanged", ({ noteId, ntxId }) => {
if (ntxId === noteContext?.ntxId) {
setHoistedNoteId(noteId);
}
});
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
return {
note,
note: note,
noteId: noteContext?.note?.noteId,
notePath: noteContext?.notePath,
hoistedNoteId,
hoistedNoteId: noteContext?.hoistedNoteId,
ntxId: noteContext?.ntxId,
viewScope: noteContext?.viewScope,
componentId: parentComponent.componentId,
@@ -336,72 +326,7 @@ export function useNoteContext() {
parentComponent,
isReadOnlyTemporarilyDisabled
};
}
/**
* Similar to {@link useNoteContext}, but instead of using the note context from the split container that the component is part of, it uses the active note context instead
* (the note currently focused by the user).
*/
export function useActiveNoteContext() {
const [ noteContext, setNoteContext ] = useState<NoteContext | undefined>(appContext.tabManager.getActiveContext() ?? undefined);
const [ notePath, setNotePath ] = useState<string | null | undefined>();
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ , setViewScope ] = useState<ViewScope>();
const [ hoistedNoteId, setHoistedNoteId ] = useState(noteContext?.hoistedNoteId);
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
const [ refreshCounter, setRefreshCounter ] = useState(0);
useEffect(() => {
if (!noteContext) {
setNoteContext(appContext.tabManager.getActiveContext() ?? undefined);
}
}, [ noteContext ]);
useEffect(() => {
setNote(noteContext?.note);
}, [ notePath ]);
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], () => {
const noteContext = appContext.tabManager.getActiveContext() ?? undefined;
setNoteContext(noteContext);
setHoistedNoteId(noteContext?.hoistedNoteId);
setNotePath(noteContext?.notePath);
setViewScope(noteContext?.viewScope);
});
useTriliumEvent("frocaReloaded", () => {
setNote(noteContext?.note);
});
useTriliumEvent("noteTypeMimeChanged", ({ noteId }) => {
if (noteId === note?.noteId) {
setRefreshCounter(refreshCounter + 1);
}
});
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId === noteContext?.ntxId) {
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
}
});
useTriliumEvent("hoistedNoteChanged", ({ noteId, ntxId }) => {
if (ntxId === noteContext?.ntxId) {
setHoistedNoteId(noteId);
}
});
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
return {
note,
noteId: noteContext?.note?.noteId,
notePath: noteContext?.notePath,
hoistedNoteId,
ntxId: noteContext?.ntxId,
viewScope: noteContext?.viewScope,
componentId: parentComponent.componentId,
noteContext,
parentComponent,
isReadOnlyTemporarilyDisabled
};
}
/**
@@ -444,7 +369,7 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re
const setter = useCallback((value: string | undefined) => {
if (note) {
attributes.setAttribute(note, "relation", relationName, value);
attributes.setAttribute(note, "relation", relationName, value)
}
}, [note]);
@@ -494,7 +419,7 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: FilterLa
const setter = useCallback((value: string | null | undefined) => {
if (note) {
if (value !== null) {
attributes.setLabel(note.noteId, labelName, value);
attributes.setLabel(note.noteId, labelName, value)
} else {
attributes.removeOwnedLabelByName(note, labelName);
}
@@ -553,7 +478,7 @@ export function useNoteLabelInt(note: FNote | undefined | null, labelName: Filte
return [
(value ? parseInt(value, 10) : undefined),
(newValue) => setValue(String(newValue))
];
]
}
export function useNoteBlob(note: FNote | null | undefined, componentId?: string): FBlob | null | undefined {
@@ -570,7 +495,7 @@ export function useNoteBlob(note: FNote | null | undefined, componentId?: string
}
}
useEffect(() => { refresh(); }, [ note?.noteId ]);
useEffect(() => { refresh() }, [ note?.noteId ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (!note) return;
@@ -632,7 +557,7 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
useDebugValue(widget);
return [ <div className={containerClassName} style={containerStyle} ref={ref} />, widget ];
return [ <div className={containerClassName} style={containerStyle} ref={ref} />, widget ]
}
/**
@@ -659,7 +584,7 @@ export function useElementSize(ref: RefObject<HTMLElement>) {
return () => {
resizeObserver.unobserve(element);
resizeObserver.disconnect();
};
}
}, [ ref ]);
return size;
@@ -719,7 +644,7 @@ export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Toolti
return { showTooltip, hideTooltip };
}
const tooltips = new Set<Tooltip>();
let tooltips = new Set<Tooltip>();
/**
* Similar to {@link useTooltip}, but doesn't expose methods to imperatively hide or show the tooltip.
@@ -748,7 +673,7 @@ export function useStaticTooltip(elRef: RefObject<Element>, config?: Partial<Too
// workaround for https://github.com/twbs/bootstrap/issues/37474
(tooltip as any)._activeTrigger = {};
(tooltip as any)._element = document.createElement('noscript'); // placeholder with no behavior
};
}
}, [ elRef, config ]);
}
@@ -791,7 +716,7 @@ export function useImperativeSearchHighlighlighting(highlightedTokens: string[]
const highlightRegex = useMemo(() => {
if (!highlightedTokens?.length) return null;
const regex = highlightedTokens.map((token) => escapeRegExp(token)).join("|");
return new RegExp(regex, "gi");
return new RegExp(regex, "gi")
}, [ highlightedTokens ]);
return (el: HTMLElement | null | undefined) => {
@@ -858,7 +783,7 @@ export function useNoteTreeDrag(containerRef: MutableRef<HTMLElement | null | un
container.addEventListener("dragenter", onDragEnter);
container.addEventListener("dragover", onDragOver);
container.addEventListener("drop", onDrop);
container.addEventListener("dragleave", onDragLeave);
container.addEventListener("dragleave", onDragLeave)
return () => {
container.removeEventListener("dragenter", onDragEnter);
@@ -894,7 +819,7 @@ export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", conta
for (const binding of bindings) {
removeIndividualBinding(binding);
}
};
}
}, [ scope, containerRef, parentComponent, ntxId ]);
}
@@ -919,12 +844,10 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h
*/
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly");
const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
const enableEditing = useCallback((enabled = true) => {
const enableEditing = useCallback(() => {
if (noteContext?.viewScope) {
noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
}
}, [noteContext]);
@@ -935,11 +858,11 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
setIsReadOnly(readOnly);
});
}
}, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]);
}, [ note, noteContext, noteContext?.viewScope ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {
setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
setIsReadOnly(false);
}
});
@@ -1002,50 +925,3 @@ export function useLauncherVisibility(launchNoteId: string) {
return isVisible;
}
export function useNote(noteId: string | null | undefined, silentNotFoundError = false) {
const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : undefined);
const requestIdRef = useRef(0);
useEffect(() => {
if (!noteId) {
setNote(undefined);
return;
}
if (note?.noteId === noteId) {
return;
}
// Try to read from cache.
const cachedNote = froca.getNoteFromCache(noteId);
if (cachedNote) {
setNote(cachedNote);
return;
}
// Read it asynchronously.
const requestId = ++requestIdRef.current;
froca.getNote(noteId, silentNotFoundError).then(readNote => {
// Only update if this is the latest request.
if (readNote && requestId === requestIdRef.current) {
setNote(readNote);
}
});
}, [ note, noteId, silentNotFoundError ]);
if (note?.noteId === noteId) {
return note;
}
return undefined;
}
export function useNoteIcon(note: FNote | null | undefined) {
const [ icon, setIcon ] = useState(note?.getIcon());
const iconClass = useNoteLabel(note, "iconClass");
useEffect(() => {
setIcon(note?.getIcon());
}, [ note, iconClass ]);
return icon;
}

View File

@@ -1,29 +1,26 @@
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { createPortal } from "preact/compat";
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
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 toast from "../../services/toast";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import Dropdown from "../react/Dropdown";
import FormDropdownList from "../react/FormDropdownList";
import { NOTE_TYPES } from "../../services/note_types";
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import HelpButton from "../react/HelpButton";
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 protected_session from "../../services/protected_session";
import FormDropdownList from "../react/FormDropdownList";
import toast from "../../services/toast";
import branches from "../../services/branches";
import sync from "../../services/sync";
import HelpButton from "../react/HelpButton";
import { TabContext } from "./ribbon-interface";
import Modal from "../react/Modal";
import { CodeMimeTypesList } from "../type_widgets/options/code_notes";
import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector";
import { ContentLanguagesList } from "../type_widgets/options/i18n";
import { TabContext } from "./ribbon-interface";
import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector";
export default function BasicPropertiesTab({ note }: TabContext) {
return (
@@ -40,40 +37,18 @@ export default function BasicPropertiesTab({ note }: TabContext) {
}
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 currentNoteType = useNoteProperty(note, "type") ?? undefined;
const currentNoteMime = useNoteProperty(note, "mime");
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, noCodeNotes }: {
currentNoteType?: NoteType;
currentNoteMime?: string | null;
note?: FNote | null;
setModalShown: Dispatch<StateUpdater<boolean>>;
noCodeNotes?: boolean;
}) {
const mimeTypes = useMimeTypes();
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
return;
@@ -93,94 +68,71 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
}, [ note, currentNoteType, currentNoteMime ]);
return (
<>
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
const badges: FormListBadge[] = [];
if (isNew) {
badges.push({
className: "new-note-type-badge",
text: t("note_types.new-feature")
});
}
if (isBeta) {
badges.push({
text: t("note_types.beta-feature")
});
}
<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")}
>
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
const badges: FormListBadge[] = [];
if (isNew) {
badges.push({
className: "new-note-type-badge",
text: t("note_types.new-feature")
});
}
if (isBeta) {
badges.push({
text: t("note_types.beta-feature")
});
}
const checked = (type === currentNoteType);
if (noCodeNotes || type !== "code") {
return (
<FormListItem
checked={checked}
badges={badges}
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
const checked = (type === currentNoteType);
if (type !== "code") {
return (
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
);
}
})}
badges={badges}
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
)
}
})}
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
</>
);
}
{mimeTypes.map(({ title, mime }) => (
<FormListItem onClick={() => changeNoteType("code", mime)}>
{title}
</FormListItem>
))}
export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteType, setModalShown }: {
currentMimeType?: string;
mimeTypes: MimeType[];
changeNoteType(type: NoteType, mime: string): void;
setModalShown(shown: boolean): void;
}) {
return (
<>
{mimeTypes.map(({ title, mime }) => (
<FormListItem
key={mime}
checked={mime === currentMimeType}
onClick={() => changeNoteType("code", mime)}
>
{title}
</FormListItem>
))}
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</Dropdown>
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</>
);
}
export function useMimeTypes() {
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
const mimeTypes = useMemo(() => {
mime_types.loadMimeTypes();
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled);
}, [ codeNotesMimeTypes ]); // eslint-disable-line react-hooks/exhaustive-deps
return mimeTypes;
}
export function NoteTypeOptionsModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
return (
<Modal
className="code-mime-types-modal"
title={t("code_mime_types.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="xl" scrollable
>
<CodeMimeTypesList />
</Modal>
);
<Modal
className="code-mime-types-modal"
title={t("code_mime_types.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="xl" scrollable
>
<CodeMimeTypesList />
</Modal>
</div>
)
}
function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
@@ -235,11 +187,22 @@ function EditabilitySelect({ note }: { note?: FNote | null }) {
}}
/>
</div>
);
)
}
function BookmarkSwitch({ note }: { note?: FNote | null }) {
const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
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 (
<div className="bookmark-switch-container">
@@ -247,36 +210,18 @@ function BookmarkSwitch({ note }: { note?: FNote | null }) {
switchOnName={t("bookmark_switch.bookmark")} switchOnTooltip={t("bookmark_switch.bookmark_this_note")}
switchOffName={t("bookmark_switch.bookmark")} switchOffTooltip={t("bookmark_switch.remove_bookmark")}
currentValue={isBookmarked}
onChange={setIsBookmarked}
onChange={async (shouldBookmark) => {
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 ?? "")}
/>
</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 }) {
@@ -292,33 +237,16 @@ function TemplateSwitch({ note }: { note?: FNote | null }) {
currentValue={isTemplate} onChange={setIsTemplate}
/>
</div>
);
)
}
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 refreshState = useCallback(() => {
setIsShared(!!note?.hasAncestor("_share"));
}, [ note ]);
useEffect(() => refreshState(), [ refreshState ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
@@ -343,71 +271,63 @@ export function useShareState(note: FNote | null | undefined) {
sync.syncNow(true);
}, [ note ]);
return [ isShared, switchShareState ] as const;
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>
)
}
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 }: { note: FNote | null | undefined }) {
const [ modalShown, setModalShown ] = useState(false);
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
return (
<>
<LocaleSelector
locales={locales}
defaultLocale={DEFAULT_LOCALE}
currentValue={currentNoteLanguage} onChange={setCurrentNoteLanguage}
extraChildren={<>
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
</>}
/>
{createPortal(
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
document.body
)}
</>
);
}
export function useLanguageSwitcher(note: FNote | null | undefined) {
const [ languages ] = useTriliumOption("languages");
const DEFAULT_LOCALE = {
id: "",
name: t("note_language.not_set")
};
const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language");
const [ modalShown, setModalShown ] = useState(false);
const locales = useMemo(() => {
const enabledLanguages = JSON.parse(languages ?? "[]") as string[];
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
return filteredLanguages;
}, [ languages ]);
return { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage };
}
export function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
return (
<Modal
className="content-languages-modal"
title={t("content_language.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="lg" scrollable
>
<ContentLanguagesList />
</Modal>
<div className="note-language-container">
<span>{t("basic_properties.language")}:</span>
&nbsp;
<LocaleSelector
locales={locales}
defaultLocale={DEFAULT_LOCALE}
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
extraChildren={(
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
)}
>
</LocaleSelector>
<HelpButton helpPage="B0lcI9xz1r8K" style={{ marginInlineStart: "4px" }} />
<Modal
className="content-languages-modal"
title={t("content_language.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="lg" scrollable
>
<ContentLanguagesList />
</Modal>
</div>
);
}

View File

@@ -12,41 +12,34 @@ import FormCheckbox from "../react/FormCheckbox";
import FormTextBox from "../react/FormTextBox";
import { ComponentChildren } from "preact";
import { ViewTypeOptions } from "../collections/interface";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
export const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: t("book_properties.grid"),
list: t("book_properties.list"),
calendar: t("book_properties.calendar"),
table: t("book_properties.table"),
geoMap: t("book_properties.geo-map"),
board: t("book_properties.board"),
presentation: t("book_properties.presentation")
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: t("book_properties.grid"),
list: t("book_properties.list"),
calendar: t("book_properties.calendar"),
table: t("book_properties.table"),
geoMap: t("book_properties.geo-map"),
board: t("book_properties.board"),
presentation: t("book_properties.presentation")
};
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function CollectionPropertiesTab({ note }: TabContext) {
const [viewType, setViewType] = useViewType(note);
const properties = bookPropertiesConfig[viewType].properties;
const [ viewType, setViewType ] = useNoteLabel(note, "viewType");
const defaultViewType = (note?.type === "search" ? "list" : "grid");
const viewTypeWithDefault = (viewType ?? defaultViewType) as ViewTypeOptions;
const properties = bookPropertiesConfig[viewTypeWithDefault].properties;
return (
<div className="book-properties-widget">
{note && (
<>
{!isNewLayout && <CollectionTypeSwitcher viewType={viewType} setViewType={setViewType} />}
<BookProperties viewType={viewType} note={note} properties={properties} />
</>
)}
</div>
);
}
export function useViewType(note: FNote | null | undefined) {
const [ viewType, setViewType ] = useNoteLabel(note, "viewType");
const defaultViewType = (note?.type === "search" ? "list" : "grid");
const viewTypeWithDefault = (viewType ?? defaultViewType) as ViewTypeOptions;
return [ viewTypeWithDefault, setViewType ] as const;
return (
<div className="book-properties-widget">
{note && (
<>
<CollectionTypeSwitcher viewType={viewTypeWithDefault} setViewType={setViewType} />
<BookProperties viewType={viewTypeWithDefault} note={note} properties={properties} />
</>
)}
</div>
);
}
function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, setViewType: (newValue: string) => void }) {
@@ -155,7 +148,7 @@ function NumberPropertyView({ note, property }: { note: FNote, property: NumberP
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
style={{ width: (property.width ?? 100) + "px" }}
min={property.min ?? 0}
disabled={disabled}
/>

View File

@@ -1,16 +1,24 @@
import { EditedNotesResponse } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { TabContext } from "./ribbon-interface";
import { EditedNotesResponse } from "@triliumnext/commons";
import server from "../../services/server";
import { t } from "../../services/i18n";
import froca from "../../services/froca";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
export default function EditedNotesTab({ note }: TabContext) {
const editedNotes = useEditedNotes(note);
const [ editedNotes, setEditedNotes ] = useState<EditedNotesResponse>();
useEffect(() => {
if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes);
});
}, [ note?.noteId ]);
return (
<div className="edited-notes-widget" style={{
@@ -23,7 +31,7 @@ export default function EditedNotesTab({ note }: TabContext) {
<div className="edited-notes-list use-tn-links">
{joinElements(editedNotes.map(editedNote => {
return (
<span key={editedNote.noteId} className="edited-note-line">
<span className="edited-note-line">
{editedNote.isDeleted ? (
<i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i>
) : (
@@ -32,28 +40,12 @@ export default function EditedNotesTab({ note }: TabContext) {
</>
)}
</span>
);
)
}), " ")}
</div>
) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
)}
</div>
);
}
export function useEditedNotes(note: FNote | null | undefined) {
const [ editedNotes, setEditedNotes ] = useState<EditedNotesResponse>();
useEffect(() => {
if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes);
});
}, [ note ]);
return editedNotes;
)
}

View File

@@ -1,13 +1,13 @@
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import toast from "../../services/toast";
import { formatSize } from "../../services/utils";
import Button from "../react/Button";
import { FormFileUploadButton } from "../react/FormFileUpload";
import { useNoteBlob, useNoteLabel } from "../react/hooks";
import Button from "../react/Button";
import protected_session_holder from "../../services/protected_session_holder";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import toast from "../../services/toast";
import server from "../../services/server";
import FNote from "../../entities/fnote";
export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
@@ -54,7 +54,19 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
icon="bx bx-folder-open"
text={t("file_properties.upload_new_revision")}
disabled={!canAccessProtectedNote}
onChange={buildUploadNewFileRevisionListener(note)}
onChange={(fileToUpload) => {
if (!fileToUpload) {
return;
}
server.upload(`notes/${note.noteId}/file`, fileToUpload[0]).then((result) => {
if (result.uploaded) {
toast.showMessage(t("file_properties.upload_success"));
} else {
toast.showError(t("file_properties.upload_failed"));
}
});
}}
/>
</div>
</td>
@@ -65,19 +77,3 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
</div>
);
}
export function buildUploadNewFileRevisionListener(note: FNote) {
return (fileToUpload: FileList | null) => {
if (!fileToUpload) {
return;
}
server.upload(`notes/${note.noteId}/file`, fileToUpload[0]).then((result) => {
if (result.uploaded) {
toast.showMessage(t("file_properties.upload_success"));
} else {
toast.showError(t("file_properties.upload_failed"));
}
});
};
}

View File

@@ -1,151 +0,0 @@
import { NoteType } from "@triliumnext/commons";
import { beforeAll, describe, expect, it, vi } from "vitest";
import NoteContext from "../../components/note_context";
import { ViewMode } from "../../services/link";
import { randomString } from "../../services/utils";
import { buildNote } from "../../test/easy-froca";
import { getFormattingToolbarState } from "./FormattingToolbar";
interface NoteContextInfo {
type: NoteType;
viewScope?: ViewMode;
isReadOnly?: boolean;
}
describe("Formatting toolbar logic", () => {
beforeAll(() => {
vi.mock("../../services/tree.ts", () => ({
default: {
getActiveContextNotePath() {
return "root";
},
resolveNotePath(inputNotePath: string) {
return inputNotePath;
},
getNoteIdFromUrl(url) {
return url.split("/").at(-1);
}
}
}));
buildNote({
id: "root",
title: "Root"
});
});
async function buildConfig(noteContextInfos: NoteContextInfo[], activeIndex: number = 0) {
const noteContexts: NoteContext[] = [];
for (const noteContextData of noteContextInfos) {
const noteContext = new NoteContext(randomString(10));
const note = buildNote({
title: randomString(5),
type: noteContextData.type
});
noteContext.noteId = note.noteId;
expect(noteContext.note).toBe(note);
noteContext.viewScope = {
viewMode: noteContextData.viewScope ?? "default"
};
noteContext.isReadOnly = async () => !!noteContextData.isReadOnly;
noteContext.getSubContexts = () => [];
noteContexts.push(noteContext);
};
const mainNoteContext = noteContexts[0];
for (const noteContext of noteContexts) {
noteContext.getMainContext = () => mainNoteContext;
}
mainNoteContext.getSubContexts = () => noteContexts;
return noteContexts[activeIndex];
}
async function testSplit(noteContextInfos: NoteContextInfo[], activeIndex: number = 0, editor = "ckeditor-classic") {
const noteContext = await buildConfig(noteContextInfos, activeIndex);
return await getFormattingToolbarState(noteContext, noteContext.note, editor);
}
describe("Single split", () => {
it("should be hidden for floating toolbar", async () => {
expect(await testSplit([ { type: "text" } ], 0, "ckeditor-balloon")).toBe("hidden");
});
it("should be visible for single text note", async () => {
expect(await testSplit([ { type: "text" } ])).toBe("visible");
});
it("should be hidden for read-only text note", async () => {
expect(await testSplit([ { type: "text", isReadOnly: true } ])).toBe("hidden");
});
it("should be hidden for non-text note", async () => {
expect(await testSplit([ { type: "code" } ])).toBe("hidden");
});
it("should be hidden for wrong view mode", async () => {
expect(await testSplit([ { type: "text", viewScope: "attachments" } ])).toBe("hidden");
});
});
describe("Multi split", () => {
it("should be hidden for floating toolbar", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text" },
], 0, "ckeditor-balloon")).toBe("hidden");
});
it("should be visible for two text notes", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text" },
])).toBe("visible");
});
it("should be disabled if on a non-text note", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "code" },
], 1)).toBe("disabled");
});
it("should be hidden for all non-text notes", async () => {
expect(await testSplit([
{ type: "code" },
{ type: "canvas" },
])).toBe("hidden");
});
it("should be hidden for all read-only text notes", async () => {
expect(await testSplit([
{ type: "text", isReadOnly: true },
{ type: "text", isReadOnly: true },
])).toBe("hidden");
});
it("should be visible for mixed view mode", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text", viewScope: "attachments" }
])).toBe("visible");
});
it("should be hidden for all wrong view mode", async () => {
expect(await testSplit([
{ type: "text", viewScope: "attachments" },
{ type: "text", viewScope: "attachments" }
])).toBe("hidden");
});
it("should be disabled for wrong view mode", async () => {
expect(await testSplit([
{ type: "text" },
{ type: "text", viewScope: "attachments" }
], 1)).toBe("disabled");
});
});
});

View File

@@ -1,9 +1,5 @@
import clsx from "clsx";
import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { useActiveNoteContext, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { useRef } from "preact/hooks";
import { useTriliumEvent, useTriliumOption } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
/**
@@ -37,116 +33,5 @@ export default function FormattingToolbar({ hidden, ntxId }: TabContext) {
ref={containerRef}
className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`}
/>
);
)
};
const toolbarCache = new Map<string, HTMLElement | null | undefined>();
export function FixedFormattingToolbar() {
const containerRef = useRef<HTMLDivElement>(null);
const { note, noteContext, ntxId } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const renderState = useRenderState(noteContext, note);
const [ toolbarToRender, setToolbarToRender ] = useState<HTMLElement | null | undefined>();
// Populate the cache with the toolbar of every note context.
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
if (!eventNtxId) return;
const toolbar = editor.ui.view.toolbar?.element;
toolbarCache.set(eventNtxId, toolbar);
// Replace on the spot if the editor crashed.
if (eventNtxId === ntxId) {
setToolbarToRender(toolbar);
}
});
// Clean the cache when tabs are closed.
useTriliumEvent("noteContextRemoved", ({ ntxIds: eventNtxIds }) => {
for (const eventNtxId of eventNtxIds) {
toolbarCache.delete(eventNtxId);
}
});
// Switch between the cached toolbar when user navigates to a different note context.
useEffect(() => {
if (!ntxId) return;
const toolbar = toolbarCache.get(ntxId);
if (toolbar) {
setToolbarToRender(toolbar);
}
}, [ ntxId, noteType, noteContext ]);
// Render the toolbar.
useEffect(() => {
if (toolbarToRender) {
containerRef.current?.replaceChildren(toolbarToRender);
} else {
containerRef.current?.replaceChildren();
}
}, [ toolbarToRender ]);
return (
<div
ref={containerRef}
className={clsx("classic-toolbar-widget", {
"hidden-ext": renderState === "hidden",
"disabled": renderState === "disabled"
})}
/>
);
}
function useRenderState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined) {
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
const [ state, setState ] = useState("hidden");
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved", "readOnlyTemporarilyDisabled" ], () => {
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
});
useEffect(() => {
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
}, [ activeNoteContext, activeNote, textNoteEditorType ]);
return state;
}
export async function getFormattingToolbarState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined, textNoteEditorType: string) {
if (!activeNoteContext || textNoteEditorType !== "ckeditor-classic") {
return "hidden";
}
const subContexts = activeNoteContext?.getMainContext().getSubContexts() ?? [];
if (subContexts.length === 1) {
if (activeNote?.type !== "text" || activeNoteContext.viewScope?.viewMode !== "default") {
return "hidden";
}
const isReadOnly = await activeNoteContext.isReadOnly();
if (isReadOnly) {
return "hidden";
}
return "visible";
}
// If there are multiple note contexts (e.g. splits), the logic is slightly different.
const textNoteContexts = subContexts.filter(s => s.note?.type === "text" && s.viewScope?.viewMode === "default");
const textNoteContextsReadOnly = await Promise.all(textNoteContexts.map(sc => sc.isReadOnly()));
// If all text notes are hidden, no need to display the toolbar at all.
if (textNoteContextsReadOnly.indexOf(false) === -1) {
return "hidden";
}
// If the current subcontext is not a text note, but there is at least an editable text then it must be disabled.
if (activeNote?.type !== "text") return "disabled";
// If the current subcontext is a text note, it must not be read-only.
const subContextIndex = textNoteContexts.indexOf(activeNoteContext);
if (subContextIndex !== -1) {
if (textNoteContextsReadOnly[subContextIndex]) return "disabled";
}
if (activeNoteContext.viewScope?.viewMode !== "default") return "disabled";
return "visible";
}

View File

@@ -1,16 +1,14 @@
import { useContext } from "preact/hooks";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import server from "../../services/server";
import toast from "../../services/toast";
import { useNoteBlob, useNoteLabel } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
import { clearBrowserCache, formatSize } from "../../services/utils";
import Button from "../react/Button";
import { FormFileUploadButton } from "../react/FormFileUpload";
import { useNoteBlob, useNoteLabel } from "../react/hooks";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import { ParentComponent } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
import { useContext } from "preact/hooks";
import { FormFileUploadButton } from "../react/FormFileUpload";
import server from "../../services/server";
import toast from "../../services/toast";
export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
@@ -62,27 +60,23 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
<FormFileUploadButton
text={t("image_properties.upload_new_revision")}
icon="bx bx-folder-open"
onChange={buildUploadNewImageRevisionListener(note)}
onChange={async (files) => {
if (!files) return;
const fileToUpload = files[0]; // copy to allow reset below
const result = await server.upload(`images/${note.noteId}`, fileToUpload);
if (result.uploaded) {
toast.showMessage(t("image_properties.upload_success"));
await clearBrowserCache();
} else {
toast.showError(t("image_properties.upload_failed", { message: result.message }));
}
}}
/>
</div>
</>
)}
</div>
);
}
export function buildUploadNewImageRevisionListener(note: FNote) {
return async (files: FileList | null) => {
if (!files) return;
const fileToUpload = files[0]; // copy to allow reset below
const result = await server.upload(`images/${note.noteId}`, fileToUpload);
if (result.uploaded) {
toast.showMessage(t("image_properties.upload_success"));
await clearBrowserCache();
} else {
toast.showError(t("image_properties.upload_failed", { message: result.message }));
}
};
)
}

View File

@@ -9,7 +9,7 @@ import RawHtml from "../react/RawHtml";
import { joinElements } from "../react/react_utils";
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
export default function InheritedAttributesTab({ note, componentId }: Pick<TabContext, "note" | "componentId">) {
export default function InheritedAttributesTab({ note, componentId }: TabContext) {
const [ inheritedAttributes, setInheritedAttributes ] = useState<FAttribute[]>();
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
@@ -34,7 +34,7 @@ export default function InheritedAttributesTab({ note, componentId }: Pick<TabCo
refresh();
}
});
return (
<div className="inherited-attributes-widget">
<div className="inherited-attributes-container selectable-text">
@@ -83,4 +83,4 @@ function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onC
onClick={onClick}
/>
);
}
}

View File

@@ -1,306 +1,174 @@
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 { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks";
import { useTriliumOption } from "../react/hooks";
import ActionButton from "../react/ActionButton"
import appContext, { CommandNames } from "../../components/app_context";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import branches from "../../services/branches";
import dialog from "../../services/dialog";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { t } from "../../services/i18n";
import protected_session from "../../services/protected_session";
import Dropdown from "../react/Dropdown";
import FNote from "../../entities/fnote"
import NoteContext from "../../components/note_context";
import server from "../../services/server";
import toast from "../../services/toast";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import ws from "../../services/ws";
import ClosePaneButton from "../buttons/close_pane_button";
import CreatePaneButton from "../buttons/create_pane_button";
import MovePaneButton from "../buttons/move_pane_button";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
import NoteActionsCustom from "./NoteActionsCustom";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
interface NoteActionsProps {
note?: FNote;
noteContext?: NoteContext;
}
export default function NoteActions() {
const { note, ntxId, noteContext } = useNoteContext();
return (
<div className="ribbon-button-container" style={{ contain: "none" }}>
{isNewLayout && (
<>
{note && ntxId && noteContext && <NoteActionsCustom note={note} ntxId={ntxId} noteContext={noteContext} />}
<MovePaneButton direction="left" />
<MovePaneButton direction="right" />
<ClosePaneButton />
<CreatePaneButton />
</>
)}
{note && !isNewLayout && <RevisionsButton note={note} />}
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
</div>
);
export default function NoteActions({ note, noteContext }: NoteActionsProps) {
return (
<>
{note && <RevisionsButton note={note} />}
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
</>
);
}
function RevisionsButton({ note }: { note: FNote }) {
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
return (isEnabled &&
<ActionButton
icon="bx bx-history"
text={t("revisions_button.note_revisions")}
triggerCommand="showRevisions"
titlePosition="bottom"
/>
);
return (isEnabled &&
<ActionButton
icon="bx bx-history"
text={t("revisions_button.note_revisions")}
triggerCommand="showRevisions"
titlePosition="bottom"
/>
);
}
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
const parentComponent = useContext(ParentComponent);
const noteType = useNoteProperty(note, "type") ?? "";
const [viewType] = useNoteLabel(note, "viewType");
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
const isExportableToImage = ["mermaid", "mindMap"].includes(noteType);
const isContentAvailable = note.isContentAvailable();
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType);
const isHelpPage = note.noteId.startsWith("_help");
const [syncServerHost] = useTriliumOption("syncServerHost");
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isNormalViewMode = noteContext?.viewScope?.viewMode === "default";
const parentComponent = useContext(ParentComponent);
const noteType = useNoteProperty(note, "type") ?? "";
const [ viewType ] = useNoteLabel(note, "viewType");
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType);
const [ syncServerHost ] = useTriliumOption("syncServerHost");
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
return (
<Dropdown
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
className="note-actions"
hideToggleArrow
noSelectButtonStyle
iconAction>
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
command={() => enableEditing()} />
<FormDropdownDivider />
</>}
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
{isNewLayout && <CommandItem command="toggleRibbonTabNoteMap" icon="bx bxs-network-chart" disabled={isInOptionsOrHelp} text={t("note_actions.note_map")} />}
<FormDropdownDivider />
{isNewLayout && isNormalViewMode && !isHelpPage && <>
<NoteBasicProperties note={note} />
<FormDropdownDivider />
</>}
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath,
defaultType: "single"
})} />
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
{isExportableToImage && isNormalViewMode && isContentAvailable && <ExportAsImage ntxId={noteContext.ntxId} parentComponent={parentComponent} />}
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
<FormDropdownDivider />
<CommandItem command="showRevisions" icon="bx bx-history" text={t("note_actions.view_revisions")} />
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<FormDropdownDivider />
{canBeConvertedToAttachment && <ConvertToAttachment note={note} />}
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")}
/>}
<FormDropdownSubmenu icon="bx bx-wrench" title={t("note_actions.advanced")} dropStart>
<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="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
{(syncServerHost && isElectron) &&
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
}
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
</FormDropdownSubmenu>
<FormDropdownDivider />
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
disabled={isInOptionsOrHelp}
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
/>
</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-share-alt"
title={t("shared_switch.shared")}
currentValue={isShared} onChange={switchShareState}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
<FormListToggleableItem
icon="bx bx-lock-alt"
title={t("protect_note.toggle-on")}
currentValue={!!isProtected} onChange={shouldProtect => protected_session.protectNote(note.noteId, shouldProtect, false)}
/>
<FormListToggleableItem
icon="bx bx-bookmark"
title={t("bookmark_switch.bookmark")}
currentValue={isBookmarked} onChange={setIsBookmarked}
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
/>
return (
<Dropdown
buttonClassName="bx bx-dots-vertical-rounded"
className="note-actions"
hideToggleArrow
noSelectButtonStyle
iconAction>
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
command={() => enableEditing()} />
<FormDropdownDivider />
</>}
<NoteTypeDropdown note={note} />
<EditabilityDropdown note={note} />
{canBeConvertedToAttachment && <ConvertToAttachment note={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="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")} />}
<FormDropdownDivider />
<FormListToggleableItem
icon="bx bx-copy-alt"
title={t("template_switch.template")}
currentValue={isTemplate} onChange={setIsTemplate}
helpPage="KC1HB96bqqHX"
disabled={note?.noteId.startsWith("_options")}
/>
</>;
}
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath,
defaultType: "single"
})} />
<FormDropdownDivider />
function EditabilityDropdown({ note }: { note: FNote }) {
const [ readOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const [ autoReadOnlyDisabled, setAutoReadOnlyDisabled ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
<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="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
{(syncServerHost && isElectron) &&
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
}
<FormDropdownDivider />
function setState(readOnly: boolean, autoReadOnlyDisabled: boolean) {
setReadOnly(readOnly);
setAutoReadOnlyDisabled(autoReadOnlyDisabled);
}
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
disabled={isInOptionsOrHelp}
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
/>
<FormDropdownDivider />
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");
return (
<FormDropdownSubmenu title={t("basic_properties.note_type")} icon="bx bx-file" dropStart>
<NoteTypeDropdownContent
currentNoteType={currentNoteType}
currentNoteMime={currentNoteMime}
note={note}
setModalShown={() => { /* no-op since no code notes are displayed here */ }}
noCodeNotes
/>
</FormDropdownSubmenu>
);
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
</Dropdown>
);
}
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
return (
<>
<FormListHeader text="Development Actions" />
<FormListHeader text="Development-only Actions" />
<FormListItem
icon="bx bx-printer"
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
>Open print page</FormListItem>
<FormListItem
icon="bx bx-error"
disabled={note.type !== "text"}
onClick={() => {
noteContext?.getTextEditor(editor => {
editor.editing.view.change(() => {
throw new Error("Editor crashed.");
{note.type === "text" && (
<FormListItem
icon="bx bx-error"
onClick={() => {
noteContext?.getTextEditor(editor => {
editor.editing.view.change(() => {
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 }) {
return <FormListItem
icon={icon}
title={title}
triggerCommand={typeof command === "string" ? command : undefined}
onClick={typeof command === "function" ? command : undefined}
disabled={disabled}
>{text}</FormListItem>;
return <FormListItem
icon={icon}
title={title}
triggerCommand={typeof command === "string" ? command : undefined}
onClick={typeof command === "function" ? command : undefined}
disabled={disabled}
>{text}</FormListItem>
}
function ConvertToAttachment({ note }: { note: FNote }) {
return (
<FormListItem
icon="bx bx-paperclip"
onClick={async () => {
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
return;
}
return (
<FormListItem
icon="bx bx-paperclip"
onClick={async () => {
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
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) {
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
return;
}
if (!newAttachment) {
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
return;
}
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: {
viewMode: "attachments",
attachmentId: newAttachment.attachmentId
}
});
}}
>{t("note_actions.convert_into_attachment")}</FormListItem>
);
}
function ExportAsImage({ ntxId, parentComponent }: { ntxId: string | null | undefined, parentComponent: Component | null | undefined }) {
return (
<FormDropdownSubmenu
icon="bx bxs-file-image"
title={t("note_actions.export_as_image")}
dropStart
>
<FormListItem
icon="bx bxs-file-png"
onClick={() => parentComponent?.triggerEvent("exportPng", { ntxId })}
>{t("note_actions.export_as_image_png")}</FormListItem>
<FormListItem
icon="bx bx-shape-polygon"
onClick={() => parentComponent?.triggerEvent("exportSvg", { ntxId })}
>{t("note_actions.export_as_image_svg")}</FormListItem>
</FormDropdownSubmenu>
);
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: {
viewMode: "attachments",
attachmentId: newAttachment.attachmentId
}
});
}}
>{t("note_actions.convert_into_attachment")}</FormListItem>
)
}

View File

@@ -1,258 +0,0 @@
import { NoteType } from "@triliumnext/commons";
import { useContext, useEffect, useState } from "preact/hooks";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import { openInAppHelpFromUrl } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface";
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
import ActionButton from "../react/ActionButton";
import { FormFileUploadActionButton } from "../react/FormFileUpload";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
interface NoteActionsCustomProps {
note: FNote;
ntxId: string;
noteContext: NoteContext;
}
interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
noteMime: string;
noteType: NoteType;
isReadOnly: boolean;
isDefaultViewMode: boolean;
parentComponent: Component;
viewType: ViewTypeOptions | null | undefined;
}
/**
* Part of {@link NoteActions} on the new layout, but are rendered with a slight spacing
* from the rest of the note items and the buttons differ based on the note type.
*/
export default function NoteActionsCustom(props: NoteActionsCustomProps) {
const { note } = props;
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const [ viewType ] = useNoteLabel(note, "viewType");
const parentComponent = useContext(ParentComponent);
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const innerProps: NoteActionsCustomInnerProps | false = !!noteType && noteMime !== undefined && !!parentComponent && {
...props,
noteType,
noteMime,
viewType: viewType as ViewTypeOptions | null | undefined,
isDefaultViewMode: props.noteContext.viewScope?.viewMode === "default",
parentComponent,
isReadOnly
};
return (innerProps &&
<div className="note-actions-custom">
<AddChildButton {...innerProps} />
<RunActiveNoteButton {...innerProps } />
<OpenTriliumApiDocsButton {...innerProps} />
<SwitchSplitOrientationButton {...innerProps} />
<ToggleReadOnlyButton {...innerProps} />
<SaveToNoteButton {...innerProps} />
<RefreshButton {...innerProps} />
<CopyReferenceToClipboardButton {...innerProps} />
<InAppHelpButton {...innerProps} />
<NoteActionsCustomInner {...innerProps} />
</div>
);
}
//#region Note type mappings
function NoteActionsCustomInner(props: NoteActionsCustomInnerProps) {
switch (props.note.type) {
case "file":
return <FileActions {...props} />;
case "image":
return <ImageActions {...props} />;
default:
return null;
}
}
function FileActions(props: NoteActionsCustomInnerProps) {
return (
<>
<UploadNewRevisionButton {...props} onChange={buildUploadNewFileRevisionListener(props.note)} />
<OpenExternallyButton {...props} />
<DownloadFileButton {...props} />
</>
);
}
function ImageActions(props: NoteActionsCustomInnerProps) {
return (
<>
<UploadNewRevisionButton {...props} onChange={buildUploadNewImageRevisionListener(props.note)} />
<OpenExternallyButton {...props} />
<DownloadFileButton {...props} />
</>
);
}
//#endregion
//#region Shared buttons
function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps & {
onChange: (files: FileList | null) => void;
}) {
return (
<FormFileUploadActionButton
icon="bx bx-folder-open"
text={t("image_properties.upload_new_revision")}
disabled={!note.isContentAvailable()}
onChange={onChange}
/>
);
}
function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-link-external"
text={t("file_properties.open")}
disabled={note.isProtected}
onClick={() => openNoteExternally(note.noteId, noteMime)}
/>
);
}
function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-download"
text={t("file_properties.download")}
disabled={!note.isContentAvailable()}
onClick={() => downloadFileNote(note.noteId)}
/>
);
}
//#region Floating buttons
function CopyReferenceToClipboardButton({ ntxId, noteType, parentComponent }: NoteActionsCustomInnerProps) {
return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) &&
<ActionButton
text={t("image_properties.copy_reference_to_clipboard")}
icon="bx bx-copy"
onClick={() => parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })}
/>
);
}
function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, noteContext }: NoteActionsCustomInnerProps) {
const isEnabled = (note.noteId === "_backendLog" || noteType === "render") && isDefaultViewMode;
return (isEnabled &&
<ActionButton
text={t("backend_log.refresh")}
icon="bx bx-refresh"
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
/>
);
}
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const isShown = note.type === "mermaid" && note.isContentAvailable() && isDefaultViewMode;
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
return isShown && <ActionButton
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
disabled={isReadOnly}
/>;
}
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <ActionButton
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
onClick={() => setReadOnly(!isReadOnly)}
/>;
}
function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript") || noteMime === "text/x-sqlite;schema=trilium";
return isEnabled && <ActionButton
icon="bx bx-play"
text={t("code_buttons.execute_button_title")}
triggerCommand="runActiveNote"
/>;
}
function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
const [ isEnabled, setIsEnabled ] = useState(false);
function refresh() {
setIsEnabled(noteMime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely());
}
useEffect(refresh, [ note, noteMime ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getBranchRows().find(b => b.noteId === note.noteId)) {
refresh();
}
});
return isEnabled && <ActionButton
icon="bx bx-save"
text={t("code_buttons.save_to_note_button_title")}
onClick={buildSaveSqlToNoteHandler(note)}
/>;
}
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript;env=");
return isEnabled && <ActionButton
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
/>;
}
function InAppHelpButton({ note, noteType }: NoteActionsCustomInnerProps) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl && (noteType !== "book");
return isEnabled && (
<ActionButton
icon="bx bx-help-circle"
text={t("help-button.title")}
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
/>
);
}
function AddChildButton({ parentComponent, noteType, viewType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
if (noteType === "book" && viewType === "geoMap") {
return <ActionButton
icon="bx bx-plus-circle"
text={t("geo-map.create-child-note-title")}
onClick={() => parentComponent.triggerEvent("geoMapCreateChildNote", { ntxId })}
disabled={isReadOnly}
/>;
} else if (noteType === "relationMap") {
return <ActionButton
icon="bx bx-folder-plus"
text={t("relation_map_buttons.create_child_note_title")}
onClick={() => parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId })}
disabled={isReadOnly}
/>;
}
}
//#endregion

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { TabContext } from "./ribbon-interface";
import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
import server from "../../services/server";
import Button from "../react/Button";
@@ -7,75 +8,12 @@ import { formatDateTime } from "../../utils/formatters";
import { formatSize } from "../../services/utils";
import LoadingSpinner from "../react/LoadingSpinner";
import { useTriliumEvent } from "../react/hooks";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import FNote from "../../entities/fnote";
import LinkButton from "../react/LinkButton";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function NoteInfoTab({ note }: { note: FNote | null | undefined }) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
return (
<div className="note-info-widget">
{note && (
<>
<div className="note-info-item">
<span>{t("note_info_widget.note_id")}:</span>
<span className="note-info-id selectable-text">{note.noteId}</span>
</div>
{!isNewLayout && <div className="note-info-item">
<span>{t("note_info_widget.created")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
</div>}
{!isNewLayout && <div className="note-info-item">
<span>{t("note_info_widget.modified")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
</div>}
<div className="note-info-item">
<span>{t("note_info_widget.type")}:</span>
<span>
<span className="note-info-type">{note.type}</span>{' '}
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
</span>
</div>
<div className="note-info-item">
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
<span className="note-info-size-col-span">
<NoteSizeWidget {...sizeProps} />
</span>
</div>
</>
)}
</div>
);
}
export function NoteSizeWidget({ isLoading, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }: Omit<ReturnType<typeof useNoteMetadata>, "metadata">) {
return <>
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<LinkButton
text={t("note_info_widget.calculate")}
onClick={requestSizeInfo}
/>
)}
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</>;
}
export function useNoteMetadata(note: FNote | null | undefined) {
export default function NoteInfoTab({ note }: TabContext) {
const [ metadata, setMetadata ] = useState<MetadataResponse>();
const [ isLoading, setIsLoading ] = useState(false);
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();
const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState<SubtreeSizeResponse>();
const [ metadata, setMetadata ] = useState<MetadataResponse>();
function refresh() {
if (note) {
@@ -87,20 +25,7 @@ export function useNoteMetadata(note: FNote | null | 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 ]);
useEffect(refresh, [ note?.noteId ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) {
@@ -108,5 +33,62 @@ export function useNoteMetadata(note: FNote | null | undefined) {
}
});
return { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo };
return (
<div className="note-info-widget">
{note && (
<>
<div className="note-info-item">
<span>{t("note_info_widget.note_id")}:</span>
<span className="note-info-id selectable-text">{note.noteId}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.created")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.modified")}:</span>
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.type")}:</span>
<span>
<span className="note-info-type">{note.type}</span>{' '}
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
</span>
</div>
<div className="note-info-item">
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
<span className="note-info-size-col-span">
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<Button
className="calculate-button"
icon="bx bx-calculator"
text={t("note_info_widget.calculate")}
onClick={() => {
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);
}}
/>
)}
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</span>
</div>
</>
)}
</div>
)
}

View File

@@ -1,25 +1,33 @@
import { useEffect, useMemo, useState } from "preact/hooks";
import FNote, { NotePathRecord } from "../../entities/fnote";
import { TabContext } from "./ribbon-interface";
import { t } from "../../services/i18n";
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks";
import { useEffect, useMemo, useState } from "preact/hooks";
import { NotePathRecord } from "../../entities/fnote";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
import LinkButton from "../react/LinkButton";
import clsx from "clsx";
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
return <NotePathsWidget sortedNotePaths={sortedNotePaths} currentNotePath={notePath} />;
}
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
function refresh() {
if (!note) return;
setSortedNotePaths(note
.getSortedNotePathRecords(hoistedNoteId)
.filter((notePath) => !notePath.isHidden));
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (!noteId) return;
if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId)
|| loadResults.isNoteReloaded(noteId)) {
refresh();
}
});
export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
sortedNotePaths: NotePathRecord[] | undefined;
currentNotePath?: string | null | undefined;
}) {
return (
<div class="note-paths-widget">
<>
@@ -30,48 +38,24 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
<ul className="note-path-list">
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
<NotePath
key={sortedNotePath.notePath}
currentNotePath={currentNotePath}
currentNotePath={notePath}
notePathRecord={sortedNotePath}
/>
)) : undefined}
</ul>
<LinkButton
text={t("note_paths.clone_button")}
<Button
triggerCommand="cloneNoteIdsTo"
text={t("note_paths.clone_button")}
/>
</>
</div>
);
}
export function useSortedNotePaths(note: FNote | null | undefined, hoistedNoteId?: string) {
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
function refresh() {
if (!note) return;
setSortedNotePaths(note
.getSortedNotePathRecords(hoistedNoteId)
.filter((notePath) => !notePath.isHidden));
}
useEffect(refresh, [ note, hoistedNoteId ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (!noteId) return;
if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId)
|| loadResults.isNoteReloaded(noteId)) {
refresh();
}
});
return sortedNotePaths;
)
}
function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: string | null, notePathRecord?: NotePathRecord }) {
const notePath = notePathRecord?.notePath;
const notePathString = useMemo(() => (notePath ?? []).join("/"), [ notePath ]);
const notePath = notePathRecord?.notePath ?? [];
const notePathString = useMemo(() => notePath.join("/"), [ notePath ]);
const [ classes, icons ] = useMemo(() => {
const classes: string[] = [];
@@ -84,17 +68,17 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
classes.push("path-in-hoisted-subtree");
} else {
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") });
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") })
}
if (notePathRecord?.isArchived) {
classes.push("path-archived");
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") });
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") })
}
if (notePathRecord?.isSearch) {
classes.push("path-search");
icons.push({ icon: "bx bx-search", title: t("note_paths.search") });
icons.push({ icon: "bx bx-search", title: t("note_paths.search") })
}
return [ classes.join(" "), icons ];
@@ -103,23 +87,20 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
// Determine the full note path (for the links) of every component of the current note path.
const pathSegments: string[] = [];
const fullNotePaths: string[] = [];
for (const noteId of notePath ?? []) {
for (const noteId of notePath) {
pathSegments.push(noteId);
fullNotePaths.push(pathSegments.join("/"));
}
return (
<li class={classes}>
{joinElements(fullNotePaths.map((notePath, index, arr) => (
<NoteLink key={notePath}
className={clsx({"basename": (index === arr.length - 1)})}
notePath={notePath}
noPreview />
{joinElements(fullNotePaths.map(notePath => (
<NoteLink notePath={notePath} noPreview />
)), NOTE_PATH_TITLE_SEPARATOR)}
{icons.map(({ icon, title }) => (
<i key={title} class={icon} title={title} />
<span class={icon} title={title} />
))}
</li>
);
)
}

View File

@@ -1,5 +1,4 @@
import { useMemo, useRef } from "preact/hooks";
import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks";
import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor";
import { TabContext } from "./ribbon-interface";
@@ -26,5 +25,5 @@ export default function OwnedAttributesTab({ note, hidden, activate, ntxId, ...r
<AttributeEditor api={api} ntxId={ntxId} note={note} {...restProps} hidden={hidden} />
)}
</div>
);
)
}

View File

@@ -1,15 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useElementSize, useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import "./style.css";
import { KeyboardActionNames } from "@triliumnext/commons";
import clsx from "clsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { EventNames } from "../../components/app_context";
import { Indexed, numberObjectsInPlace } from "../../services/utils";
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import { EventNames } from "../../components/app_context";
import NoteActions from "./NoteActions";
import { TabConfiguration, TitleContext } from "./ribbon-interface";
import { KeyboardActionNames } from "@triliumnext/commons";
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
import { TabConfiguration, TitleContext } from "./ribbon-interface";
import clsx from "clsx";
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
@@ -43,6 +42,16 @@ export default function Ribbon() {
refresh();
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
// Manage height.
const containerRef = useRef<HTMLDivElement>(null);
const size = useElementSize(containerRef);
useEffect(() => {
if (!containerRef.current || !size) return;
const parentEl = containerRef.current.closest<HTMLDivElement>(".note-split");
if (!parentEl) return;
parentEl.style.setProperty("--ribbon-height", `${size.height}px`);
}, [ size ]);
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
useEffect(() => {
if (!computedTabs) return;
@@ -55,7 +64,7 @@ export default function Ribbon() {
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
if (!computedTabs) return;
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
if (correspondingTab?.shouldShow) {
if (correspondingTab) {
if (activeTabIndex !== correspondingTab.index) {
setActiveTabIndex(correspondingTab.index);
} else {
@@ -67,6 +76,7 @@ export default function Ribbon() {
const shouldShowRibbon = (noteContext?.viewScope?.viewMode === "default" && !noteContext.noteId?.startsWith("_options"));
return (
<div
ref={containerRef}
className={clsx("ribbon-container", !shouldShowRibbon && "hidden-ext")}
style={{ contain: "none" }}
>
@@ -89,7 +99,9 @@ export default function Ribbon() {
/>
))}
</div>
<NoteActions />
<div className="ribbon-button-container">
{ note && <NoteActions note={note} noteContext={noteContext} /> }
</div>
</div>
<div className="ribbon-body-container">
@@ -112,7 +124,7 @@ export default function Ribbon() {
noteContext={noteContext}
componentId={componentId}
activate={useCallback(() => {
setActiveTabIndex(tab.index);
setActiveTabIndex(tab.index)
}, [setActiveTabIndex])}
/>
</div>
@@ -120,7 +132,7 @@ export default function Ribbon() {
})}
</div>
</div>
);
)
}
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
@@ -143,7 +155,7 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
<div class="ribbon-tab-spacer" />
</>
);
)
}
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {

View File

@@ -1,27 +1,27 @@
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 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";
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{
title: t("classic_editor_toolbar.title"),
icon: "bx bx-text",
show: async ({ note, noteContext }) => note?.type === "text" && noteContext?.viewScope?.viewMode === "default"
show: async ({ note, noteContext }) => note?.type === "text"
&& options.get("textNoteEditorType") === "ckeditor-classic"
&& !(await noteContext?.isReadOnly()),
toggleCommand: "toggleRibbonTabClassicEditor",
@@ -56,7 +56,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
title: t("book_properties.book_properties"),
icon: "bx bx-book",
content: CollectionPropertiesTab,
show: ({ note }) => (note?.type === "book" || note?.type === "search"),
show: ({ note }) => note?.type === "book" || note?.type === "search",
toggleCommand: "toggleRibbonTabBookProperties"
},
{
@@ -83,6 +83,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
activate: true,
},
{
// BasicProperties
title: t("basic_properties.basic_properties"),
icon: "bx bx-slider",
content: BasicPropertiesTab,

View File

@@ -1,361 +1,360 @@
import { AttributeType } from "@triliumnext/commons";
import FormTextArea from "../react/FormTextArea";
import NoteAutocomplete from "../react/NoteAutocomplete";
import FormSelect from "../react/FormSelect";
import Icon from "../react/Icon";
import FormTextBox from "../react/FormTextBox";
import { ComponentChildren, VNode } from "preact";
import { useEffect, useMemo, useRef } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import { removeOwnedAttributesByNameOrType } from "../../services/attributes";
import { t } from "../../services/i18n";
import server from "../../services/server";
import FormSelect from "../react/FormSelect";
import FormTextArea from "../react/FormTextArea";
import FormTextBox from "../react/FormTextBox";
import HelpRemoveButtons from "../react/HelpRemoveButtons";
import { AttributeType } from "@triliumnext/commons";
import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
import Icon from "../react/Icon";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { t } from "../../services/i18n";
import { useEffect, useMemo, useRef } from "preact/hooks";
import appContext from "../../components/app_context";
import server from "../../services/server";
import HelpRemoveButtons from "../react/HelpRemoveButtons";
export interface SearchOption {
attributeName: string;
attributeType: "label" | "relation";
icon: string;
label: string;
tooltip?: string;
component: (props: SearchOptionProps) => VNode;
defaultValue?: string;
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
attributeName: string;
attributeType: "label" | "relation";
icon: string;
label: string;
tooltip?: string;
component: (props: SearchOptionProps) => VNode;
defaultValue?: string;
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
}
interface SearchOptionProps {
note: FNote;
refreshResults: () => void;
attributeName: string;
attributeType: "label" | "relation";
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
defaultValue?: string;
error?: { message: string };
note: FNote;
refreshResults: () => void;
attributeName: string;
attributeType: "label" | "relation";
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
defaultValue?: string;
error?: { message: string };
}
export const SEARCH_OPTIONS: SearchOption[] = [
{
attributeName: "searchString",
attributeType: "label",
icon: "bx bx-text",
label: t("search_definition.search_string"),
component: SearchStringOption
},
{
attributeName: "searchScript",
attributeType: "relation",
defaultValue: "root",
icon: "bx bx-code",
label: t("search_definition.search_script"),
component: SearchScriptOption
},
{
attributeName: "ancestor",
attributeType: "relation",
defaultValue: "root",
icon: "bx bx-filter-alt",
label: t("search_definition.ancestor"),
component: AncestorOption,
additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ]
},
{
attributeName: "fastSearch",
attributeType: "label",
icon: "bx bx-run",
label: t("search_definition.fast_search"),
tooltip: t("search_definition.fast_search_description"),
component: FastSearchOption
},
{
attributeName: "includeArchivedNotes",
attributeType: "label",
icon: "bx bx-archive",
label: t("search_definition.include_archived"),
tooltip: t("search_definition.include_archived_notes_description"),
component: IncludeArchivedNotesOption
},
{
attributeName: "orderBy",
attributeType: "label",
defaultValue: "relevancy",
icon: "bx bx-arrow-from-top",
label: t("search_definition.order_by"),
component: OrderByOption,
additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ]
},
{
attributeName: "limit",
attributeType: "label",
defaultValue: "10",
icon: "bx bx-stop",
label: t("search_definition.limit"),
tooltip: t("search_definition.limit_description"),
component: LimitOption
},
{
attributeName: "debug",
attributeType: "label",
icon: "bx bx-bug",
label: t("search_definition.debug"),
tooltip: t("search_definition.debug_description"),
component: DebugOption
}
{
attributeName: "searchString",
attributeType: "label",
icon: "bx bx-text",
label: t("search_definition.search_string"),
component: SearchStringOption
},
{
attributeName: "searchScript",
attributeType: "relation",
defaultValue: "root",
icon: "bx bx-code",
label: t("search_definition.search_script"),
component: SearchScriptOption
},
{
attributeName: "ancestor",
attributeType: "relation",
defaultValue: "root",
icon: "bx bx-filter-alt",
label: t("search_definition.ancestor"),
component: AncestorOption,
additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ]
},
{
attributeName: "fastSearch",
attributeType: "label",
icon: "bx bx-run",
label: t("search_definition.fast_search"),
tooltip: t("search_definition.fast_search_description"),
component: FastSearchOption
},
{
attributeName: "includeArchivedNotes",
attributeType: "label",
icon: "bx bx-archive",
label: t("search_definition.include_archived"),
tooltip: t("search_definition.include_archived_notes_description"),
component: IncludeArchivedNotesOption
},
{
attributeName: "orderBy",
attributeType: "label",
defaultValue: "relevancy",
icon: "bx bx-arrow-from-top",
label: t("search_definition.order_by"),
component: OrderByOption,
additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ]
},
{
attributeName: "limit",
attributeType: "label",
defaultValue: "10",
icon: "bx bx-stop",
label: t("search_definition.limit"),
tooltip: t("search_definition.limit_description"),
component: LimitOption
},
{
attributeName: "debug",
attributeType: "label",
icon: "bx bx-bug",
label: t("search_definition.debug"),
tooltip: t("search_definition.debug_description"),
component: DebugOption
}
];
function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: {
note: FNote;
title: string,
titleIcon?: string,
children?: ComponentChildren,
help?: ComponentChildren,
attributeName: string,
attributeType: AttributeType,
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
note: FNote;
title: string,
titleIcon?: string,
children?: ComponentChildren,
help?: ComponentChildren,
attributeName: string,
attributeType: AttributeType,
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
}) {
return (
<tr className={attributeName}>
<td className="title-column">
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
{title}
</td>
<td>{children}</td>
<HelpRemoveButtons
help={help}
removeText={t("abstract_search_option.remove_this_search_option")}
onRemove={() => {
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
if (additionalAttributesToDelete) {
for (const { type, name } of additionalAttributesToDelete) {
removeOwnedAttributesByNameOrType(note, type, name);
}
}
}}
/>
</tr>
);
return (
<tr className={attributeName}>
<td className="title-column">
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
{title}
</td>
<td>{children}</td>
<HelpRemoveButtons
help={help}
removeText={t("abstract_search_option.remove_this_search_option")}
onRemove={() => {
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
if (additionalAttributesToDelete) {
for (const { type, name } of additionalAttributesToDelete) {
removeOwnedAttributesByNameOrType(note, type, name);
}
}
}}
/>
</tr>
)
}
function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) {
const [ searchString, setSearchString ] = useNoteLabel(note, "searchString");
const inputRef = useRef<HTMLTextAreaElement>(null);
const currentValue = useRef(searchString ?? "");
const spacedUpdate = useSpacedUpdate(async () => {
const searchString = currentValue.current;
appContext.lastSearchString = searchString;
setSearchString(searchString);
const [ searchString, setSearchString ] = useNoteLabel(note, "searchString");
const inputRef = useRef<HTMLTextAreaElement>(null);
const currentValue = useRef(searchString ?? "");
const spacedUpdate = useSpacedUpdate(async () => {
const searchString = currentValue.current;
appContext.lastSearchString = searchString;
setSearchString(searchString);
if (note.title.startsWith(t("search_string.search_prefix"))) {
await server.put(`notes/${note.noteId}/title`, {
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}`}`
});
if (note.title.startsWith(t("search_string.search_prefix"))) {
await server.put(`notes/${note.noteId}/title`, {
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}`}`
});
}
}, 1000);
// React to errors
const { showTooltip, hideTooltip } = useTooltip(inputRef, {
trigger: "manual",
title: `${t("search_string.error", { error: error?.message })}`,
html: true,
placement: "bottom"
});
// Auto-focus.
useEffect(() => inputRef.current?.focus(), []);
useEffect(() => {
if (error) {
showTooltip();
setTimeout(() => hideTooltip(), 4000);
} else {
hideTooltip();
}
}, [ error ]);
return <SearchOption
title={t("search_string.title_column")}
help={<>
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
<ul style="marigin-bottom: 0;">
<li>{t("search_string.full_text_search")}</li>
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
<li><code>#year &lt;= 2000</code> - {t("search_string.label_year_comparison")}</li>
<li><code>note.dateCreated &gt;= MONTH-1</code> - {t("search_string.label_date_created")}</li>
</ul>
</>}
note={note} {...restProps}
>
<FormTextArea
inputRef={inputRef}
className="search-string"
placeholder={t("search_string.placeholder")}
currentValue={searchString ?? ""}
onChange={text => {
currentValue.current = text;
spacedUpdate.scheduleUpdate();
}}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
// this also in effect disallows new lines in query string.
// on one hand, this makes sense since search string is a label
// on the other hand, it could be nice for structuring long search string. It's probably a niche case though.
await spacedUpdate.updateNowIfNecessary();
refreshResults();
}
}, 1000);
// React to errors
const { showTooltip, hideTooltip } = useTooltip(inputRef, {
trigger: "manual",
title: `${t("search_string.error", { error: error?.message })}`,
html: true,
placement: "bottom"
});
// Auto-focus.
useEffect(() => inputRef.current?.focus(), []);
useEffect(() => {
if (error) {
showTooltip();
setTimeout(() => hideTooltip(), 4000);
} else {
hideTooltip();
}
}, [ error ]);
return <SearchOption
title={t("search_string.title_column")}
help={<>
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
<ul style="marigin-bottom: 0;">
<li>{t("search_string.full_text_search")}</li>
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
<li><code>#year &lt;= 2000</code> - {t("search_string.label_year_comparison")}</li>
<li><code>note.dateCreated &gt;= MONTH-1</code> - {t("search_string.label_date_created")}</li>
</ul>
</>}
note={note} {...restProps}
>
<FormTextArea
inputRef={inputRef}
className="search-string"
placeholder={t("search_string.placeholder")}
currentValue={searchString ?? ""}
onChange={text => {
currentValue.current = text;
spacedUpdate.scheduleUpdate();
}}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
// this also in effect disallows new lines in query string.
// on one hand, this makes sense since search string is a label
// on the other hand, it could be nice for structuring long search string. It's probably a niche case though.
await spacedUpdate.updateNowIfNecessary();
refreshResults();
}
}}
/>
</SearchOption>;
}}
/>
</SearchOption>
}
function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript");
const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript");
return <SearchOption
title={t("search_script.title")}
help={<>
<p>{t("search_script.description1")}</p>
<p>{t("search_script.description2")}</p>
<p>{t("search_script.example_title")}</p>
<pre>{t("search_script.example_code")}</pre>
{t("search_script.note")}
</>}
note={note} {...restProps}
>
<NoteAutocomplete
noteId={searchScript !== "root" ? searchScript ?? undefined : undefined}
noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
placeholder={t("search_script.placeholder")}
/>
</SearchOption>;
return <SearchOption
title={t("search_script.title")}
help={<>
<p>{t("search_script.description1")}</p>
<p>{t("search_script.description2")}</p>
<p>{t("search_script.example_title")}</p>
<pre>{t("search_script.example_code")}</pre>
{t("search_script.note")}
</>}
note={note} {...restProps}
>
<NoteAutocomplete
noteId={searchScript !== "root" ? searchScript ?? undefined : undefined}
noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
placeholder={t("search_script.placeholder")}
/>
</SearchOption>
}
function AncestorOption({ note, ...restProps}: SearchOptionProps) {
const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor");
const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth");
const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor");
const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth");
const options = useMemo(() => {
const options: { value: string | undefined; label: string }[] = [
{ value: "", label: t("ancestor.depth_doesnt_matter") },
{ value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` }
];
const options = useMemo(() => {
const options: { value: string | undefined; label: string }[] = [
{ value: "", label: t("ancestor.depth_doesnt_matter") },
{ value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` }
];
for (let i=2; i<=9; i++) options.push({ value: `eq${ i}`, label: t("ancestor.depth_eq", { count: i }) });
for (let i=0; i<=9; i++) options.push({ value: `gt${ i}`, label: t("ancestor.depth_gt", { count: i }) });
for (let i=2; i<=9; i++) options.push({ value: `lt${ i}`, label: t("ancestor.depth_lt", { count: i }) });
for (let i=2; i<=9; i++) options.push({ value: "eq" + i, label: t("ancestor.depth_eq", { count: i }) });
for (let i=0; i<=9; i++) options.push({ value: "gt" + i, label: t("ancestor.depth_gt", { count: i }) });
for (let i=2; i<=9; i++) options.push({ value: "lt" + i, label: t("ancestor.depth_lt", { count: i }) });
return options;
}, []);
return options;
}, []);
return <SearchOption
title={t("ancestor.label")}
note={note} {...restProps}
>
<div style={{display: "flex", alignItems: "center"}}>
<NoteAutocomplete
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
placeholder={t("ancestor.placeholder")}
/>
return <SearchOption
title={t("ancestor.label")}
note={note} {...restProps}
>
<div style={{display: "flex", alignItems: "center"}}>
<NoteAutocomplete
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
placeholder={t("ancestor.placeholder")}
/>
<div style="margin-inline-start: 10px; margin-inline-end: 10px">{t("ancestor.depth_label")}:</div>
<FormSelect
values={options}
keyProperty="value" titleProperty="label"
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
style={{ flexShrink: 3 }}
/>
</div>
</SearchOption>;
<div style="margin-inline-start: 10px; margin-inline-end: 10px">{t("ancestor.depth_label")}:</div>
<FormSelect
values={options}
keyProperty="value" titleProperty="label"
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
style={{ flexShrink: 3 }}
/>
</div>
</SearchOption>;
}
function FastSearchOption({ ...restProps }: SearchOptionProps) {
return <SearchOption
titleIcon="bx bx-run" title={t("fast_search.fast_search")}
help={t("fast_search.description")}
{...restProps}
/>;
return <SearchOption
titleIcon="bx bx-run" title={t("fast_search.fast_search")}
help={t("fast_search.description")}
{...restProps}
/>
}
function DebugOption({ ...restProps }: SearchOptionProps) {
return <SearchOption
titleIcon="bx bx-bug" title={t("debug.debug")}
help={<>
<p>{t("debug.debug_info")}</p>
{t("debug.access_info")}
</>}
{...restProps}
/>;
return <SearchOption
titleIcon="bx bx-bug" title={t("debug.debug")}
help={<>
<p>{t("debug.debug_info")}</p>
{t("debug.access_info")}
</>}
{...restProps}
/>
}
function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) {
return <SearchOption
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
{...restProps}
/>;
return <SearchOption
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
{...restProps}
/>
}
function OrderByOption({ note, ...restProps }: SearchOptionProps) {
const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy");
const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection");
const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy");
const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection");
return <SearchOption
titleIcon="bx bx-arrow-from-top"
title={t("order_by.order_by")}
note={note} {...restProps}
>
<FormSelect
className="w-auto d-inline"
currentValue={orderBy ?? "relevancy"} onChange={setOrderBy}
keyProperty="value" titleProperty="title"
values={[
{ value: "relevancy", title: t("order_by.relevancy") },
{ value: "title", title: t("order_by.title") },
{ value: "dateCreated", title: t("order_by.date_created") },
{ value: "dateModified", title: t("order_by.date_modified") },
{ value: "contentSize", title: t("order_by.content_size") },
{ value: "contentAndAttachmentsSize", title: t("order_by.content_and_attachments_size") },
{ value: "contentAndAttachmentsAndRevisionsSize", title: t("order_by.content_and_attachments_and_revisions_size") },
{ value: "revisionCount", title: t("order_by.revision_count") },
{ value: "childrenCount", title: t("order_by.children_count") },
{ value: "parentCount", title: t("order_by.parent_count") },
{ value: "ownedLabelCount", title: t("order_by.owned_label_count") },
{ value: "ownedRelationCount", title: t("order_by.owned_relation_count") },
{ value: "targetRelationCount", title: t("order_by.target_relation_count") },
{ value: "random", title: t("order_by.random") }
]}
/>
{" "}
<FormSelect
className="w-auto d-inline"
currentValue={orderDirection ?? "asc"} onChange={setOrderDirection}
keyProperty="value" titleProperty="title"
values={[
{ value: "asc", title: t("order_by.asc") },
{ value: "desc", title: t("order_by.desc") }
]}
/>
</SearchOption>;
return <SearchOption
titleIcon="bx bx-arrow-from-top"
title={t("order_by.order_by")}
note={note} {...restProps}
>
<FormSelect
className="w-auto d-inline"
currentValue={orderBy ?? "relevancy"} onChange={setOrderBy}
keyProperty="value" titleProperty="title"
values={[
{ value: "relevancy", title: t("order_by.relevancy") },
{ value: "title", title: t("order_by.title") },
{ value: "dateCreated", title: t("order_by.date_created") },
{ value: "dateModified", title: t("order_by.date_modified") },
{ value: "contentSize", title: t("order_by.content_size") },
{ value: "contentAndAttachmentsSize", title: t("order_by.content_and_attachments_size") },
{ value: "contentAndAttachmentsAndRevisionsSize", title: t("order_by.content_and_attachments_and_revisions_size") },
{ value: "revisionCount", title: t("order_by.revision_count") },
{ value: "childrenCount", title: t("order_by.children_count") },
{ value: "parentCount", title: t("order_by.parent_count") },
{ value: "ownedLabelCount", title: t("order_by.owned_label_count") },
{ value: "ownedRelationCount", title: t("order_by.owned_relation_count") },
{ value: "targetRelationCount", title: t("order_by.target_relation_count") },
{ value: "random", title: t("order_by.random") }
]}
/>
{" "}
<FormSelect
className="w-auto d-inline"
currentValue={orderDirection ?? "asc"} onChange={setOrderDirection}
keyProperty="value" titleProperty="title"
values={[
{ value: "asc", title: t("order_by.asc") },
{ value: "desc", title: t("order_by.desc") }
]}
/>
</SearchOption>
}
function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) {
const [ limit, setLimit ] = useNoteLabel(note, "limit");
const [ limit, setLimit ] = useNoteLabel(note, "limit");
return <SearchOption
titleIcon="bx bx-stop"
title={t("limit.limit")}
help={t("limit.take_first_x_results")}
note={note} {...restProps}
>
<FormTextBox
type="number" min="1" step="1"
currentValue={limit ?? defaultValue} onChange={setLimit}
/>
</SearchOption>;
return <SearchOption
titleIcon="bx bx-stop"
title={t("limit.limit")}
help={t("limit.take_first_x_results")}
note={note} {...restProps}
>
<FormTextBox
type="number" min="1" step="1"
currentValue={limit ?? defaultValue} onChange={setLimit}
/>
</SearchOption>
}

View File

@@ -1,218 +1,207 @@
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { TabContext } from "./ribbon-interface";
import { SaveSearchNoteResponse } from "@triliumnext/commons";
import attributes from "../../services/attributes";
import FNote from "../../entities/fnote";
import toast from "../../services/toast";
import froca from "../../services/froca";
import { useContext, useEffect, useState } from "preact/hooks";
import { ParentComponent } from "../react/react_utils";
import { useTriliumEvent } from "../react/hooks";
import appContext from "../../components/app_context";
import server from "../../services/server";
import ws from "../../services/ws";
import tree from "../../services/tree";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
import Dropdown from "../react/Dropdown";
import Icon from "../react/Icon";
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
import { FormListHeader, FormListItem } from "../react/FormList";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import { getErrorMessage } from "../../services/utils";
import "./SearchDefinitionTab.css";
import { SaveSearchNoteResponse } from "@triliumnext/commons";
import { useContext, useEffect, useState } from "preact/hooks";
export default function SearchDefinitionTab({ note, ntxId, hidden }: TabContext) {
const parentComponent = useContext(ParentComponent);
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
const [ error, setError ] = useState<{ message: string }>();
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
import tree from "../../services/tree";
import { getErrorMessage } from "../../services/utils";
import ws from "../../services/ws";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import CollectionProperties from "../note_bars/CollectionProperties";
import Button from "../react/Button";
import Dropdown from "../react/Dropdown";
import { FormListHeader, FormListItem } from "../react/FormList";
import { useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
function refreshOptions() {
if (!note) return;
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
const availableOptions: SearchOption[] = [];
const activeOptions: SearchOption[] = [];
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
const parentComponent = useContext(ParentComponent);
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
const [ error, setError ] = useState<{ message: string }>();
function refreshOptions() {
if (!note) return;
const availableOptions: SearchOption[] = [];
const activeOptions: SearchOption[] = [];
for (const searchOption of SEARCH_OPTIONS) {
const attr = note.getAttribute(searchOption.attributeType, searchOption.attributeName);
if (attr) {
activeOptions.push(searchOption);
} else {
availableOptions.push(searchOption);
}
}
setSearchOptions({ availableOptions, activeOptions });
for (const searchOption of SEARCH_OPTIONS) {
const attr = note.getAttribute(searchOption.attributeType, searchOption.attributeName);
if (attr) {
activeOptions.push(searchOption);
} else {
availableOptions.push(searchOption);
}
}
async function refreshResults() {
const noteId = note?.noteId;
if (!noteId) {
return;
}
setSearchOptions({ availableOptions, activeOptions });
}
try {
const result = await froca.loadSearchNote(noteId);
if (result?.error) {
setError({ message: result?.error});
} else {
setError(undefined);
}
} catch (e: unknown) {
toast.showError(getErrorMessage(e));
}
parentComponent?.triggerEvent("searchRefreshed", { ntxId });
async function refreshResults() {
const noteId = note?.noteId;
if (!noteId) {
return;
}
// Refresh the list of available and active options.
useEffect(refreshOptions, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
refreshOptions();
try {
const result = await froca.loadSearchNote(noteId);
if (result?.error) {
setError({ message: result?.error})
} else {
setError(undefined);
}
});
} catch (e: unknown) {
toast.showError(getErrorMessage(e));
}
return (
<div className="search-definition-widget">
<div className="search-settings">
{note && !hidden && (
<table className="search-setting-table">
<tbody>
<tr>
<td className="title-column">{t("search_definition.add_search_option")}</td>
<td colSpan={2} className="add-search-option">
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
size="small"
icon={icon}
text={label}
title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
parentComponent?.triggerEvent("searchRefreshed", { ntxId });
}
<AddBulkActionButton note={note} />
</td>
</tr>
</tbody>
<tbody className="search-options">
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
const Component = component;
return <Component
attributeName={attributeName}
attributeType={attributeType}
note={note}
refreshResults={refreshResults}
error={error}
additionalAttributesToDelete={additionalAttributesToDelete}
defaultValue={defaultValue}
/>;
})}
// Refresh the list of available and active options.
useEffect(refreshOptions, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
refreshOptions();
}
});
{isNewLayout && <tr className="view-options">
<td className="title-column">{t("search_definition.view_options")}</td>
<td><CollectionProperties note={note} /></td>
</tr>}
</tbody>
<BulkActionsList note={note} />
<tbody className="search-actions">
<tr>
<td colSpan={3}>
<div className="search-actions-container">
<Button
icon="bx bx-search"
text={t("search_definition.search_button")}
keyboardShortcut="Enter"
onClick={refreshResults}
/>
return (
<div className="search-definition-widget">
<div className="search-settings">
{note && !hidden &&
<table className="search-setting-table">
<tbody>
<tr>
<td className="title-column">{t("search_definition.add_search_option")}</td>
<td colSpan={2} className="add-search-option">
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
size="small"
icon={icon}
text={label}
title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
<Button
icon="bx bxs-zap"
text={t("search_definition.search_execute")}
onClick={async () => {
await server.post(`search-and-execute-note/${note.noteId}`);
refreshResults();
toast.showMessage(t("search_definition.actions_executed"), 3000);
}}
/>
<AddBulkActionButton note={note} />
</td>
</tr>
</tbody>
<tbody className="search-options">
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
const Component = component;
return <Component
attributeName={attributeName}
attributeType={attributeType}
note={note}
refreshResults={refreshResults}
error={error}
additionalAttributesToDelete={additionalAttributesToDelete}
defaultValue={defaultValue}
/>;
})}
</tbody>
<BulkActionsList note={note} />
<tbody className="search-actions">
<tr>
<td colSpan={3}>
<div className="search-actions-container">
<Button
icon="bx bx-search"
text={t("search_definition.search_button")}
keyboardShortcut="Enter"
onClick={refreshResults}
/>
{note.isHiddenCompletely() && <Button
icon="bx bx-save"
text={t("search_definition.save_to_note")}
onClick={async () => {
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
if (!notePath) {
return;
}
<Button
icon="bx bxs-zap"
text={t("search_definition.search_execute")}
onClick={async () => {
await server.post(`search-and-execute-note/${note.noteId}`);
refreshResults();
toast.showMessage(t("search_definition.actions_executed"), 3000);
}}
/>
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
{note.isHiddenCompletely() && <Button
icon="bx bx-save"
text={t("search_definition.save_to_note")}
onClick={async () => {
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
if (!notePath) {
return;
}
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
}}
/>}
</div>
</td>
</tr>
</tbody>
</table>
)}
</div>
</div>
);
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
}}
/>}
</div>
</td>
</tr>
</tbody>
</table>
}
</div>
</div>
)
}
function BulkActionsList({ note }: { note: FNote }) {
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
function refreshBulkActions() {
if (note) {
setBulkActions(bulk_action.parseActions(note));
}
function refreshBulkActions() {
if (note) {
setBulkActions(bulk_action.parseActions(note));
}
}
// React to changes.
useEffect(refreshBulkActions, [ note ]);
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) {
refreshBulkActions();
}
});
// React to changes.
useEffect(refreshBulkActions, [ note ]);
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) {
refreshBulkActions();
}
});
return (
<tbody className="action-options">
{bulkActions?.map(bulkAction => (
bulkAction.doRender()
))}
</tbody>
);
return (
<tbody className="action-options">
{bulkActions?.map(bulkAction => (
bulkAction.doRender()
))}
</tbody>
)
}
function AddBulkActionButton({ note }: { note: FNote }) {
return (
<Dropdown
buttonClassName="action-add-toggle btn btn-sm"
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
noSelectButtonStyle
>
{ACTION_GROUPS.map(({ actions, title }) => (
<>
<FormListHeader text={title} />
return (
<Dropdown
buttonClassName="action-add-toggle btn btn-sm"
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
noSelectButtonStyle
>
{ACTION_GROUPS.map(({ actions, title }) => (
<>
<FormListHeader text={title} />
{actions.map(({ actionName, actionTitle }) => (
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</>
{actions.map(({ actionName, actionTitle }) => (
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</Dropdown>
);
</>
))}
</Dropdown>
)
}

View File

@@ -1,13 +1,12 @@
import { SimilarNoteResponse } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
import NoteLink from "../react/NoteLink";
import { TabContext } from "./ribbon-interface";
import { SimilarNoteResponse } from "@triliumnext/commons";
import server from "../../services/server";
import { t } from "../../services/i18n";
import froca from "../../services/froca";
import NoteLink from "../react/NoteLink";
export default function SimilarNotesTab({ note }: Pick<TabContext, "note">) {
export default function SimilarNotesTab({ note }: TabContext) {
const [ similarNotes, setSimilarNotes ] = useState<SimilarNoteResponse>();
useEffect(() => {
@@ -18,7 +17,7 @@ export default function SimilarNotesTab({ note }: Pick<TabContext, "note">) {
await froca.getNotes(noteIds, true); // preload all at once
}
setSimilarNotes(similarNotes);
});
});
}
}, [ note?.noteId ]);
@@ -43,5 +42,5 @@ export default function SimilarNotesTab({ note }: Pick<TabContext, "note">) {
)}
</div>
</div>
);
}
)
}

View File

@@ -18,8 +18,7 @@ interface BookConfig {
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
bindToLabel: FilterLabelsByType<boolean>
}
export interface ButtonProperty {
@@ -41,11 +40,10 @@ export interface NumberProperty {
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
export interface ComboBoxItem {
interface ComboBoxItem {
value: string;
label: string;
}
@@ -58,7 +56,6 @@ interface ComboBoxGroup {
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
@@ -110,13 +107,11 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
properties: [
{
label: t("book_properties_config.hide-weekends"),
icon: "bx bx-calendar-week",
type: "checkbox",
bindToLabel: "calendar:hideWeekends"
},
{
label: t("book_properties_config.display-week-numbers"),
icon: "bx bx-hash",
type: "checkbox",
bindToLabel: "calendar:weekNumbers"
}
@@ -126,7 +121,6 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
properties: [
{
label: t("book_properties_config.map-style"),
icon: "bx bx-palette",
type: "combobox",
bindToLabel: "map:style",
defaultValue: DEFAULT_MAP_LAYER_NAME,
@@ -153,7 +147,6 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
},
{
label: t("book_properties_config.show-scale"),
icon: "bx bx-ruler",
type: "checkbox",
bindToLabel: "map:scale"
}
@@ -163,7 +156,6 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
properties: [
{
label: t("book_properties_config.max-nesting-depth"),
icon: "bx bx-subdirectory-right",
type: "number",
bindToLabel: "maxNestingDepth",
width: 65,
@@ -179,7 +171,6 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
{
label: "Theme",
type: "combobox",
icon: "bx bx-palette",
bindToLabel: "presentation:theme",
defaultValue: DEFAULT_THEME,
options: getPresentationThemes().map(theme => ({

Some files were not shown because too many files have changed in this diff Show More