chore(client/ts): port components/note_context

This commit is contained in:
Elian Doran 2024-12-23 15:16:41 +02:00
parent c06dc23ecf
commit 8a47b2f5a8
No known key found for this signature in database
7 changed files with 85 additions and 38 deletions

View File

@ -22,6 +22,7 @@ import { Node } from "../services/tree.js";
import LoadResults from "../services/load_results.js"; import LoadResults from "../services/load_results.js";
import { Attribute } from "../services/attribute_parser.js"; import { Attribute } from "../services/attribute_parser.js";
import NoteTreeWidget from "../widgets/note_tree.js"; import NoteTreeWidget from "../widgets/note_tree.js";
import { GetTextEditorCallback } from "./note_context.js";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; 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}). * Base interface for the data/arguments for a given command (see {@link CommandMappings}).
*/ */
export interface CommandData { export interface CommandData {
ntxId?: string; ntxId?: string | null;
} }
/** /**
@ -59,6 +60,10 @@ export interface NoteCommandData extends CommandData {
viewScope?: ViewScope; 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}. * 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 & { executeInActiveNoteDetailWidget: CommandData & {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void
}; };
executeWithTextEditor: CommandData & ExecuteCommandData & {
callback?: GetTextEditorCallback;
};
executeWithCodeEditor: CommandData & ExecuteCommandData;
executeWithContentElement: CommandData & ExecuteCommandData;
executeWithTypeWidget: CommandData & ExecuteCommandData;
addTextToActiveEditor: CommandData & { addTextToActiveEditor: CommandData & {
text: string; text: string;
}; };
@ -181,8 +192,7 @@ type EventMappings = {
}; };
addNewLabel: CommandData; addNewLabel: CommandData;
addNewRelation: CommandData; addNewRelation: CommandData;
sqlQueryResults: { sqlQueryResults: CommandData & {
ntxId: string;
results: SqlExecuteResults; results: SqlExecuteResults;
} }
} }

View File

@ -1,18 +1,39 @@
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js"; import server from "../services/server.js";
import utils from "../services/utils.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 treeService from "../services/tree.js";
import Component from "./component.js"; import Component from "./component.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js"; import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js"; import options from "../services/options.js";
import { ViewScope } from "../services/link.js";
import FNote from "../entities/fnote.js";
class NoteContext extends Component { interface SetNoteOpts {
constructor(ntxId = null, hoistedNoteId = 'root', mainNtxId = null) { 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(); super();
this.ntxId = ntxId || this.constructor.generateNtxId(); this.ntxId = ntxId || NoteContext.generateNtxId();
this.hoistedNoteId = hoistedNoteId; this.hoistedNoteId = hoistedNoteId;
this.mainNtxId = mainNtxId; this.mainNtxId = mainNtxId;
@ -41,7 +62,7 @@ class NoteContext extends Component {
return !this.noteId; return !this.noteId;
} }
async setNote(inputNotePath, opts = {}) { async setNote(inputNotePath: string, opts: SetNoteOpts = {}) {
opts.triggerSwitchEvent = opts.triggerSwitchEvent !== undefined ? opts.triggerSwitchEvent : true; opts.triggerSwitchEvent = opts.triggerSwitchEvent !== undefined ? opts.triggerSwitchEvent : true;
opts.viewScope = opts.viewScope || {}; opts.viewScope = opts.viewScope || {};
opts.viewScope.viewMode = opts.viewScope.viewMode || "default"; opts.viewScope.viewMode = opts.viewScope.viewMode || "default";
@ -84,16 +105,16 @@ class NoteContext extends Component {
async setHoistedNoteIfNeeded() { async setHoistedNoteIfNeeded() {
if (this.hoistedNoteId === 'root' if (this.hoistedNoteId === 'root'
&& this.notePath.startsWith("root/_hidden") && this.notePath?.startsWith("root/_hidden")
&& !this.note.isLabelTruthy("keepCurrentHoisting") && !this.note?.isLabelTruthy("keepCurrentHoisting")
) { ) {
// hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note // hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note
let hoistedNoteId = '_hidden'; let hoistedNoteId = '_hidden';
if (this.note.isLaunchBarConfig()) { if (this.note?.isLaunchBarConfig()) {
hoistedNoteId = '_lbRoot'; hoistedNoteId = '_lbRoot';
} else if (this.note.isOptions()) { } else if (this.note?.isOptions()) {
hoistedNoteId = '_options'; hoistedNoteId = '_options';
} }
@ -138,19 +159,19 @@ class NoteContext extends Component {
} }
} }
saveToRecentNotes(resolvedNotePath) { saveToRecentNotes(resolvedNotePath: string) {
setTimeout(async () => { setTimeout(async () => {
// we include the note in the recent list only if the user stayed on the note at least 5 seconds // 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) { if (resolvedNotePath && resolvedNotePath === this.notePath) {
await server.post('recent-notes', { await server.post('recent-notes', {
noteId: this.note.noteId, noteId: this.note?.noteId,
notePath: this.notePath notePath: this.notePath
}); });
} }
}, 5000); }, 5000);
} }
async getResolvedNotePath(inputNotePath) { async getResolvedNotePath(inputNotePath: string) {
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId); const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId);
if (!resolvedNotePath) { if (!resolvedNotePath) {
@ -165,8 +186,7 @@ class NoteContext extends Component {
return resolvedNotePath; return resolvedNotePath;
} }
/** @returns {FNote} */ get note(): FNote | null {
get note() {
if (!this.noteId || !(this.noteId in froca.notes)) { if (!this.noteId || !(this.noteId in froca.notes)) {
return null; return null;
} }
@ -206,7 +226,7 @@ class NoteContext extends Component {
await this.setHoistedNoteId('root'); await this.setHoistedNoteId('root');
} }
async setHoistedNoteId(noteIdToHoist) { async setHoistedNoteId(noteIdToHoist: string) {
if (this.hoistedNoteId === noteIdToHoist) { if (this.hoistedNoteId === noteIdToHoist) {
return; return;
} }
@ -225,7 +245,7 @@ class NoteContext extends Component {
/** @returns {Promise<boolean>} */ /** @returns {Promise<boolean>} */
async isReadOnly() { async isReadOnly() {
if (this.viewScope.readOnlyTemporarilyDisabled) { if (this?.viewScope?.readOnlyTemporarilyDisabled) {
return false; return false;
} }
@ -238,22 +258,26 @@ class NoteContext extends Component {
return true; return true;
} }
if (this.viewScope.viewMode === 'source') { if (this.viewScope?.viewMode === 'source') {
return true; return true;
} }
const blob = await this.note.getBlob(); const blob = await this.note.getBlob();
if (!blob) {
return false;
}
const sizeLimit = this.note.type === 'text' const sizeLimit = this.note.type === 'text'
? options.getInt('autoReadonlySizeText') ? options.getInt('autoReadonlySizeText')
: options.getInt('autoReadonlySizeCode'); : options.getInt('autoReadonlySizeCode');
return blob.contentLength > sizeLimit return sizeLimit
&& blob.contentLength > sizeLimit
&& !this.note.isLabelTruthy('autoReadOnlyDisabled'); && !this.note.isLabelTruthy('autoReadOnlyDisabled');
} }
async entitiesReloadedEvent({loadResults}) { async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) { if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
const noteRow = loadResults.getEntityRow('notes', this.noteId); const noteRow = loadResults.getEntityRow('notes', this.noteId);
if (noteRow.isDeleted) { if (noteRow.isDeleted) {
@ -270,14 +294,14 @@ class NoteContext extends Component {
hasNoteList() { hasNoteList() {
return this.note return this.note
&& this.viewScope.viewMode === 'default' && this.viewScope?.viewMode === 'default'
&& this.note.hasChildren() && this.note.hasChildren()
&& ['book', 'text', 'code'].includes(this.note.type) && ['book', 'text', 'code'].includes(this.note.type)
&& this.note.mime !== 'text/x-sqlite;schema=trilium' && this.note.mime !== 'text/x-sqlite;schema=trilium'
&& !this.note.isLabelTruthy('hideChildrenOverview'); && !this.note.isLabelTruthy('hideChildrenOverview');
} }
async getTextEditor(callback) { async getTextEditor(callback: GetTextEditorCallback) {
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', { return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', {
callback, callback,
resolve, resolve,
@ -306,7 +330,7 @@ class NoteContext extends Component {
}))); })));
} }
timeout(promise) { timeout(promise: Promise<unknown>) {
return Promise.race([ return Promise.race([
promise, promise,
new Promise(res => setTimeout(() => res(null), 200)) new Promise(res => setTimeout(() => res(null), 200))
@ -327,11 +351,11 @@ class NoteContext extends Component {
const { note, viewScope } = this; const { note, viewScope } = this;
let title = viewScope.viewMode === 'default' let title = viewScope?.viewMode === 'default'
? note.title ? note.title
: `${note.title}: ${viewScope.viewMode}`; : `${note.title}: ${viewScope?.viewMode}`;
if (viewScope.attachmentId) { if (viewScope?.attachmentId) {
// assuming the attachment has been already loaded // assuming the attachment has been already loaded
const attachment = await note.getAttachmentById(viewScope.attachmentId); const attachment = await note.getAttachmentById(viewScope.attachmentId);

View File

@ -1,8 +1,10 @@
import { EntityRowNames } from "./services/load_results.js";
// TODO: Deduplicate with src/services/entity_changes_interface.ts // TODO: Deduplicate with src/services/entity_changes_interface.ts
export interface EntityChange { export interface EntityChange {
id?: number | null; id?: number | null;
noteId?: string; noteId?: string;
entityName: string; entityName: EntityRowNames;
entityId: string; entityId: string;
entity?: any; entity?: any;
positions?: Record<string, number>; positions?: Record<string, number>;

View File

@ -30,6 +30,7 @@ type ViewMode = "default" | "source" | "attachments" | string;
export interface ViewScope { export interface ViewScope {
viewMode?: ViewMode; viewMode?: ViewMode;
attachmentId?: string; attachmentId?: string;
readOnlyTemporarilyDisabled?: boolean;
} }
interface CreateLinkOptions { interface CreateLinkOptions {

View File

@ -1,3 +1,4 @@
import { NoteRow } from "../../../becca/entities/rows.js";
import { EntityChange } from "../server_types.js"; import { EntityChange } from "../server_types.js";
interface BranchRow { interface BranchRow {
@ -30,8 +31,16 @@ interface ContentNoteIdToComponentIdRow {
componentId: string; componentId: string;
} }
type EntityRowMappings = {
"notes": NoteRow,
"branches": BranchRow,
"attributes": AttributeRow
}
export type EntityRowNames = keyof EntityRowMappings;
export default class LoadResults { 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 noteIdToComponentId: Record<string, string[]>;
private componentIdToNoteIds: Record<string, string[]>; private componentIdToNoteIds: Record<string, string[]>;
private branchRows: BranchRow[]; private branchRows: BranchRow[];
@ -43,14 +52,15 @@ export default class LoadResults {
private attachmentRows: AttachmentRow[]; private attachmentRows: AttachmentRow[];
constructor(entityChanges: EntityChange[]) { constructor(entityChanges: EntityChange[]) {
this.entities = {}; const entities: Record<string, Record<string, any>> = {};
for (const {entityId, entityName, entity} of entityChanges) { for (const {entityId, entityName, entity} of entityChanges) {
if (entity) { if (entity) {
this.entities[entityName] = this.entities[entityName] || []; entities[entityName] = entities[entityName] || [];
this.entities[entityName][entityId] = entity; entities[entityName][entityId] = entity;
} }
} }
this.entities = entities;
this.noteIdToComponentId = {}; this.noteIdToComponentId = {};
this.componentIdToNoteIds = {}; this.componentIdToNoteIds = {};
@ -70,8 +80,8 @@ export default class LoadResults {
this.attachmentRows = []; this.attachmentRows = [];
} }
getEntityRow(entityName: string, entityId: string) { getEntityRow<T extends EntityRowNames>(entityName: T, entityId: string): EntityRowMappings[T] {
return this.entities[entityName]?.[entityId]; return (this.entities[entityName]?.[entityId]);
} }
addNote(noteId: string, componentId?: string | null) { addNote(noteId: string, componentId?: string | null) {

View File

@ -21,7 +21,7 @@ async function touchProtectedSession() {
} }
} }
function touchProtectedSessionIfNecessary(note: FNote) { function touchProtectedSessionIfNecessary(note: FNote | null) {
if (note && note.isProtected && isProtectedSessionAvailable()) { if (note && note.isProtected && isProtectedSessionAvailable()) {
touchProtectedSession(); touchProtectedSession();
} }

View File

@ -382,7 +382,7 @@ function escapeRegExp(str: string) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
} }
function areObjectsEqual () { function areObjectsEqual(...args: unknown[]) {
let i; let i;
let l; let l;
let leftChain: Object[]; let leftChain: Object[];