Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms

This commit is contained in:
Adorian Doran
2025-01-29 17:26:24 +02:00
73 changed files with 952 additions and 696 deletions

View File

@@ -36,6 +36,12 @@ interface DateLimits {
maxDate: string;
}
interface SimilarNote {
score: number;
notePath: string[];
noteId: string;
}
function filterUrlValue(value: string) {
return value
.replace(/https?:\/\//gi, "")
@@ -247,7 +253,7 @@ function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) {
return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
}
async function findSimilarNotes(noteId: string) {
async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefined> {
const results = [];
let i = 0;
@@ -417,6 +423,7 @@ async function findSimilarNotes(noteId: string) {
// this takes care of note hoisting
if (!notePath) {
// TODO: This return is suspicious, it should probably be continue
return;
}

View File

@@ -71,7 +71,7 @@ export interface ExecuteCommandData extends CommandData {
export type CommandMappings = {
"api-log-messages": CommandData;
focusTree: CommandData,
focusOnDetail: Required<CommandData>;
focusOnDetail: CommandData;
focusOnSearchDefinition: Required<CommandData>;
searchNotes: CommandData & {
searchString?: string;
@@ -104,6 +104,8 @@ export type CommandMappings = {
openNoteInNewTab: CommandData;
openNoteInNewSplit: CommandData;
openNoteInNewWindow: CommandData;
hideLeftPane: CommandData;
showLeftPane: CommandData;
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
@@ -236,6 +238,9 @@ type EventMappings = {
beforeNoteSwitch: {
noteContext: NoteContext;
};
beforeNoteContextRemove: {
ntxIds: string[];
};
noteSwitched: {
noteContext: NoteContext;
notePath: string | null;
@@ -286,6 +291,9 @@ type EventMappings = {
tabReorder: {
ntxIdsInOrder: string[]
};
refreshNoteList: {
noteId: string;
}
};
export type EventListener<T extends EventNames> = {

View File

@@ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this;
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
try {
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);

View File

@@ -9,6 +9,8 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
import options from "./services/options.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
await appContext.earlyInit();
@@ -44,10 +46,9 @@ if (utils.isElectron()) {
}
function initOnElectron() {
const electron = utils.dynamicRequire("electron");
const electron: typeof Electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
const electronRemote = utils.dynamicRequire("@electron/remote");
const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote");
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
@@ -58,7 +59,7 @@ function initOnElectron() {
}
}
function initTitleBarButtons(style, currentWindow) {
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
if (window.glob.platform === "win32") {
const applyWindowsOverlay = () => {
const color = style.getPropertyValue("--native-titlebar-background");
@@ -81,9 +82,14 @@ function initTitleBarButtons(style, currentWindow) {
}
}
function initTransparencyEffects(style, currentWindow) {
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
if (window.glob.platform === "win32") {
const material = style.getPropertyValue("--background-material");
currentWindow.setBackgroundMaterial(material);
// TriliumNextTODO: find a nicer way to make TypeScript happy unfortunately TS did not like Array.includes here
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
if (foundBgMaterialOption) {
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
}
}
}

View File

@@ -36,12 +36,12 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
interface NotePathRecord {
export interface NotePathRecord {
isArchived: boolean;
isInHoistedSubTree: boolean;
isSearch: boolean;
isSearch?: boolean;
notePath: string[];
isHidden: boolean;
}
@@ -402,14 +402,14 @@ class FNote {
return notePaths;
}
getSortedNotePathRecords(hoistedNoteId = "root") {
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
const isHoistedRoot = hoistedNoteId === "root";
const notePaths = this.getAllNotePaths().map((path) => ({
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden")
}));

View File

@@ -8,6 +8,7 @@ interface NoteRow {
}
interface BranchRow {
noteId?: string;
branchId: string;
componentId: string;
parentNoteId?: string;
@@ -157,7 +158,7 @@ export default class LoadResults {
return Object.keys(this.noteIdToComponentId);
}
isNoteReloaded(noteId: string, componentId = null) {
isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
if (!noteId) {
return false;
}

View File

@@ -124,6 +124,10 @@ function escapeHtml(str: string) {
return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
}
export function escapeQuotes(value: string) {
return value.replaceAll("\"", "&quot;");
}
function formatSize(size: number) {
size = Math.max(Math.round(size / 1024), 1);

View File

@@ -3,7 +3,7 @@
*
* @param noteId of the given note to be fetched. If false, fetches current note.
*/
async function fetchNote(noteId = null) {
async function fetchNote(noteId: string | null = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
@@ -25,3 +25,9 @@ document.addEventListener(
},
false
);
// workaround to prevent webpack from removing "fetchNote" as dead code:
// add fetchNote as property to the window object
Object.defineProperty(window, "fetchNote", {
value: fetchNote
});

View File

@@ -43,6 +43,7 @@ interface CustomGlobals {
appCssNoteIds: string[];
triliumVersion: string;
TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform;
}
type RequireMethod = (moduleName: string) => any;

View File

@@ -14,6 +14,7 @@ import type AttributeDetailWidget from "./attribute_detail.js";
import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
import type FNote from "../../entities/fnote.js";
import { escapeQuotes } from "../../services/utils.js";
const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body1")}</p>
@@ -76,8 +77,8 @@ const TPL = `
<div class="attribute-list-editor" tabindex="200"></div>
<div class="bx bx-save save-attributes-button" title="${t("attribute_editor.save_attributes")}"></div>
<div class="bx bx-plus add-new-attribute-button" title="${t("attribute_editor.add_a_new_attribute")}"></div>
<div class="bx bx-save save-attributes-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
<div class="bx bx-plus add-new-attribute-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
<div class="attribute-errors" style="display: none;"></div>
</div>

View File

@@ -193,7 +193,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
* Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
* @returns whether the widget is enabled.
*/
isEnabled() {
isEnabled(): boolean | null | undefined {
return true;
}
@@ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
*/
doRender() {}
toggleInt(show: boolean) {
toggleInt(show: boolean | null | undefined) {
this.$widget.toggleClass("hidden-int", !show);
}

View File

@@ -2,9 +2,11 @@ import options from "../../services/options.js";
import splitService from "../../services/resizer.js";
import CommandButtonWidget from "./command_button.js";
import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor(isHorizontalLayout) {
constructor(isHorizontalLayout: boolean) {
super();
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
@@ -32,7 +34,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("leftPaneVisible")) {
this.refreshIcon();
}

View File

@@ -1,6 +1,10 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import keyboardActionsService from "../../services/keyboard_actions.js";
import attributeService from "../../services/attributes.js";
import type CommandButtonWidget from "../buttons/command_button.js";
import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js";
import type { EventData, EventNames } from "../../components/app_context.js";
const TPL = `
<div class="ribbon-container">
@@ -8,11 +12,11 @@ const TPL = `
.ribbon-container {
margin-bottom: 5px;
}
.ribbon-top-row {
display: flex;
}
.ribbon-tab-container {
display: flex;
flex-direction: row;
@@ -21,10 +25,10 @@ const TPL = `
flex-grow: 1;
flex-flow: row wrap;
}
.ribbon-tab-title {
color: var(--muted-text-color);
border-bottom: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
min-width: 24px;
flex-basis: 24px;
max-width: max-content;
@@ -36,7 +40,7 @@ const TPL = `
position: relative;
top: 3px;
}
.ribbon-tab-title.active {
color: var(--main-text-color);
border-bottom: 3px solid var(--main-text-color);
@@ -44,7 +48,7 @@ const TPL = `
overflow: hidden;
text-overflow: ellipsis;
}
.ribbon-tab-title:hover {
cursor: pointer;
}
@@ -52,11 +56,11 @@ const TPL = `
.ribbon-tab-title:hover {
color: var(--main-text-color);
}
.ribbon-tab-title:first-of-type {
padding-left: 10px;
}
.ribbon-tab-spacer {
flex-basis: 0;
min-width: 0;
@@ -64,41 +68,41 @@ const TPL = `
flex-grow: 1;
border-bottom: 1px solid var(--main-border-color);
}
.ribbon-tab-spacer:last-of-type {
flex-grow: 1;
flex-basis: 0;
min-width: 0;
max-width: 10000px;
}
.ribbon-button-container {
display: flex;
border-bottom: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
margin-right: 5px;
}
.ribbon-button-container > * {
position: relative;
top: -3px;
margin-left: 10px;
}
.ribbon-body {
display: none;
border-bottom: 1px solid var(--main-border-color);
margin-left: 10px;
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
}
.ribbon-body.active {
display: block;
}
.ribbon-tab-title-label {
display: none;
}
.ribbon-tab-title.active .ribbon-tab-title-label {
display: inline;
}
@@ -108,11 +112,21 @@ const TPL = `
<div class="ribbon-tab-container"></div>
<div class="ribbon-button-container"></div>
</div>
<div class="ribbon-body-container"></div>
</div>`;
export default class RibbonContainer extends NoteContextAwareWidget {
private lastActiveComponentId?: string | null;
private lastNoteType?: NoteType;
private ribbonWidgets: NoteContextAwareWidget[];
private buttonWidgets: CommandButtonWidget[];
private $tabContainer!: JQuery<HTMLElement>;
private $buttonContainer!: JQuery<HTMLElement>;
private $bodyContainer!: JQuery<HTMLElement>;
constructor() {
super();
@@ -122,10 +136,10 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
isEnabled() {
return super.isEnabled() && this.noteContext.viewScope.viewMode === "default";
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
}
ribbon(widget) {
ribbon(widget: NoteContextAwareWidget) { // TODO: Base class
super.child(widget);
this.ribbonWidgets.push(widget);
@@ -133,7 +147,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
return this;
}
button(widget) {
button(widget: CommandButtonWidget) {
super.child(widget);
this.buttonWidgets.push(widget);
@@ -163,7 +177,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
});
}
toggleRibbonTab($ribbonTitle, refreshActiveTab = true) {
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
const activate = !$ribbonTitle.hasClass("active");
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
@@ -181,14 +195,15 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const activeChild = this.getActiveRibbonWidget();
if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
if (refreshActiveTab) {
if (handleEventPromise) {
handleEventPromise.then(() => activeChild.focus?.());
handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
} else {
activeChild.focus?.();
// TODO: Base class
(activeChild as any)?.focus();
}
}
}
@@ -203,7 +218,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
await super.noteSwitched();
}
async refreshWithNote(note, noExplicitActivation = false) {
async refreshWithNote(note: FNote, noExplicitActivation = false) {
this.lastNoteType = note.type;
let $ribbonTabToActivate, $lastActiveRibbon;
@@ -211,7 +226,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
this.$tabContainer.empty();
for (const ribbonWidget of this.ribbonWidgets) {
const ret = await ribbonWidget.getTitle(note);
// TODO: Base class for ribbon widget
const ret = await (ribbonWidget as any).getTitle(note);
if (!ret.show) {
continue;
@@ -219,8 +235,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const $ribbonTitle = $('<div class="ribbon-tab-title">')
.attr("data-ribbon-component-id", ribbonWidget.componentId)
.attr("data-ribbon-component-name", ribbonWidget.name)
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", ribbonWidget.toggleCommand))
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", (ribbonWidget as any).toggleCommand)) // TODO: base class
.append(" ")
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
@@ -238,7 +254,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
keyboardActionsService.getActions().then((actions) => {
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
title: function () {
title: () => {
const toggleCommandName = $(this).attr("data-toggle-command");
const action = actions.find((act) => act.actionName === toggleCommandName);
const title = $(this).attr("data-title");
@@ -246,7 +262,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
if (action && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title;
return title ?? "";
}
}
});
@@ -263,27 +279,27 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
isRibbonTabActive(name) {
isRibbonTabActive(name: string) {
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
return $ribbonComponent.hasClass("active");
}
ensureOwnedAttributesAreOpen(ntxId) {
if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
}
}
addNewLabelEvent({ ntxId }) {
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
addNewRelationEvent({ ntxId }) {
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
toggleRibbonTabWithName(name, ntxId) {
toggleRibbonTabWithName(name: string, ntxId?: string) {
if (!this.isNoteContext(ntxId)) {
return false;
}
@@ -295,23 +311,23 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
handleEvent(name, data) {
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
const PREFIX = "toggleRibbonTab";
if (name.startsWith(PREFIX)) {
let componentName = name.substr(PREFIX.length);
componentName = componentName[0].toLowerCase() + componentName.substr(1);
this.toggleRibbonTabWithName(componentName, data.ntxId);
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
} else {
return super.handleEvent(name, data);
}
}
async handleEventInChildren(name, data) {
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
if (["activeContextChanged", "setNoteContext"].includes(name)) {
// won't trigger .refresh();
await super.handleEventInChildren("setNoteContext", data);
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
} else if (this.isEnabled() || name === "initialRenderComplete") {
const activeRibbonWidget = this.getActiveRibbonWidget();
@@ -326,8 +342,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (!this.note) {
return;
}
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
// note type influences the list of available ribbon tabs the most
// check for the type is so that we don't update on each title rename
this.lastNoteType = this.note.type;
@@ -338,7 +358,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
noteTypeMimeChangedEvent() {
async noteTypeMimeChangedEvent() {
// We are ignoring the event which triggers a refresh since it is usually already done by a different
// event and causing a race condition in which the items appear twice.
}

View File

@@ -1,4 +1,4 @@
import utils from "../../services/utils.js";
import utils, { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js";
import importService from "../../services/import.js";
import options from "../../services/options.js";
@@ -27,21 +27,21 @@ const TPL = `
<strong>${t("import.options")}:</strong>
<div class="checkbox">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.safeImportTooltip")}">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.safeImportTooltip"))}">
<input class="safe-import-checkbox" value="1" type="checkbox" checked>
<span>${t("import.safeImport")}</span>
</label>
</div>
<div class="checkbox">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.explodeArchivesTooltip")}">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.explodeArchivesTooltip"))}">
<input class="explode-archives-checkbox" value="1" type="checkbox" checked>
<span>${t("import.explodeArchives")}</span>
</label>
</div>
<div class="checkbox">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.shrinkImagesTooltip")}">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.shrinkImagesTooltip"))}">
<input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t("import.shrinkImages")}</span>
</label>
</div>

View File

@@ -1,5 +1,5 @@
import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import utils, { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js";
import importService from "../../services/import.js";
import options from "../../services/options.js";
@@ -24,7 +24,7 @@ const TPL = `
<div class="form-group">
<strong>${t("upload_attachments.options")}:</strong>
<div class="checkbox">
<label data-bs-toggle="tooltip" title="${t("upload_attachments.tooltip")}">
<label data-bs-toggle="tooltip" title="${escapeQuotes(t("upload_attachments.tooltip"))}">
<input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span>
</label>
</div>

View File

@@ -1,6 +1,10 @@
import attributeService from "../services/attributes.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
const TPL = `
<div class="dropdown editability-select-widget">
@@ -9,13 +13,17 @@ const TPL = `
width: 300px;
}
.editability-dropdown .dropdown-item {
display: block !important;
}
.editability-dropdown .dropdown-item div {
font-size: small;
color: var(--muted-text-color);
white-space: normal;
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button editability-button">
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle editability-button">
<span class="editability-active-desc">${t("editability_select.auto")}</span>
<span class="caret"></span>
</button>
@@ -40,9 +48,15 @@ const TPL = `
`;
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
private dropdown!: bootstrap.Dropdown;
private $editabilityActiveDesc!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
// TODO: Remove once bootstrap is added to webpack.
//@ts-ignore
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
@@ -52,24 +66,28 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
const editability = $(e.target).closest("[data-editability]").attr("data-editability");
if (!this.note || !this.noteId) {
return;
}
for (const ownedAttr of this.note.getOwnedLabels()) {
if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
}
}
if (editability !== "auto") {
if (editability && editability !== "auto") {
await attributeService.addLabel(this.noteId, editability);
}
});
}
async refreshWithNote(note) {
let editability = "auto";
async refreshWithNote(note: FNote) {
let editability: Editability = "auto";
if (this.note.isLabelTruthy("readOnly")) {
if (this.note?.isLabelTruthy("readOnly")) {
editability = "readOnly";
} else if (this.note.isLabelTruthy("autoReadOnlyDisabled")) {
} else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
editability = "autoReadOnlyDisabled";
}
@@ -85,7 +103,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
this.$editabilityActiveDesc.text(labels[editability]);
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
this.refresh();
}

View File

@@ -7,6 +7,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
import linkService from "../../services/link.js";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="backlinks-widget">
@@ -14,7 +15,7 @@ const TPL = `
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
border-radius: 10px;
border-color: var(--main-border-color);
@@ -25,11 +26,11 @@ const TPL = `
justify-content: space-between;
align-items: center;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-items {
z-index: 10;
position: absolute;
@@ -42,29 +43,41 @@ const TPL = `
padding: 20px;
overflow-y: auto;
}
.backlink-excerpt {
border-left: 2px solid var(--main-border-color);
padding-left: 10px;
opacity: 80%;
font-size: 90%;
}
.backlink-excerpt .backlink-link { /* the actual backlink */
font-weight: bold;
background-color: yellow;
}
</style>
<div class="backlinks-ticker">
<span class="backlinks-count"></span>
</div>
</div>
<div class="backlinks-items" style="display: none;"></div>
</div>
`;
// TODO: Deduplicate with server
interface Backlink {
noteId: string;
relationName?: string;
excerpts?: string[];
}
export default class BacklinksWidget extends NoteContextAwareWidget {
private $count!: JQuery<HTMLElement>;
private $items!: JQuery<HTMLElement>;
private $ticker!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$count = this.$widget.find(".backlinks-count");
@@ -73,7 +86,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.$count.on("click", () => {
this.$items.toggle();
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
if (this.$items.is(":visible")) {
this.renderBacklinks();
@@ -83,7 +96,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.contentSized();
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.clearItems();
if (this.noteContext?.viewScope?.viewMode !== "default") {
@@ -92,7 +105,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
}
// can't use froca since that would count only relations from loaded notes
const resp = await server.get(`note-map/${this.noteId}/backlink-count`);
// TODO: Deduplicate response type
const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
this.toggle(false);
@@ -106,7 +120,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
);
}
toggle(show) {
toggle(show: boolean) {
this.$widget.toggleClass("hidden-no-content", !show);
}
@@ -121,7 +135,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.$items.empty();
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
if (!backlinks.length) {
return;
@@ -143,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
if (backlink.relationName) {
$item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
} else {
$item.append(...backlink.excerpts);
$item.append(...backlink.excerpts ?? []);
}
this.$items.append($item);

View File

@@ -40,7 +40,7 @@ export default class GeoMapWidget extends NoteContextAwareWidget {
const L = (await import("leaflet")).default;
const map = L.map(this.$container[0], {
worldCopyJump: true
});
this.map = map;

View File

@@ -10,9 +10,9 @@ import type NoteContext from "../components/note_context.js";
class NoteContextAwareWidget extends BasicWidget {
protected noteContext?: NoteContext;
isNoteContext(ntxId: string | null | undefined) {
isNoteContext(ntxId: string | string[] | null | undefined) {
if (Array.isArray(ntxId)) {
return this.noteContext && ntxId.includes(this.noteContext.ntxId);
return this.noteContext && this.noteContext.ntxId && ntxId.includes(this.noteContext.ntxId);
} else {
return this.noteContext && this.noteContext.ntxId === ntxId;
}
@@ -54,7 +54,7 @@ class NoteContextAwareWidget extends BasicWidget {
*
* @returns true when an active note exists
*/
isEnabled() {
isEnabled(): boolean | null | undefined {
return !!this.note;
}

View File

@@ -147,11 +147,14 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
*/
checkFullHeight() {
// https://github.com/zadam/trilium/issues/2522
this.$widget.toggleClass(
"full-height",
(!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
this.noteContext.viewScope.viewMode === "attachments"
);
const isBackendNote = this.noteContext?.noteId === "_backendLog";
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type);
const isFullHeight = (!this.noteContext.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|| this.noteContext.viewScope.viewMode === "attachments"
|| isBackendNote;
this.$widget.toggleClass("full-height", isFullHeight);
}
getTypeWidget() {

View File

@@ -1,5 +1,7 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import NoteListRenderer from "../services/note_list_renderer.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
const TPL = `
<div class="note-list-widget">
@@ -8,19 +10,25 @@ const TPL = `
min-height: 0;
overflow: auto;
}
.note-list-widget .note-list {
padding: 10px;
}
</style>
<div class="note-list-widget-content">
</div>
</div>`;
export default class NoteListWidget extends NoteContextAwareWidget {
private $content!: JQuery<HTMLElement>;
private isIntersecting?: boolean;
private noteIdRefreshed?: string;
private shownNoteId?: string | null;
isEnabled() {
return super.isEnabled() && this.noteContext.hasNoteList();
return super.isEnabled() && this.noteContext?.hasNoteList();
}
doRender() {
@@ -50,13 +58,13 @@ export default class NoteListWidget extends NoteContextAwareWidget {
// console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
// console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
if (this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) {
if (this.note && this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) {
this.shownNoteId = this.noteId;
this.renderNoteList(this.note);
}
}
async renderNoteList(note) {
async renderNoteList(note: FNote) {
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
await noteListRenderer.renderList();
}
@@ -67,8 +75,8 @@ export default class NoteListWidget extends NoteContextAwareWidget {
await super.refresh();
}
async refreshNoteListEvent({ noteId }) {
if (this.isNote(noteId)) {
async refreshNoteListEvent({ noteId }: EventData<"refreshNoteList">) {
if (this.isNote(noteId) && this.note) {
await this.renderNoteList(this.note);
}
}
@@ -78,7 +86,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
* If it's evaluated before note detail, then it's clearly intersected (visible) although after note detail load
* it is not intersected (visible) anymore.
*/
noteDetailRefreshedEvent({ ntxId }) {
noteDetailRefreshedEvent({ ntxId }: EventData<"noteDetailRefreshed">) {
if (!this.isNoteContext(ntxId)) {
return;
}
@@ -88,14 +96,14 @@ export default class NoteListWidget extends NoteContextAwareWidget {
setTimeout(() => this.checkRenderStatus(), 100);
}
notesReloadedEvent({ noteIds }) {
if (noteIds.includes(this.noteId)) {
notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) {
if (this.noteId && noteIds.includes(this.noteId)) {
this.refresh();
}
}
entitiesReloadedEvent({ loadResults }) {
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
this.shownNoteId = null; // force render
this.checkRenderStatus();

View File

@@ -163,7 +163,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
private themeStyle!: string;
private $container!: JQuery<HTMLElement>;
private $styleResolver!: JQuery<HTMLElement>;
private graph!: ForceGraph;
graph!: ForceGraph;
private noteIdToSizeMap!: Record<string, number>;
private zoomLevel!: number;
private nodes!: Node[];

View File

@@ -3,10 +3,11 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import SpacedUpdate from "../services/spaced_update.js";
import appContext from "../components/app_context.js";
import appContext, { type EventData } from "../components/app_context.js";
import branchService from "../services/branches.js";
import shortcutService from "../services/shortcuts.js";
import utils from "../services/utils.js";
import type FNote from "../entities/fnote.js";
const TPL = `
<div class="note-title-widget">
@@ -33,13 +34,20 @@ const TPL = `
</div>`;
export default class NoteTitleWidget extends NoteContextAwareWidget {
private $noteTitle!: JQuery<HTMLElement>;
private deleteNoteOnEscape: boolean;
private spacedUpdate: SpacedUpdate;
constructor() {
super();
this.spacedUpdate = new SpacedUpdate(async () => {
const title = this.$noteTitle.val();
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
if (this.note) {
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
}
await server.put(`notes/${this.noteId}/title`, { title }, this.componentId);
});
@@ -62,37 +70,36 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
});
shortcutService.bindElShortcut(this.$noteTitle, "esc", () => {
if (this.deleteNoteOnEscape && this.noteContext.isActive()) {
if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) {
branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
}
});
shortcutService.bindElShortcut(this.$noteTitle, "return", () => {
this.triggerCommand("focusOnDetail", { ntxId: this.noteContext.ntxId });
this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId });
});
}
async refreshWithNote(note) {
const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext.viewScope.viewMode !== "default";
async refreshWithNote(note: FNote) {
const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext?.viewScope?.viewMode !== "default";
this.$noteTitle.val(isReadOnly ? await this.noteContext.getNavigationTitle() : note.title);
this.$noteTitle.val(isReadOnly ? await this.noteContext?.getNavigationTitle() || "" : note.title);
this.$noteTitle.prop("readonly", isReadOnly);
this.setProtectedStatus(note);
}
/** @param {FNote} note */
setProtectedStatus(note) {
setProtectedStatus(note: FNote) {
this.$noteTitle.toggleClass("protected", !!note.isProtected);
}
async beforeNoteSwitchEvent({ noteContext }) {
async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
if (this.isNoteContext(noteContext.ntxId)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
async beforeNoteContextRemoveEvent({ ntxIds }) {
async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
if (this.isNoteContext(ntxIds)) {
await this.spacedUpdate.updateNowIfNecessary();
}
@@ -112,8 +119,8 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
}
}
entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteReloaded(this.noteId)) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId) && this.note) {
// not updating the title specifically since the synced title might be older than what the user is currently typing
this.setProtectedStatus(this.note);
}

View File

@@ -21,6 +21,7 @@ const NOTE_TYPES = [
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true },
{ type: "book", mime: "", title: t("note_types.book"), selectable: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
{ type: "geoMap", mime: "application/json", title: t("note_types.geo-map"), selectable: true },
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true }
];

View File

@@ -3,6 +3,8 @@ import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import server from "../../services/server.js";
import utils from "../../services/utils.js";
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="note-info-widget">
@@ -10,18 +12,18 @@ const TPL = `
.note-info-widget {
padding: 12px;
}
.note-info-widget-table {
max-width: 100%;
max-width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
.note-info-widget-table td, .note-info-widget-table th {
padding: 5px;
}
.note-info-mime {
max-width: 13em;
overflow: hidden;
@@ -61,7 +63,33 @@ const TPL = `
</div>
`;
// TODO: Deduplicate with server
interface NoteSizeResponse {
noteSize: number;
}
interface SubtreeSizeResponse {
subTreeNoteCount: number;
subTreeSize: number;
}
interface MetadataResponse {
dateCreated: number;
dateModified: number;
}
export default class NoteInfoWidget extends NoteContextAwareWidget {
private $noteId!: JQuery<HTMLElement>;
private $dateCreated!: JQuery<HTMLElement>;
private $dateModified!: JQuery<HTMLElement>;
private $type!: JQuery<HTMLElement>;
private $mime!: JQuery<HTMLElement>;
private $noteSizesWrapper!: JQuery<HTMLElement>;
private $noteSize!: JQuery<HTMLElement>;
private $subTreeSize!: JQuery<HTMLElement>;
private $calculateButton!: JQuery<HTMLElement>;
get name() {
return "noteInfo";
}
@@ -71,7 +99,7 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
}
isEnabled() {
return this.note;
return !!this.note;
}
getTitle() {
@@ -104,10 +132,10 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
const noteSizeResp = await server.get<NoteSizeResponse>(`stats/note-size/${this.noteId}`);
this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize));
const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`);
const subTreeResp = await server.get<SubtreeSizeResponse>(`stats/subtree-size/${this.noteId}`);
if (subTreeResp.subTreeNoteCount > 1) {
this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount }));
@@ -117,8 +145,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
});
}
async refreshWithNote(note) {
const metadata = await server.get(`notes/${this.noteId}/metadata`);
async refreshWithNote(note: FNote) {
const metadata = await server.get<MetadataResponse>(`notes/${this.noteId}/metadata`);
this.$noteId.text(note.noteId);
this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated);
@@ -137,8 +165,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
this.$noteSizesWrapper.hide();
}
entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId)) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId))) {
this.refresh();
}
}

View File

@@ -8,18 +8,18 @@ const TPL = `
.note-map-ribbon-widget {
position: relative;
}
.note-map-ribbon-widget .note-map-container {
height: 300px;
}
.open-full-button, .collapse-button {
position: absolute;
right: 5px;
bottom: 5px;
z-index: 1000;
}
.style-resolver {
color: var(--muted-text-color);
display: none;
@@ -33,6 +33,13 @@ const TPL = `
</div>`;
export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
private openState!: "small" | "full";
private noteMapWidget: NoteMapWidget;
private $container!: JQuery<HTMLElement>;
private $openFullButton!: JQuery<HTMLElement>;
private $collapseButton!: JQuery<HTMLElement>;
constructor() {
super();
@@ -106,7 +113,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
setSmallSize() {
const SMALL_SIZE_HEIGHT = 300;
const width = this.$widget.width();
const width = this.$widget.width() ?? 0;
this.$widget.find(".note-map-container").height(SMALL_SIZE_HEIGHT).width(width);
}
@@ -114,9 +121,11 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
setFullHeight() {
const { top } = this.$widget[0].getBoundingClientRect();
const height = $(window).height() - top;
const width = this.$widget.width();
const height = ($(window).height() ?? 0) - top;
const width = (this.$widget.width() ?? 0);
this.$widget.find(".note-map-container").height(height).width(width);
this.$widget.find(".note-map-container")
.height(height)
.width(width);
}
}

View File

@@ -2,6 +2,9 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
import treeService from "../../services/tree.js";
import linkService from "../../services/link.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type { NotePathRecord } from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js";
const TPL = `
<div class="note-paths-widget">
@@ -37,6 +40,10 @@ const TPL = `
</div>`;
export default class NotePathsWidget extends NoteContextAwareWidget {
private $notePathIntro!: JQuery<HTMLElement>;
private $notePathList!: JQuery<HTMLElement>;
get name() {
return "notePaths";
}
@@ -59,13 +66,12 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
this.$notePathIntro = this.$widget.find(".note-path-intro");
this.$notePathList = this.$widget.find(".note-path-list");
this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.$notePathList.empty();
if (this.noteId === "root") {
if (!this.note || this.noteId === "root") {
this.$notePathList.empty().append(await this.getRenderedPath("root"));
return;
@@ -90,7 +96,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
this.$notePathList.empty().append(...renderedPaths);
}
async getRenderedPath(notePath, notePathRecord = null) {
async getRenderedPath(notePath: string, notePathRecord: NotePathRecord | null = null) {
const title = await treeService.getNotePathTitle(notePath);
const $noteLink = await linkService.createLink(notePath, { title });
@@ -128,8 +134,9 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
return $("<li>").append($noteLink);
}
entitiesReloadedEvent({ loadResults }) {
if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || loadResults.isNoteReloaded(this.noteId)) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) ||
(this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
this.refresh();
}
}

View File

@@ -3,10 +3,12 @@ import linkService from "../../services/link.js";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import type FNote from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js";
const TPL = `
<div class="similar-notes-widget">
<style>
<style>
.similar-notes-wrapper {
max-height: 200px;
overflow: auto;
@@ -31,7 +33,20 @@ const TPL = `
</div>
`;
// TODO: Deduplicate with server
interface SimilarNote {
score: number;
notePath: string[];
noteId: string;
}
export default class SimilarNotesWidget extends NoteContextAwareWidget {
private $similarNotesWrapper!: JQuery<HTMLElement>;
private title?: string;
private rendered?: boolean;
get name() {
return "similarNotes";
}
@@ -41,7 +56,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
}
isEnabled() {
return super.isEnabled() && this.note.type !== "search" && !this.note.isLabelTruthy("similarNotesWidgetDisabled");
return super.isEnabled() && this.note?.type !== "search" && !this.note?.isLabelTruthy("similarNotesWidgetDisabled");
}
getTitle() {
@@ -59,11 +74,15 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper");
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
if (!this.note) {
return;
}
// remember which title was when we found the similar notes
this.title = this.note.title;
const similarNotes = await server.get(`similar-notes/${this.noteId}`);
const similarNotes = await server.get<SimilarNote[]>(`similar-notes/${this.noteId}`);
if (similarNotes.length === 0) {
this.$similarNotesWrapper.empty().append(t("similar_notes.no_similar_notes_found"));
@@ -92,7 +111,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
this.$similarNotesWrapper.empty().append($list);
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.note && this.title !== this.note.title) {
this.rendered = false;

View File

@@ -3,35 +3,36 @@ import BasicWidget from "./basic_widget.js";
import ws from "../services/ws.js";
import options from "../services/options.js";
import syncService from "../services/sync.js";
import { escapeQuotes } from "../services/utils.js";
const TPL = `
<div class="sync-status-widget launcher-button">
<style>
.sync-status-widget {
}
.sync-status {
box-sizing: border-box;
}
.sync-status .sync-status-icon {
display: inline-block;
position: relative;
top: -5px;
font-size: 110%;
}
.sync-status .sync-status-sub-icon {
font-size: 40%;
position: absolute;
font-size: 40%;
position: absolute;
left: 0;
top: 16px;
}
.sync-status .sync-status-icon span {
border: none !important;
}
.sync-status-icon:not(.sync-status-in-progress):hover {
background-color: var(--hover-item-background-color);
cursor: pointer;
@@ -39,31 +40,31 @@ const TPL = `
</style>
<div class="sync-status">
<span class="sync-status-icon sync-status-unknown bx bx-time"
data-bs-toggle="tooltip"
title="${t("sync_status.unknown")}">
<span class="sync-status-icon sync-status-unknown bx bx-time"
data-bs-toggle="tooltip"
title="${escapeQuotes(t("sync_status.unknown"))}">
</span>
<span class="sync-status-icon sync-status-connected-with-changes bx bx-wifi"
data-bs-toggle="tooltip"
title="${t("sync_status.connected_with_changes")}">
data-bs-toggle="tooltip"
title="${escapeQuotes(t("sync_status.connected_with_changes"))}">
<span class="bx bxs-star sync-status-sub-icon"></span>
</span>
<span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi"
data-bs-toggle="tooltip"
title="${t("sync_status.connected_no_changes")}">
<span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi"
data-bs-toggle="tooltip"
title="${escapeQuotes(t("sync_status.connected_no_changes"))}">
</span>
<span class="sync-status-icon sync-status-disconnected-with-changes bx bx-wifi-off"
data-bs-toggle="tooltip"
title="${t("sync_status.disconnected_with_changes")}">
data-bs-toggle="tooltip"
title="${escapeQuotes(t("sync_status.disconnected_with_changes"))}">
<span class="bx bxs-star sync-status-sub-icon"></span>
</span>
<span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off"
<span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off"
data-bs-toggle="tooltip"
title="${t("sync_status.disconnected_no_changes")}">
title="${escapeQuotes(t("sync_status.disconnected_no_changes"))}">
</span>
<span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin"
<span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin"
data-bs-toggle="tooltip"
title="${t("sync_status.in_progress")}">
title="${escapeQuotes(t("sync_status.in_progress"))}">
</span>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -26,6 +26,11 @@
border-radius: 2pt !important;
}
span[style] {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* Fix visibility of checkbox checkmarks
see https://github.com/TriliumNext/Notes/issues/901 */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after {

View File

@@ -396,6 +396,10 @@ body.desktop .dropdown-menu {
color: var(--dropdown-item-icon-destructive-color);
}
.dropdown-item > span:not([class]) {
width: 100%;
}
.CodeMirror {
height: 100%;
background: inherit;

View File

@@ -31,6 +31,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
padding: 0px 10px;
letter-spacing: 0.5px;
font-weight: bold;
top: 0;
}
.attachment-content-wrapper pre code,

View File

@@ -1339,7 +1339,7 @@ body .calendar-dropdown-widget .calendar-body a:hover {
}
/* Item title for deleted notes */
.recent-changes-content ul li.deleted-note .note-title {
.recent-changes-content ul li.deleted-note .note-title > .note-title {
text-decoration: line-through;
}

View File

@@ -1350,7 +1350,7 @@
"mermaid-diagram": "Mermaid Diagram",
"canvas": "Canvas",
"web-view": "Webansicht",
"mind-map": "Mind Map (Beta)",
"mind-map": "Mind Map",
"file": "Datei",
"image": "Bild",
"launcher": "Launcher",

View File

@@ -1403,7 +1403,7 @@
"mermaid-diagram": "Mermaid Diagram",
"canvas": "Canvas",
"web-view": "Web View",
"mind-map": "Mind Map (Beta)",
"mind-map": "Mind Map",
"file": "File",
"image": "Image",
"launcher": "Launcher",

View File

@@ -1403,7 +1403,7 @@
"mermaid-diagram": "Diagrama Mermaid",
"canvas": "Lienzo",
"web-view": "Vista Web",
"mind-map": "Mapa Mental (beta)",
"mind-map": "Mapa Mental",
"file": "Archivo",
"image": "Imagen",
"launcher": "Lanzador",

View File

@@ -1351,7 +1351,7 @@
"mermaid-diagram": "Diagramme Mermaid",
"canvas": "Canevas",
"web-view": "Affichage Web",
"mind-map": "Carte mentale (Beta)",
"mind-map": "Carte mentale",
"file": "Fichier",
"image": "Image",
"launcher": "Raccourci",

View File

@@ -1367,7 +1367,7 @@
"canvas": "Schiță",
"code": "Cod sursă",
"mermaid-diagram": "Diagramă Mermaid",
"mind-map": "Hartă mentală (beta)",
"mind-map": "Hartă mentală",
"note-map": "Hartă notițe",
"relation-map": "Hartă relații",
"render-note": "Randare notiță",

View File

@@ -6,6 +6,12 @@ import type BNote from "../../becca/entities/bnote.js";
import type BAttribute from "../../becca/entities/battribute.js";
import type { Request } from "express";
interface Backlink {
noteId: string;
relationName?: string;
excerpts?: string[];
}
function buildDescendantCountMap(noteIdsToCount: string[]) {
if (!Array.isArray(noteIdsToCount)) {
throw new Error("noteIdsToCount: type error");
@@ -325,7 +331,7 @@ function findExcerpts(sourceNote: BNote, referencedNoteId: string) {
return excerpts;
}
function getFilteredBacklinks(note: BNote) {
function getFilteredBacklinks(note: BNote): BAttribute[] {
return (
note
.getTargetRelations()
@@ -344,7 +350,7 @@ function getBacklinkCount(req: Request) {
};
}
function getBacklinks(req: Request) {
function getBacklinks(req: Request): Backlink[] {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);

View File

@@ -5,6 +5,7 @@ import fs from "fs";
import dataDir from "./data_dir.js";
import path from "path";
import resourceDir from "./resource_dir.js";
import { envToBoolean } from "./utils.js";
const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
@@ -14,6 +15,79 @@ if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) {
fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
}
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
export interface TriliumConfig {
General: {
instanceName: string;
noAuthentication: boolean;
noBackup: boolean;
noDesktopIcon: boolean;
};
Network: {
host: string;
port: string;
https: boolean;
certPath: string;
keyPath: string;
trustedReverseProxy: boolean | string;
};
Sync: {
syncServerHost: string;
syncServerTimeout: string;
syncProxy: string;
};
}
//prettier-ignore
const config: TriliumConfig = {
General: {
instanceName:
process.env.TRILIUM_GENERAL_INSTANCENAME || iniConfig.General.instanceName || "",
noAuthentication:
envToBoolean(process.env.TRILIUM_GENERAL_NOAUTHENTICATION) || iniConfig.General.noAuthentication || false,
noBackup:
envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false,
noDesktopIcon:
envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false
},
Network: {
host:
process.env.TRILIUM_NETWORK_HOST || iniConfig.Network.host || "0.0.0.0",
port:
process.env.TRILIUM_NETWORK_PORT || iniConfig.Network.port || "3000",
https:
envToBoolean(process.env.TRILIUM_NETWORK_HTTPS) || iniConfig.Network.https || false,
certPath:
process.env.TRILIUM_NETWORK_CERTPATH || iniConfig.Network.certPath || "",
keyPath:
process.env.TRILIUM_NETWORK_KEYPATH || iniConfig.Network.keyPath || "",
trustedReverseProxy:
process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false
},
Sync: {
syncServerHost:
process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "",
syncServerTimeout:
process.env.TRILIUM_SYNC_SERVER_TIMEOUT || iniConfig?.Sync?.syncServerTimeout || "120000",
syncProxy:
// additionally checking in iniConfig for inconsistently named syncProxy for backwards compatibility
process.env.TRILIUM_SYNC_SERVER_PROXY || iniConfig?.Sync?.syncProxy || iniConfig?.Sync?.syncServerProxy || ""
}
};
export default config;

View File

@@ -0,0 +1,103 @@
import { describe, it, expect } from "vitest";
import importUtils from "./utils.js";
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
describe("#extractHtmlTitle", () => {
const htmlWithNoTitle = `
<html>
<body>
<div>abc</div>
</body>
</html>`;
const htmlWithTitle = `
<html><head>
<title>Test Title</title>
</head>
<body>
<div>abc</div>
</body>
</html>`;
const htmlWithTitleWOpeningBracket = `
<html><head>
<title>Test < Title</title>
</head>
<body>
<div>abc</div>
</body>
</html>`;
// prettier-ignore
const testCases: TestCase<typeof importUtils.extractHtmlTitle>[] = [
[
"w/ existing <title> tag, it should return the content of the title tag",
[htmlWithTitle],
"Test Title"
],
[
// @TriliumNextTODO: this seems more like an unwanted behaviour to me check if this needs rather fixing
"with existing <title> tag, that includes an opening HTML tag '<', it should return null",
[htmlWithTitleWOpeningBracket],
null
],
[
"w/o an existing <title> tag, it should reutrn null",
[htmlWithNoTitle],
null
],
[
"w/ empty string content, it should return null",
[""],
null
]
];
testCases.forEach((testCase) => {
const [desc, fnParams, expected] = testCase;
return it(desc, () => {
const actual = importUtils.extractHtmlTitle(...fnParams);
expect(actual).toStrictEqual(expected);
});
});
});
describe("#handleH1", () => {
// prettier-ignore
const testCases: TestCase<typeof importUtils.handleH1>[] = [
[
"w/ single <h1> tag w/ identical text content as the title tag: the <h1> tag should be stripped",
["<h1>Title</h1>", "Title"],
""
],
[
"w/ multiple <h1> tags, with the fist matching the title tag: the first <h1> tag should be stripped and subsequent tags converted to <h2>",
["<h1>Title</h1><h1>Header 1</h1><h1>Header 2</h1>", "Title"],
"<h2>Header 1</h2><h2>Header 2</h2>"
],
[
"w/ no <h1> tag and only <h2> tags, it should not cause any changes and return the same content",
["<h2>Heading 1</h2><h2>Heading 2</h2>", "Title"],
"<h2>Heading 1</h2><h2>Heading 2</h2>"
],
[
"w/ multiple <h1> tags, and the 1st matching the title tag, it should strip ONLY the very first occurence of the <h1> tags in the returned content",
["<h1>Topic ABC</h1><h1>Heading 1</h1><h1>Topic ABC</h1>", "Topic ABC"],
"<h2>Heading 1</h2><h2>Topic ABC</h2>"
],
[
"w/ multiple <h1> tags, and the 1st matching NOT the title tag, it should NOT strip any other <h1> tags",
["<h1>Introduction</h1><h1>Topic ABC</h1><h1>Summary</h1>", "Topic ABC"],
"<h2>Introduction</h2><h2>Topic ABC</h2><h2>Summary</h2>"
]
];
testCases.forEach((testCase) => {
const [desc, fnParams, expected] = testCase;
return it(desc, () => {
const actual = importUtils.handleH1(...fnParams);
expect(actual).toStrictEqual(expected);
});
});
});

View File

@@ -1,14 +1,19 @@
"use strict";
function handleH1(content: string, title: string) {
content = content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => {
if (title.trim() === text.trim()) {
return ""; // remove whole H1 tag
} else {
return `<h2>${text}</h2>`;
let isFirstH1Handled = false;
return content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => {
const convertedContent = `<h2>${text}</h2>`;
// strip away very first found h1 tag, if it matches the title
if (!isFirstH1Handled) {
isFirstH1Handled = true;
return title.trim() === text.trim() ? "" : convertedContent;
}
return convertedContent;
});
return content;
}
function extractHtmlTitle(content: string): string | null {

View File

@@ -19,7 +19,7 @@ function getRunAtHours(note: BNote): number[] {
}
function runNotesWithLabel(runAttrValue: string) {
const instanceName = config.General ? config.General.instanceName : null;
const instanceName = config.General.instanceName;
const currentHours = new Date().getHours();
const notes = attributeService.getNotesWithLabel("run", runAttrValue);

View File

@@ -1,7 +1,6 @@
"use strict";
import optionService from "./options.js";
import type { OptionNames } from "./options_interface.js";
import config from "./config.js";
/*
@@ -11,14 +10,14 @@ import config from "./config.js";
* to live sync server.
*/
function get(name: OptionNames) {
return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
function get(name: keyof typeof config.Sync) {
return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
}
export default {
// env variable is the easiest way to guarantee we won't overwrite prod data during development
// after copying prod document/data directory
getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get("syncServerHost"),
getSyncServerHost: () => get("syncServerHost"),
isSyncSetup: () => {
const syncServerHost = get("syncServerHost");

View File

@@ -295,6 +295,18 @@ export function isString(x: any) {
return Object.prototype.toString.call(x) === "[object String]";
}
// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
export function envToBoolean(val: string | undefined) {
if (val === undefined || typeof val !== "string") return undefined;
const valLc = val.toLowerCase().trim();
if (valLc === "true") return true;
if (valLc === "false") return false;
return undefined;
}
/**
* Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package.
* On development builds, this simply refers to the root directory of the application.
@@ -352,5 +364,6 @@ export default {
isString,
getResourceDir,
isMac,
isWindows
isWindows,
envToBoolean
};

View File

@@ -1,4 +1,4 @@
import WebSocket from "ws";
import { WebSocketServer as WebSocketServer, WebSocket } from "ws";
import { isElectron, randomString } from "./utils.js";
import log from "./log.js";
import sql from "./sql.js";
@@ -10,7 +10,7 @@ import becca from "../becca/becca.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import env from "./env.js";
import type { IncomingMessage, Server } from "http";
import type { IncomingMessage, Server as HttpServer } from "http";
import type { EntityChange } from "./entity_changes_interface.js";
if (env.isDev()) {
@@ -24,7 +24,7 @@ if (env.isDev()) {
.on("unlink", debouncedReloadFrontend);
}
let webSocketServer!: WebSocket.Server;
let webSocketServer!: WebSocketServer;
let lastSyncedPush: number | null = null;
interface Message {
@@ -58,8 +58,8 @@ interface Message {
}
type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;
function init(httpServer: Server, sessionParser: SessionParser) {
webSocketServer = new WebSocket.Server({
function init(httpServer: HttpServer, sessionParser: SessionParser) {
webSocketServer = new WebSocketServer({
verifyClient: (info, done) => {
sessionParser(info.req, {}, () => {
const allowed = isElectron() || (info.req as any).session.loggedIn || (config.General && config.General.noAuthentication);