mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	chore(client/ts): port components/note_context
This commit is contained in:
		@@ -22,6 +22,7 @@ import { Node } from "../services/tree.js";
 | 
			
		||||
import LoadResults from "../services/load_results.js";
 | 
			
		||||
import { Attribute } from "../services/attribute_parser.js";
 | 
			
		||||
import NoteTreeWidget from "../widgets/note_tree.js";
 | 
			
		||||
import { GetTextEditorCallback } from "./note_context.js";
 | 
			
		||||
 | 
			
		||||
interface Layout {
 | 
			
		||||
    getRootWidget: (appContext: AppContext) => RootWidget;
 | 
			
		||||
@@ -39,7 +40,7 @@ interface BeforeUploadListener extends Component {
 | 
			
		||||
 * Base interface for the data/arguments for a given command (see {@link CommandMappings}).
 | 
			
		||||
 */
 | 
			
		||||
export interface CommandData {
 | 
			
		||||
    ntxId?: string;
 | 
			
		||||
    ntxId?: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -59,6 +60,10 @@ export interface NoteCommandData extends CommandData {
 | 
			
		||||
    viewScope?: ViewScope;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ExecuteCommandData extends CommandData {
 | 
			
		||||
    resolve: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The keys represent the different commands that can be triggered via {@link AppContext#triggerCommand} (first argument), and the values represent the data or arguments definition of the given command. All data for commands must extend {@link CommandData}.
 | 
			
		||||
 */
 | 
			
		||||
@@ -130,6 +135,12 @@ export type CommandMappings = {
 | 
			
		||||
    executeInActiveNoteDetailWidget: CommandData & {
 | 
			
		||||
        callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void
 | 
			
		||||
    };
 | 
			
		||||
    executeWithTextEditor: CommandData & ExecuteCommandData & {
 | 
			
		||||
        callback?: GetTextEditorCallback;
 | 
			
		||||
    };
 | 
			
		||||
    executeWithCodeEditor: CommandData & ExecuteCommandData;
 | 
			
		||||
    executeWithContentElement: CommandData & ExecuteCommandData;
 | 
			
		||||
    executeWithTypeWidget: CommandData & ExecuteCommandData;
 | 
			
		||||
    addTextToActiveEditor: CommandData & {
 | 
			
		||||
        text: string;
 | 
			
		||||
    };
 | 
			
		||||
@@ -181,8 +192,7 @@ type EventMappings = {
 | 
			
		||||
    };
 | 
			
		||||
    addNewLabel: CommandData;
 | 
			
		||||
    addNewRelation: CommandData;
 | 
			
		||||
    sqlQueryResults: {
 | 
			
		||||
        ntxId: string;
 | 
			
		||||
    sqlQueryResults: CommandData & {
 | 
			
		||||
        results: SqlExecuteResults;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,39 @@
 | 
			
		||||
import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
			
		||||
import server from "../services/server.js";
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import appContext from "./app_context.js";
 | 
			
		||||
import appContext, { EventData, 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 { ViewScope } from "../services/link.js";
 | 
			
		||||
import FNote from "../entities/fnote.js";
 | 
			
		||||
 | 
			
		||||
class NoteContext extends Component {
 | 
			
		||||
    constructor(ntxId = null, hoistedNoteId = 'root', mainNtxId = null) {
 | 
			
		||||
interface SetNoteOpts {
 | 
			
		||||
    triggerSwitchEvent?: unknown;
 | 
			
		||||
    viewScope?: ViewScope;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type GetTextEditorCallback = () => void;
 | 
			
		||||
 | 
			
		||||
class NoteContext extends Component
 | 
			
		||||
    implements EventListener<"entitiesReloaded">
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    ntxId: string | null;
 | 
			
		||||
    hoistedNoteId: string;
 | 
			
		||||
    private mainNtxId: string | null;
 | 
			
		||||
 | 
			
		||||
    private notePath?: string | null;
 | 
			
		||||
    private noteId?: string | null;
 | 
			
		||||
    private parentNoteId?: string | null;
 | 
			
		||||
    private viewScope?: ViewScope;
 | 
			
		||||
 | 
			
		||||
    constructor(ntxId: string | null = null, hoistedNoteId: string = 'root', mainNtxId: string | null = null) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.ntxId = ntxId || this.constructor.generateNtxId();
 | 
			
		||||
        this.ntxId = ntxId || NoteContext.generateNtxId();
 | 
			
		||||
        this.hoistedNoteId = hoistedNoteId;
 | 
			
		||||
        this.mainNtxId = mainNtxId;
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +62,7 @@ class NoteContext extends Component {
 | 
			
		||||
        return !this.noteId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setNote(inputNotePath, opts = {}) {
 | 
			
		||||
    async setNote(inputNotePath: string, opts: SetNoteOpts = {}) {
 | 
			
		||||
        opts.triggerSwitchEvent = opts.triggerSwitchEvent !== undefined ? opts.triggerSwitchEvent : true;
 | 
			
		||||
        opts.viewScope = opts.viewScope || {};
 | 
			
		||||
        opts.viewScope.viewMode = opts.viewScope.viewMode || "default";
 | 
			
		||||
@@ -84,16 +105,16 @@ class NoteContext extends Component {
 | 
			
		||||
 | 
			
		||||
    async setHoistedNoteIfNeeded() {
 | 
			
		||||
        if (this.hoistedNoteId === 'root'
 | 
			
		||||
            && this.notePath.startsWith("root/_hidden")
 | 
			
		||||
            && !this.note.isLabelTruthy("keepCurrentHoisting")
 | 
			
		||||
            && this.notePath?.startsWith("root/_hidden")
 | 
			
		||||
            && !this.note?.isLabelTruthy("keepCurrentHoisting")
 | 
			
		||||
        ) {
 | 
			
		||||
            // hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note
 | 
			
		||||
 | 
			
		||||
            let hoistedNoteId = '_hidden';
 | 
			
		||||
 | 
			
		||||
            if (this.note.isLaunchBarConfig()) {
 | 
			
		||||
            if (this.note?.isLaunchBarConfig()) {
 | 
			
		||||
                hoistedNoteId = '_lbRoot';
 | 
			
		||||
            } else if (this.note.isOptions()) {
 | 
			
		||||
            } else if (this.note?.isOptions()) {
 | 
			
		||||
                hoistedNoteId = '_options';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -138,19 +159,19 @@ class NoteContext extends Component {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    saveToRecentNotes(resolvedNotePath) {
 | 
			
		||||
    saveToRecentNotes(resolvedNotePath: string) {
 | 
			
		||||
        setTimeout(async () => {
 | 
			
		||||
            // we include the note in the recent list only if the user stayed on the note at least 5 seconds
 | 
			
		||||
            if (resolvedNotePath && resolvedNotePath === this.notePath) {
 | 
			
		||||
                await server.post('recent-notes', {
 | 
			
		||||
                    noteId: this.note.noteId,
 | 
			
		||||
                    noteId: this.note?.noteId,
 | 
			
		||||
                    notePath: this.notePath
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }, 5000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getResolvedNotePath(inputNotePath) {
 | 
			
		||||
    async getResolvedNotePath(inputNotePath: string) {
 | 
			
		||||
        const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!resolvedNotePath) {
 | 
			
		||||
@@ -165,8 +186,7 @@ class NoteContext extends Component {
 | 
			
		||||
        return resolvedNotePath;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {FNote} */
 | 
			
		||||
    get note() {
 | 
			
		||||
    get note(): FNote | null {
 | 
			
		||||
        if (!this.noteId || !(this.noteId in froca.notes)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
@@ -206,7 +226,7 @@ class NoteContext extends Component {
 | 
			
		||||
        await this.setHoistedNoteId('root');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setHoistedNoteId(noteIdToHoist) {
 | 
			
		||||
    async setHoistedNoteId(noteIdToHoist: string) {
 | 
			
		||||
        if (this.hoistedNoteId === noteIdToHoist) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -225,7 +245,7 @@ class NoteContext extends Component {
 | 
			
		||||
 | 
			
		||||
    /** @returns {Promise<boolean>} */
 | 
			
		||||
    async isReadOnly() {
 | 
			
		||||
        if (this.viewScope.readOnlyTemporarilyDisabled) {
 | 
			
		||||
        if (this?.viewScope?.readOnlyTemporarilyDisabled) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -238,22 +258,26 @@ class NoteContext extends Component {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.viewScope.viewMode === 'source') {
 | 
			
		||||
        if (this.viewScope?.viewMode === 'source') {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const blob = await this.note.getBlob();
 | 
			
		||||
        if (!blob) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const sizeLimit = this.note.type === 'text'
 | 
			
		||||
            ? options.getInt('autoReadonlySizeText')
 | 
			
		||||
            : options.getInt('autoReadonlySizeCode');
 | 
			
		||||
 | 
			
		||||
        return blob.contentLength > sizeLimit
 | 
			
		||||
        return sizeLimit
 | 
			
		||||
            && blob.contentLength > sizeLimit
 | 
			
		||||
            && !this.note.isLabelTruthy('autoReadOnlyDisabled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async entitiesReloadedEvent({loadResults}) {
 | 
			
		||||
        if (loadResults.isNoteReloaded(this.noteId)) {
 | 
			
		||||
    async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
 | 
			
		||||
            const noteRow = loadResults.getEntityRow('notes', this.noteId);
 | 
			
		||||
 | 
			
		||||
            if (noteRow.isDeleted) {
 | 
			
		||||
@@ -270,14 +294,14 @@ class NoteContext extends Component {
 | 
			
		||||
 | 
			
		||||
    hasNoteList() {
 | 
			
		||||
        return this.note
 | 
			
		||||
            && this.viewScope.viewMode === 'default'
 | 
			
		||||
            && this.viewScope?.viewMode === 'default'
 | 
			
		||||
            && this.note.hasChildren()
 | 
			
		||||
            && ['book', 'text', 'code'].includes(this.note.type)
 | 
			
		||||
            && this.note.mime !== 'text/x-sqlite;schema=trilium'
 | 
			
		||||
            && !this.note.isLabelTruthy('hideChildrenOverview');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getTextEditor(callback) {
 | 
			
		||||
    async getTextEditor(callback: GetTextEditorCallback) {
 | 
			
		||||
        return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', {
 | 
			
		||||
            callback,
 | 
			
		||||
            resolve,
 | 
			
		||||
@@ -306,7 +330,7 @@ class NoteContext extends Component {
 | 
			
		||||
        })));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    timeout(promise) {
 | 
			
		||||
    timeout(promise: Promise<unknown>) {
 | 
			
		||||
        return Promise.race([
 | 
			
		||||
            promise,
 | 
			
		||||
            new Promise(res => setTimeout(() => res(null), 200))
 | 
			
		||||
@@ -327,11 +351,11 @@ class NoteContext extends Component {
 | 
			
		||||
 | 
			
		||||
        const { note, viewScope } = this;
 | 
			
		||||
 | 
			
		||||
        let title = viewScope.viewMode === 'default'
 | 
			
		||||
        let title = viewScope?.viewMode === 'default'
 | 
			
		||||
            ? note.title
 | 
			
		||||
            : `${note.title}: ${viewScope.viewMode}`;
 | 
			
		||||
            : `${note.title}: ${viewScope?.viewMode}`;
 | 
			
		||||
 | 
			
		||||
        if (viewScope.attachmentId) {
 | 
			
		||||
        if (viewScope?.attachmentId) {
 | 
			
		||||
            // assuming the attachment has been already loaded
 | 
			
		||||
            const attachment = await note.getAttachmentById(viewScope.attachmentId);
 | 
			
		||||
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
import { EntityRowNames } from "./services/load_results.js";
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate with src/services/entity_changes_interface.ts
 | 
			
		||||
export interface EntityChange {
 | 
			
		||||
    id?: number | null;
 | 
			
		||||
    noteId?: string;
 | 
			
		||||
    entityName: string;
 | 
			
		||||
    entityName: EntityRowNames;
 | 
			
		||||
    entityId: string;
 | 
			
		||||
    entity?: any;
 | 
			
		||||
    positions?: Record<string, number>;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ type ViewMode = "default" | "source" | "attachments" | string;
 | 
			
		||||
export interface ViewScope {
 | 
			
		||||
    viewMode?: ViewMode;
 | 
			
		||||
    attachmentId?: string;
 | 
			
		||||
    readOnlyTemporarilyDisabled?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CreateLinkOptions {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { NoteRow } from "../../../becca/entities/rows.js";
 | 
			
		||||
import { EntityChange } from "../server_types.js";
 | 
			
		||||
 | 
			
		||||
interface BranchRow {
 | 
			
		||||
@@ -30,8 +31,16 @@ interface ContentNoteIdToComponentIdRow {
 | 
			
		||||
    componentId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EntityRowMappings = {
 | 
			
		||||
    "notes": NoteRow,
 | 
			
		||||
    "branches": BranchRow,
 | 
			
		||||
    "attributes": AttributeRow
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type EntityRowNames = keyof EntityRowMappings;
 | 
			
		||||
 | 
			
		||||
export default class LoadResults {
 | 
			
		||||
    private entities: Record<string, Record<string, unknown>>;
 | 
			
		||||
    private entities: Record<keyof EntityRowMappings, Record<string, any>>;
 | 
			
		||||
    private noteIdToComponentId: Record<string, string[]>;
 | 
			
		||||
    private componentIdToNoteIds: Record<string, string[]>;
 | 
			
		||||
    private branchRows: BranchRow[];
 | 
			
		||||
@@ -43,14 +52,15 @@ export default class LoadResults {
 | 
			
		||||
    private attachmentRows: AttachmentRow[];
 | 
			
		||||
 | 
			
		||||
    constructor(entityChanges: EntityChange[]) {
 | 
			
		||||
        this.entities = {};
 | 
			
		||||
        const entities: Record<string, Record<string, any>> = {};
 | 
			
		||||
 | 
			
		||||
        for (const {entityId, entityName, entity} of entityChanges) {
 | 
			
		||||
            if (entity) {
 | 
			
		||||
                this.entities[entityName] = this.entities[entityName] || [];
 | 
			
		||||
                this.entities[entityName][entityId] = entity;
 | 
			
		||||
                entities[entityName] = entities[entityName] || [];
 | 
			
		||||
                entities[entityName][entityId] = entity;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.entities = entities;
 | 
			
		||||
 | 
			
		||||
        this.noteIdToComponentId = {};
 | 
			
		||||
        this.componentIdToNoteIds = {};
 | 
			
		||||
@@ -70,8 +80,8 @@ export default class LoadResults {
 | 
			
		||||
        this.attachmentRows = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getEntityRow(entityName: string, entityId: string) {
 | 
			
		||||
        return this.entities[entityName]?.[entityId];
 | 
			
		||||
    getEntityRow<T extends EntityRowNames>(entityName: T, entityId: string): EntityRowMappings[T] {
 | 
			
		||||
        return (this.entities[entityName]?.[entityId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addNote(noteId: string, componentId?: string | null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ async function touchProtectedSession() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function touchProtectedSessionIfNecessary(note: FNote) {
 | 
			
		||||
function touchProtectedSessionIfNecessary(note: FNote | null) {
 | 
			
		||||
    if (note && note.isProtected && isProtectedSessionAvailable()) {
 | 
			
		||||
        touchProtectedSession();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -382,7 +382,7 @@ function escapeRegExp(str: string) {
 | 
			
		||||
    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function areObjectsEqual () {
 | 
			
		||||
function areObjectsEqual(...args: unknown[]) {
 | 
			
		||||
    let i;
 | 
			
		||||
    let l;
 | 
			
		||||
    let leftChain: Object[];
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user