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

This commit is contained in:
Adorian Doran
2025-01-12 23:45:03 +02:00
612 changed files with 138078 additions and 26087 deletions

View File

@@ -6,7 +6,7 @@ sqlInit.dbReady.then(async () => {
try {
console.log("Starting anonymization...");
const resp = await anonymizationService.createAnonymizedCopy('full');
const resp = await anonymizationService.createAnonymizedCopy("full");
if (resp.success) {
console.log(`Anonymized file has been saved to: ${resp.anonymizedFilePath}`);
@@ -15,8 +15,7 @@ sqlInit.dbReady.then(async () => {
} else {
console.log("Anonymization failed.");
}
}
catch (e: any) {
} catch (e: any) {
console.error(e.message, e.stack);
}

View File

@@ -16,8 +16,8 @@ import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js";
import { t } from "i18next";
await import('./services/handlers.js');
await import('./becca/becca_loader.js');
await import("./services/handlers.js");
await import("./becca/becca_loader.js");
const app = express();
@@ -27,8 +27,8 @@ const scriptDir = dirname(fileURLToPath(import.meta.url));
sql_init.initializeDb();
// view engine setup
app.set('views', path.join(scriptDir, 'views'));
app.set('view engine', 'ejs');
app.set("views", path.join(scriptDir, "views"));
app.set("view engine", "ejs");
app.use((req, res, next) => {
res.locals.t = t;
@@ -39,21 +39,23 @@ if (!utils.isElectron()) {
app.use(compression()); // HTTP compression
}
app.use(helmet({
hidePoweredBy: false, // errors out in electron
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false
}));
app.use(
helmet({
hidePoweredBy: false, // errors out in electron
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false
})
);
app.use(express.text({ limit: '500mb' }));
app.use(express.json({ limit: '500mb' }));
app.use(express.raw({ limit: '500mb' }));
app.use(express.text({ limit: "500mb" }));
app.use(express.json({ limit: "500mb" }));
app.use(express.raw({ limit: "500mb" }));
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(scriptDir, 'public/root')));
app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, 'public/manifest.webmanifest')));
app.use(`/robots.txt`, express.static(path.join(scriptDir, 'public/robots.txt')));
app.use(`/icon.png`, express.static(path.join(scriptDir, 'public/icon.png')));
app.use(express.static(path.join(scriptDir, "public/root")));
app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, "public/manifest.webmanifest")));
app.use(`/robots.txt`, express.static(path.join(scriptDir, "public/robots.txt")));
app.use(`/icon.png`, express.static(path.join(scriptDir, "public/icon.png")));
app.use(sessionParser);
app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`));
@@ -66,17 +68,17 @@ error_handlers.register(app);
await import("./services/sync.js");
// triggers backup timer
await import('./services/backup.js');
await import("./services/backup.js");
// trigger consistency checks timer
await import('./services/consistency_checks.js');
await import("./services/consistency_checks.js");
await import('./services/scheduler.js');
await import("./services/scheduler.js");
startScheduledCleanup();
if (utils.isElectron()) {
(await import('@electron/remote/main/index.js')).initialize();
(await import("@electron/remote/main/index.js")).initialize();
}
export default app;

View File

@@ -8,7 +8,7 @@ import BAttribute from "./entities/battribute.js";
import BBranch from "./entities/bbranch.js";
import BRevision from "./entities/brevision.js";
import BAttachment from "./entities/battachment.js";
import { AttachmentRow, BlobRow, RevisionRow } from './entities/rows.js';
import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js";
import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
@@ -55,13 +55,13 @@ export default class Becca {
}
getRoot() {
return this.getNote('root');
return this.getNote("root");
}
findAttributes(type: string, name: string): BAttribute[] {
name = name.trim().toLowerCase();
if (name.startsWith('#') || name.startsWith('~')) {
if (name.startsWith("#") || name.startsWith("~")) {
name = name.substr(1);
}
@@ -177,8 +177,7 @@ export default class Becca {
WHERE attachmentId = ? AND isDeleted = 0`
: `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [attachmentId])
.map(row => new BAttachment(row))[0];
return sql.getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentOrThrow(attachmentId: string, opts: AttachmentOpts = {}): BAttachment {
@@ -190,8 +189,7 @@ export default class Becca {
}
getAttachments(attachmentIds: string[]): BAttachment[] {
return sql.getManyRows<AttachmentRow>("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds)
.map(row => new BAttachment(row));
return sql.getManyRows<AttachmentRow>("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds).map((row) => new BAttachment(row));
}
getBlob(entity: { blobId?: string }): BBlob | null {
@@ -220,18 +218,13 @@ export default class Becca {
return null;
}
if (entityName === 'revisions') {
if (entityName === "revisions") {
return this.getRevision(entityId);
} else if (entityName === 'attachments') {
} else if (entityName === "attachments") {
return this.getAttachment(entityId);
}
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g,
group =>
group
.toUpperCase()
.replace('_', '')
);
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, (group) => group.toUpperCase().replace("_", ""));
if (!(camelCaseEntityName in this)) {
throw new Error(`Unknown entity name '${camelCaseEntityName}' (original argument '${entityName}')`);
@@ -242,12 +235,12 @@ export default class Becca {
getRecentNotesFromQuery(query: string, params: string[] = []): BRecentNote[] {
const rows = sql.getRows<BRecentNote>(query, params);
return rows.map(row => new BRecentNote(row));
return rows.map((row) => new BRecentNote(row));
}
getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] {
const rows = sql.getRows<RevisionRow>(query, params);
return rows.map(row => new BRevision(row));
return rows.map((row) => new BRevision(row));
}
/** Should be called when the set of all non-skeleton notes changes (added/removed) */

View File

@@ -11,7 +11,7 @@ import BOption from "./entities/boption.js";
import BEtapiToken from "./entities/betapi_token.js";
import cls from "../services/cls.js";
import entityConstructor from "../becca/entity_constructor.js";
import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from './entities/rows.js';
import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import ws from "../services/ws.js";
@@ -119,13 +119,13 @@ eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({ entityName, en
* It should be therefore treated as a row.
*/
function postProcessEntityUpdate(entityName: string, entityRow: any) {
if (entityName === 'notes') {
if (entityName === "notes") {
noteUpdated(entityRow);
} else if (entityName === 'branches') {
} else if (entityName === "branches") {
branchUpdated(entityRow);
} else if (entityName === 'attributes') {
} else if (entityName === "attributes") {
attributeUpdated(entityRow);
} else if (entityName === 'note_reordering') {
} else if (entityName === "note_reordering") {
noteReorderingUpdated(entityRow);
}
}
@@ -135,13 +135,13 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT
return;
}
if (entityName === 'notes') {
if (entityName === "notes") {
noteDeleted(entityId);
} else if (entityName === 'branches') {
} else if (entityName === "branches") {
branchDeleted(entityId);
} else if (entityName === 'attributes') {
} else if (entityName === "attributes") {
attributeDeleted(entityId);
} else if (entityName === 'etapi_tokens') {
} else if (entityName === "etapi_tokens") {
etapiTokenDeleted(entityId);
}
});
@@ -162,9 +162,8 @@ function branchDeleted(branchId: string) {
const childNote = becca.notes[branch.noteId];
if (childNote) {
childNote.parents = childNote.parents.filter(parent => parent.noteId !== branch.parentNoteId);
childNote.parentBranches = childNote.parentBranches
.filter(parentBranch => parentBranch.branchId !== branch.branchId);
childNote.parents = childNote.parents.filter((parent) => parent.noteId !== branch.parentNoteId);
childNote.parentBranches = childNote.parentBranches.filter((parentBranch) => parentBranch.branchId !== branch.branchId);
if (childNote.parents.length > 0) {
// subtree notes might lose some inherited attributes
@@ -175,7 +174,7 @@ function branchDeleted(branchId: string) {
const parentNote = becca.notes[branch.parentNoteId];
if (parentNote) {
parentNote.children = parentNote.children.filter(child => child.noteId !== branch.noteId);
parentNote.children = parentNote.children.filter((child) => child.noteId !== branch.noteId);
}
delete becca.childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`];
@@ -230,12 +229,12 @@ function attributeDeleted(attributeId: string) {
note.invalidateThisCache();
}
note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attribute.attributeId);
note.ownedAttributes = note.ownedAttributes.filter((attr) => attr.attributeId !== attribute.attributeId);
const targetNote = attribute.targetNote;
if (targetNote) {
targetNote.targetRelations = targetNote.targetRelations.filter(rel => rel.attributeId !== attribute.attributeId);
targetNote.targetRelations = targetNote.targetRelations.filter((rel) => rel.attributeId !== attribute.attributeId);
}
}
@@ -244,7 +243,7 @@ function attributeDeleted(attributeId: string) {
const key = `${attribute.type}-${attribute.name.toLowerCase()}`;
if (key in becca.attributeIndex) {
becca.attributeIndex[key] = becca.attributeIndex[key].filter(attr => attr.attributeId !== attribute.attributeId);
becca.attributeIndex[key] = becca.attributeIndex[key].filter((attr) => attr.attributeId !== attribute.attributeId);
}
}
@@ -282,8 +281,7 @@ function etapiTokenDeleted(etapiTokenId: string) {
eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
try {
becca.decryptProtectedNotes();
}
catch (e: any) {
} catch (e: any) {
log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`);
}
});

View File

@@ -37,7 +37,7 @@ function getNoteTitle(childNoteId: string, parentNoteId?: string) {
const branch = parentNote ? becca.getBranchFromChildAndParent(childNote.noteId, parentNote.noteId) : null;
return `${(branch && branch.prefix) ? `${branch.prefix} - ` : ''}${title}`;
return `${branch && branch.prefix ? `${branch.prefix} - ` : ""}${title}`;
}
function getNoteTitleArrayForPath(notePathArray: string[]) {
@@ -51,7 +51,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) {
const titles = [];
let parentNoteId = 'root';
let parentNoteId = "root";
let hoistedNotePassed = false;
// this is a notePath from outside of hoisted subtree, so the full title path needs to be returned
@@ -79,7 +79,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) {
function getNoteTitleForPath(notePathArray: string[]) {
const titles = getNoteTitleArrayForPath(notePathArray);
return titles.join(' / ');
return titles.join(" / ");
}
export default {

View File

@@ -9,7 +9,7 @@ import cls from "../../services/cls.js";
import log from "../../services/log.js";
import protectedSessionService from "../../services/protected_session.js";
import blobService from "../../services/blob.js";
import Becca, { ConstructorData } from '../becca-interface.js';
import Becca, { type ConstructorData } from "../becca-interface.js";
import becca from "../becca.js";
interface ContentOpts {
@@ -23,7 +23,6 @@ interface ContentOpts {
* @type T the same entity type needed for self-reference in {@link ConstructorData}.
*/
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
utcDateModified?: string;
dateCreated?: string;
dateModified?: string;
@@ -35,7 +34,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
blobId?: string;
protected beforeSaving(opts?: {}) {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
if (!(this as any)[constructorData.primaryKeyName]) {
(this as any)[constructorData.primaryKeyName] = utils.newEntityId();
}
@@ -50,19 +49,19 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
protected putEntityChange(isDeleted: boolean) {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
entityChangesService.putEntityChange({
entityName: constructorData.entityName,
entityId: (this as any)[constructorData.primaryKeyName],
hash: this.generateHash(isDeleted),
isErased: false,
utcDateChanged: this.getUtcDateChanged(),
isSynced: constructorData.entityName !== 'options' || !!this.isSynced
isSynced: constructorData.entityName !== "options" || !!this.isSynced
});
}
generateHash(isDeleted?: boolean): string {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
let contentToHash = "";
for (const propertyName of constructorData.hashedProperties) {
@@ -99,10 +98,10 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
/**
* Saves entity - executes SQL, but doesn't commit the transaction on its own
*/
* Saves entity - executes SQL, but doesn't commit the transaction on its own
*/
save(opts?: {}): this {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
const entityName = constructorData.entityName;
const primaryKeyName = constructorData.primaryKeyName;
@@ -115,7 +114,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
sql.transactional(() => {
sql.upsert(entityName, primaryKeyName, pojo);
if (entityName === 'recent_notes') {
if (entityName === "recent_notes") {
return;
}
@@ -144,7 +143,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
opts.forceFrontendReload = !!opts.forceFrontendReload;
if (content === null || content === undefined) {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
throw new Error(`Cannot set null content to ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}'`);
}
@@ -206,9 +205,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
if (this.isProtected) {
// a "random" prefix makes sure that the calculated hash/blobId is different for a decrypted/encrypted content
const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_";
return Buffer.isBuffer(unencryptedContent)
? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent])
: `${encryptedPrefixSuffix}${unencryptedContent}`;
return Buffer.isBuffer(unencryptedContent) ? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent]) : `${encryptedPrefixSuffix}${unencryptedContent}`;
} else {
return unencryptedContent;
}
@@ -216,13 +213,13 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) {
/*
* We're using the unencrypted blob for the hash calculation, because otherwise the random IV would
* cause every content blob to be unique which would balloon the database size (esp. with revisioning).
* This has minor security implications (it's easy to infer that given content is shared between different
* notes/attachments), but the trade-off comes out clearly positive.
*/
* We're using the unencrypted blob for the hash calculation, because otherwise the random IV would
* cause every content blob to be unique which would balloon the database size (esp. with revisioning).
* This has minor security implications (it's easy to infer that given content is shared between different
* notes/attachments), but the trade-off comes out clearly positive.
*/
const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation);
const blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
const blobNeedsInsert = !sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [newBlobId]);
if (!blobNeedsInsert) {
return newBlobId;
@@ -242,7 +239,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const hash = blobService.calculateContentHash(pojo);
entityChangesService.putEntityChange({
entityName: 'blobs',
entityName: "blobs",
entityId: newBlobId,
hash: hash,
isErased: false,
@@ -254,7 +251,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
});
eventService.emit(eventService.ENTITY_CHANGED, {
entityName: 'blobs',
entityName: "blobs",
entity: this
});
@@ -265,7 +262,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const row = sql.getRow<{ content: string | Buffer }>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
if (!row) {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
throw new Error(`Cannot find content for ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}', blobId '${this.blobId}'`);
}
@@ -273,26 +270,27 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
/**
* Mark the entity as (soft) deleted. It will be completely erased later.
*
* This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
*/
* Mark the entity as (soft) deleted. It will be completely erased later.
*
* This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
*/
markAsDeleted(deleteId: string | null = null) {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
const entityId = (this as any)[constructorData.primaryKeyName];
const entityName = constructorData.entityName;
this.utcDateModified = dateUtils.utcNowDateTime();
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
sql.execute(
`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
WHERE ${constructorData.primaryKeyName} = ?`,
[deleteId, this.utcDateModified, entityId]);
[deleteId, this.utcDateModified, entityId]
);
if (this.dateModified) {
this.dateModified = dateUtils.localNowDateTime();
sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`,
[this.dateModified, entityId]);
sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, [this.dateModified, entityId]);
}
log.info(`Marking ${entityName} ${entityId} as deleted`);
@@ -303,15 +301,17 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
markAsDeletedSimple() {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
const constructorData = this.constructor as unknown as ConstructorData<T>;
const entityId = (this as any)[constructorData.primaryKeyName];
const entityName = constructorData.entityName;
this.utcDateModified = dateUtils.utcNowDateTime();
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
sql.execute(
`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
WHERE ${constructorData.primaryKeyName} = ?`,
[this.utcDateModified, entityId]);
[this.utcDateModified, entityId]
);
log.info(`Marking ${entityName} ${entityId} as deleted`);

View File

@@ -6,14 +6,14 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
import protectedSessionService from "../../services/protected_session.js";
import log from "../../services/log.js";
import { AttachmentRow } from './rows.js';
import type { AttachmentRow } from "./rows.js";
import BNote from "./bnote.js";
import BBranch from "./bbranch.js";
import noteService from "../../services/notes.js";
const attachmentRoleToNoteTypeMapping = {
'image': 'image',
'file': 'file'
image: "image",
file: "file"
};
interface ContentOpts {
@@ -31,9 +31,15 @@ interface ContentOpts {
* larger amounts of data and generally not accessible to the user.
*/
class BAttachment extends AbstractBeccaEntity<BAttachment> {
static get entityName() { return "attachments"; }
static get primaryKeyName() { return "attachmentId"; }
static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; }
static get entityName() {
return "attachments";
}
static get primaryKeyName() {
return "attachmentId";
}
static get hashedProperties() {
return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"];
}
noteId?: number;
attachmentId?: string;
@@ -102,13 +108,15 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
}
isContentAvailable() {
return !this.attachmentId // new attachment which was not encrypted yet
|| !this.isProtected
|| protectedSessionService.isProtectedSessionAvailable()
return (
!this.attachmentId || // new attachment which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
getTitleOrProtected() {
return this.isContentAvailable() ? this.title : '[protected]';
return this.isContentAvailable() ? this.title : "[protected]";
}
decrypt() {
@@ -121,8 +129,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
try {
this.title = protectedSessionService.decryptString(this.title) || "";
this.isDecrypted = true;
}
catch (e: any) {
} catch (e: any) {
log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`);
}
}
@@ -136,22 +143,22 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
this._setContent(content, opts);
}
convertToNote(): { note: BNote, branch: BBranch } {
convertToNote(): { note: BNote; branch: BBranch } {
// TODO: can this ever be "search"?
if (this.type as string === 'search') {
if ((this.type as string) === "search") {
throw new Error(`Note of type search cannot have child notes`);
}
if (!this.getNote()) {
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " +
"Converting note revision's attachments to note is not (yet) supported.");
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + "Converting note revision's attachments to note is not (yet) supported.");
}
if (!(this.role in attachmentRoleToNoteTypeMapping)) {
throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`);
}
if (!this.isContentAvailable()) { // isProtected is the same for attachment
if (!this.isContentAvailable()) {
// isProtected is the same for attachment
throw new Error(`Cannot convert protected attachment outside of protected session`);
}
@@ -168,7 +175,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
const parentNote = this.getNote();
if (this.role === 'image' && parentNote.type === 'text') {
if (this.role === "image" && parentNote.type === "text") {
const origContent = parentNote.getContent();
if (typeof origContent !== "string") {
@@ -191,7 +198,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
}
getFileName() {
const type = this.role === 'image' ? 'image' : 'file';
const type = this.role === "image" ? "image" : "file";
return utils.formatDownloadTitle(this.title, type, this.mime);
}
@@ -200,9 +207,14 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
super.beforeSaving();
if (this.position === undefined || this.position === null) {
this.position = 10 + sql.getValue<number>(`SELECT COALESCE(MAX(position), 0)
this.position =
10 +
sql.getValue<number>(
`SELECT COALESCE(MAX(position), 0)
FROM attachments
WHERE ownerId = ?`, [this.noteId]);
WHERE ownerId = ?`,
[this.noteId]
);
}
this.dateModified = dateUtils.localNowDateTime();
@@ -234,8 +246,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
if (pojo.isProtected) {
if (this.isDecrypted) {
pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined;
}
else {
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}

View File

@@ -5,7 +5,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import { AttributeRow, AttributeType } from './rows.js';
import type { AttributeRow, AttributeType } from "./rows.js";
interface SavingOpts {
skipValidation?: boolean;
@@ -16,9 +16,15 @@ interface SavingOpts {
* and relation (representing named relationship between source and target note)
*/
class BAttribute extends AbstractBeccaEntity<BAttribute> {
static get entityName() { return "attributes"; }
static get primaryKeyName() { return "attributeId"; }
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable"]; }
static get entityName() {
return "attributes";
}
static get primaryKeyName() {
return "attributeId";
}
static get hashedProperties() {
return ["attributeId", "noteId", "type", "name", "value", "isInheritable"];
}
attributeId!: string;
noteId!: string;
@@ -40,16 +46,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
updateFromRow(row: AttributeRow) {
this.update([
row.attributeId,
row.noteId,
row.type,
row.name,
row.value,
row.isInheritable,
row.position,
row.utcDateModified
]);
this.update([row.attributeId, row.noteId, row.type, row.name, row.value, row.isInheritable, row.position, row.utcDateModified]);
}
update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) {
@@ -72,7 +69,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({noteId: this.noteId}));
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
this.becca.notes[this.noteId].ownedAttributes.push(this);
@@ -97,22 +94,22 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`);
}
if (this.type === 'relation' && !(this.value in this.becca.notes)) {
if (this.type === "relation" && !(this.value in this.becca.notes)) {
throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
}
}
get isAffectingSubtree() {
return this.isInheritable
|| (this.type === 'relation' && ['template', 'inherit'].includes(this.name));
return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name));
}
get targetNoteId() { // alias
return this.type === 'relation' ? this.value : undefined;
get targetNoteId() {
// alias
return this.type === "relation" ? this.value : undefined;
}
isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
get note() {
@@ -120,7 +117,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
get targetNote() {
if (this.type === 'relation') {
if (this.type === "relation") {
return this.becca.notes[this.value];
}
}
@@ -136,7 +133,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
getTargetNote() {
if (this.type !== 'relation') {
if (this.type !== "relation") {
throw new Error(`Attribute '${this.attributeId}' is not a relation.`);
}
@@ -148,7 +145,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
isDefinition() {
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:"));
}
getDefinition() {
@@ -156,9 +153,9 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
getDefinedName() {
if (this.type === 'label' && this.name.startsWith('label:')) {
if (this.type === "label" && this.name.startsWith("label:")) {
return this.name.substr(6);
} else if (this.type === 'label' && this.name.startsWith('relation:')) {
} else if (this.type === "label" && this.name.startsWith("relation:")) {
return this.name.substr(9);
} else {
return this.name;
@@ -182,7 +179,8 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}
if (this.position === undefined || this.position === null) {
const maxExistingPosition = this.getNote().getAttributes()
const maxExistingPosition = this.getNote()
.getAttributes()
.reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0);
this.position = maxExistingPosition + 10;

View File

@@ -1,11 +1,17 @@
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import { BlobRow } from "./rows.js";
import type { BlobRow } from "./rows.js";
// TODO: Why this does not extend the abstract becca?
class BBlob extends AbstractBeccaEntity<BBlob> {
static get entityName() { return "blobs"; }
static get primaryKeyName() { return "blobId"; }
static get hashedProperties() { return ["blobId", "content"]; }
static get entityName() {
return "blobs";
}
static get primaryKeyName() {
return "blobId";
}
static get hashedProperties() {
return ["blobId", "content"];
}
content!: string | Buffer;
contentLength!: number;

View File

@@ -7,7 +7,7 @@ import utils from "../../services/utils.js";
import TaskContext from "../../services/task_context.js";
import cls from "../../services/cls.js";
import log from "../../services/log.js";
import { BranchRow } from './rows.js';
import type { BranchRow } from "./rows.js";
import handlers from "../../services/handlers.js";
/**
@@ -18,10 +18,16 @@ import handlers from "../../services/handlers.js";
* Always check noteId instead.
*/
class BBranch extends AbstractBeccaEntity<BBranch> {
static get entityName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
static get entityName() {
return "branches";
}
static get primaryKeyName() {
return "branchId";
}
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "prefix"]; }
static get hashedProperties() {
return ["branchId", "noteId", "parentNoteId", "prefix"];
}
branchId?: string;
noteId!: string;
@@ -42,15 +48,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
}
updateFromRow(row: BranchRow) {
this.update([
row.branchId,
row.noteId,
row.parentNoteId,
row.prefix,
row.notePosition,
row.isExpanded,
row.utcDateModified
]);
this.update([row.branchId, row.noteId, row.parentNoteId, row.prefix, row.notePosition, row.isExpanded, row.utcDateModified]);
}
update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) {
@@ -78,7 +76,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
childNote.parentBranches.push(this);
}
if (this.noteId === 'root') {
if (this.noteId === "root") {
return;
}
@@ -97,7 +95,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
get childNote(): BNote {
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({noteId: this.noteId}));
this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
}
return this.becca.notes[this.noteId];
@@ -109,43 +107,43 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
/** @returns root branch will have undefined parent, all other branches have to have a parent note */
get parentNote(): BNote | undefined {
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== 'none') {
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== "none") {
// entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.parentNoteId, new BNote({noteId: this.parentNoteId}));
this.becca.addNote(this.parentNoteId, new BNote({ noteId: this.parentNoteId }));
}
return this.becca.notes[this.parentNoteId];
}
get isDeleted() {
return (this.branchId == undefined || !(this.branchId in this.becca.branches));
return this.branchId == undefined || !(this.branchId in this.becca.branches);
}
/**
* Branch is weak when its existence should not hinder deletion of its note.
* As a result, note with only weak branches should be immediately deleted.
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
* of deletion should not act as a clone.
*/
* Branch is weak when its existence should not hinder deletion of its note.
* As a result, note with only weak branches should be immediately deleted.
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
* of deletion should not act as a clone.
*/
get isWeak() {
return ['_share', '_lbBookmarks'].includes(this.parentNoteId);
return ["_share", "_lbBookmarks"].includes(this.parentNoteId);
}
/**
* Delete a branch. If this is a last note's branch, delete the note as well.
*
* @param deleteId - optional delete identified
*
* @returns true if note has been deleted, false otherwise
*/
* Delete a branch. If this is a last note's branch, delete the note as well.
*
* @param deleteId - optional delete identified
*
* @returns true if note has been deleted, false otherwise
*/
deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean {
if (!deleteId) {
deleteId = utils.randomString(10);
}
if (!taskContext) {
taskContext = new TaskContext('no-progress-reporting');
taskContext = new TaskContext("no-progress-reporting");
}
taskContext.increaseProgressCount();
@@ -157,13 +155,11 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
if (parentBranches.length === 1 && parentBranches[0] === this) {
// needs to be run before branches and attributes are deleted and thus attached relations disappear
handlers.runAttachedRelations(note, 'runOnNoteDeletion', note);
handlers.runAttachedRelations(note, "runOnNoteDeletion", note);
}
}
if (this.noteId === 'root'
|| this.noteId === cls.getHoistedNoteId()) {
if (this.noteId === "root" || this.noteId === cls.getHoistedNoteId()) {
throw new Error("Can't delete root or hoisted branch/note");
}
@@ -203,8 +199,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
note.markAsDeleted(deleteId);
return true;
}
else {
} else {
return false;
}
}
@@ -225,8 +220,9 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
continue;
}
if (maxNotePos < childBranch.notePosition
&& childBranch.noteId !== '_hidden' // hidden has a very large notePosition to always stay last
if (
maxNotePos < childBranch.notePosition &&
childBranch.noteId !== "_hidden" // hidden has a very large notePosition to always stay last
) {
maxNotePos = childBranch.notePosition;
}

View File

@@ -1,6 +1,6 @@
"use strict";
import { EtapiTokenRow } from "./rows.js";
import type { EtapiTokenRow } from "./rows.js";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
@@ -15,9 +15,15 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
* from tokenHash and token.
*/
class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
static get entityName() { return "etapi_tokens"; }
static get primaryKeyName() { return "etapiTokenId"; }
static get hashedProperties() { return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"]; }
static get entityName() {
return "etapi_tokens";
}
static get primaryKeyName() {
return "etapiTokenId";
}
static get hashedProperties() {
return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"];
}
etapiTokenId?: string;
name!: string;
@@ -66,7 +72,7 @@ class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
isDeleted: this.isDeleted
}
};
}
beforeSaving() {

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,21 @@
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import { OptionRow } from './rows.js';
import type { OptionRow } from "./rows.js";
/**
* Option represents a name-value pair, either directly configurable by the user or some system property.
*/
class BOption extends AbstractBeccaEntity<BOption> {
static get entityName() { return "options"; }
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
static get entityName() {
return "options";
}
static get primaryKeyName() {
return "name";
}
static get hashedProperties() {
return ["name", "value"];
}
name!: string;
value!: string;
@@ -43,7 +49,7 @@ class BOption extends AbstractBeccaEntity<BOption> {
value: this.value,
isSynced: this.isSynced,
utcDateModified: this.utcDateModified
}
};
}
}

View File

@@ -1,6 +1,6 @@
"use strict";
import { RecentNoteRow } from "./rows.js";
import type { RecentNoteRow } from "./rows.js";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
@@ -9,9 +9,15 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
* RecentNote represents recently visited note.
*/
class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
static get entityName() { return "recent_notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "notePath"]; }
static get entityName() {
return "recent_notes";
}
static get primaryKeyName() {
return "noteId";
}
static get hashedProperties() {
return ["noteId", "notePath"];
}
noteId!: string;
notePath!: string;
@@ -33,7 +39,7 @@ class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
noteId: this.noteId,
notePath: this.notePath,
utcDateCreated: this.utcDateCreated
}
};
}
}

View File

@@ -7,7 +7,7 @@ import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js";
import BAttachment from "./battachment.js";
import { AttachmentRow, RevisionRow } from './rows.js';
import type { AttachmentRow, RevisionRow } from "./rows.js";
import eraseService from "../../services/erase.js";
interface ContentOpts {
@@ -24,10 +24,15 @@ interface GetByIdOpts {
* It's used for seamless note versioning.
*/
class BRevision extends AbstractBeccaEntity<BRevision> {
static get entityName() { return "revisions"; }
static get primaryKeyName() { return "revisionId"; }
static get hashedProperties() { return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated",
"utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"]; }
static get entityName() {
return "revisions";
}
static get primaryKeyName() {
return "revisionId";
}
static get hashedProperties() {
return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
}
revisionId?: string;
noteId!: string;
@@ -75,25 +80,27 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
}
isContentAvailable() {
return !this.revisionId // new note which was not encrypted yet
|| !this.isProtected
|| protectedSessionService.isProtectedSessionAvailable()
return (
!this.revisionId || // new note which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
}
/*
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Revision entity with its own sync. The reason behind this hybrid design is that
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
* if we don't need a content, especially for bulk operations like search.
*
* This is the same approach as is used for Note's content.
*/
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Revision entity with its own sync. The reason behind this hybrid design is that
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
* if we don't need a content, especially for bulk operations like search.
*
* This is the same approach as is used for Note's content.
*/
getContent(): string | Buffer {
return this._getContent();
}
/**
* @throws Error in case of invalid JSON */
* @throws Error in case of invalid JSON */
getJsonContent(): {} | null {
const content = this.getContent();
@@ -108,8 +115,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
getJsonContentSafely(): {} | null {
try {
return this.getJsonContent();
}
catch (e) {
} catch (e) {
return null;
}
}
@@ -119,12 +125,16 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
}
getAttachments(): BAttachment[] {
return sql.getRows<AttachmentRow>(`
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND isDeleted = 0`, [this.revisionId])
.map(row => new BAttachment(row));
AND isDeleted = 0`,
[this.revisionId]
)
.map((row) => new BAttachment(row));
}
getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null {
@@ -137,29 +147,32 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId])
.map(row => new BAttachment(row))[0];
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentsByRole(role: string): BAttachment[] {
return sql.getRows<AttachmentRow>(`
return sql
.getRows<AttachmentRow>(
`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
ORDER BY position`, [this.revisionId, role])
.map(row => new BAttachment(row));
ORDER BY position`,
[this.revisionId, role]
)
.map((row) => new BAttachment(row));
}
getAttachmentByTitle(title: string): BAttachment {
// cannot use SQL to filter by title since it can be encrypted
return this.getAttachments().filter(attachment => attachment.title === title)[0];
return this.getAttachments().filter((attachment) => attachment.title === title)[0];
}
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
if (this.revisionId) {
eraseService.eraseRevisions([this.revisionId]);
@@ -199,8 +212,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
if (pojo.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
pojo.title = protectedSessionService.encrypt(this.title) || undefined;
}
else {
} else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}

View File

@@ -100,8 +100,25 @@ export interface BranchRow {
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export const ALLOWED_NOTE_TYPES = [ "file", "image", "search", "noteMap", "launcher", "doc", "contentWidget", "text", "relationMap", "render", "canvas", "mermaid", "book", "webView", "code", "mindMap" ] as const;
export type NoteType = typeof ALLOWED_NOTE_TYPES[number];
export const ALLOWED_NOTE_TYPES = [
"file",
"image",
"search",
"noteMap",
"launcher",
"doc",
"contentWidget",
"text",
"relationMap",
"render",
"canvas",
"mermaid",
"book",
"webView",
"code",
"mindMap"
] as const;
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
export interface NoteRow {
noteId: string;

View File

@@ -1,4 +1,4 @@
import { ConstructorData } from './becca-interface.js';
import type { ConstructorData } from "./becca-interface.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import BAttachment from "./entities/battachment.js";
import BAttribute from "./entities/battribute.js";
@@ -13,15 +13,15 @@ import BRevision from "./entities/brevision.js";
type EntityClass = new (row?: any) => AbstractBeccaEntity<any>;
const ENTITY_NAME_TO_ENTITY: Record<string, ConstructorData<any> & EntityClass> = {
"attachments": BAttachment,
"attributes": BAttribute,
"blobs": BBlob,
"branches": BBranch,
"etapi_tokens": BEtapiToken,
"notes": BNote,
"options": BOption,
"recent_notes": BRecentNote,
"revisions": BRevision
attachments: BAttachment,
attributes: BAttribute,
blobs: BBlob,
branches: BBranch,
etapi_tokens: BEtapiToken,
notes: BNote,
options: BOption,
recent_notes: BRecentNote,
revisions: BRevision
};
function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) {

View File

@@ -7,11 +7,7 @@ import BNote from "./entities/bnote.js";
const DEBUG = false;
const IGNORED_ATTRS = [
"datenote",
"monthnote",
"yearnote"
];
const IGNORED_ATTRS = ["datenote", "monthnote", "yearnote"];
const IGNORED_ATTR_NAMES = [
"includenotelink",
@@ -30,7 +26,7 @@ const IGNORED_ATTR_NAMES = [
"similarnoteswidgetdisabled",
"disableinclusion",
"rendernote",
"pageurl",
"pageurl"
];
interface DateLimits {
@@ -42,9 +38,9 @@ interface DateLimits {
function filterUrlValue(value: string) {
return value
.replace(/https?:\/\//ig, "")
.replace(/www.js\./ig, "")
.replace(/(\.net|\.com|\.org|\.info|\.edu)/ig, "");
.replace(/https?:\/\//gi, "")
.replace(/www.js\./gi, "")
.replace(/(\.net|\.com|\.org|\.info|\.edu)/gi, "");
}
function buildRewardMap(note: BNote) {
@@ -61,8 +57,7 @@ function buildRewardMap(note: BNote) {
const currentReward = map.get(word) || 0;
// reward grows with the length of matched string
const length = word.length
- 0.9; // to penalize specifically very short words - 1 and 2 characters
const length = word.length - 0.9; // to penalize specifically very short words - 1 and 2 characters
map.set(word, currentReward + rewardFactor * Math.pow(length, 0.7));
}
@@ -70,7 +65,7 @@ function buildRewardMap(note: BNote) {
}
for (const ancestorNote of note.getAncestors()) {
if (ancestorNote.noteId === 'root') {
if (ancestorNote.noteId === "root") {
continue;
}
@@ -94,9 +89,7 @@ function buildRewardMap(note: BNote) {
}
for (const attr of note.getAttributes()) {
if (attr.name.startsWith('child:')
|| attr.name.startsWith('relation:')
|| attr.name.startsWith('label:')) {
if (attr.name.startsWith("child:") || attr.name.startsWith("relation:") || attr.name.startsWith("label:")) {
continue;
}
@@ -111,13 +104,13 @@ function buildRewardMap(note: BNote) {
addToRewardMap(attr.name, reward);
}
if (attr.name === 'cliptype') {
if (attr.name === "cliptype") {
reward /= 2;
}
let value = attr.value;
if (value.startsWith('http')) {
if (value.startsWith("http")) {
value = filterUrlValue(value);
// words in URLs are not that valuable
@@ -127,7 +120,7 @@ function buildRewardMap(note: BNote) {
addToRewardMap(value, reward);
}
if (note.type === 'text' && note.isDecrypted) {
if (note.type === "text" && note.isDecrypted) {
const content = note.getContent();
const dom = new JSDOM(content);
@@ -135,7 +128,7 @@ function buildRewardMap(note: BNote) {
for (const el of dom.window.document.querySelectorAll(elName)) {
addToRewardMap(el.textContent, rewardFactor);
}
}
};
// the title is the top with weight 1 so smaller headings will have lower weight
@@ -154,12 +147,12 @@ function buildRewardMap(note: BNote) {
const mimeCache: Record<string, string> = {};
function trimMime(mime: string) {
if (!mime || mime === 'text/html') {
if (!mime || mime === "text/html") {
return;
}
if (!(mime in mimeCache)) {
const chunks = mime.split('/');
const chunks = mime.split("/");
let str = "";
@@ -167,7 +160,7 @@ function trimMime(mime: string) {
// we're not interested in 'text/' or 'application/' prefix
str = chunks[1];
if (str.startsWith('-x')) {
if (str.startsWith("-x")) {
str = str.substr(2);
}
}
@@ -185,7 +178,7 @@ function buildDateLimits(baseNote: BNote): DateLimits {
minDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 3600 * 1000)),
minExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 5 * 1000)),
maxExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 5 * 1000)),
maxDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 3600 * 1000)),
maxDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 3600 * 1000))
};
}
@@ -193,9 +186,34 @@ function buildDateLimits(baseNote: BNote): DateLimits {
const wordCache = new Map();
const WORD_BLACKLIST = [
"a", "the", "in", "for", "from", "but", "s", "so", "if", "while", "until",
"whether", "after", "before", "because", "since", "when", "where", "how",
"than", "then", "and", "either", "or", "neither", "nor", "both", "also"
"a",
"the",
"in",
"for",
"from",
"but",
"s",
"so",
"if",
"while",
"until",
"whether",
"after",
"before",
"because",
"since",
"when",
"where",
"how",
"than",
"then",
"and",
"either",
"or",
"neither",
"nor",
"both",
"also"
];
function splitToWords(text: string) {
@@ -212,8 +230,7 @@ function splitToWords(text: string) {
// special case for english plurals
else if (words[idx].length > 2 && words[idx].endsWith("es")) {
words[idx] = words[idx].substr(0, words[idx] - 2);
}
else if (words[idx].length > 1 && words[idx].endsWith("s")) {
} else if (words[idx].length > 1 && words[idx].endsWith("s")) {
words[idx] = words[idx].substr(0, words[idx] - 1);
}
}
@@ -227,9 +244,7 @@ function splitToWords(text: string) {
* that it doesn't actually need to be shown to the user.
*/
function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) {
return sourceNote.getAttributes().find(attr => attr.type === 'relation'
&& ['includenotelink', 'imagelink'].includes(attr.name)
&& attr.value === targetNote.noteId);
return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
}
async function findSimilarNotes(noteId: string) {
@@ -246,14 +261,13 @@ async function findSimilarNotes(noteId: string) {
try {
dateLimits = buildDateLimits(baseNote);
}
catch (e: any) {
} catch (e: any) {
throw new Error(`Date limits failed with ${e.message}, entity: ${JSON.stringify(baseNote.getPojo())}`);
}
const rewardMap = buildRewardMap(baseNote);
let ancestorRewardCache: Record<string, number> = {};
const ancestorNoteIds = new Set(baseNote.getAncestors().map(note => note.noteId));
const ancestorNoteIds = new Set(baseNote.getAncestors().map((note) => note.noteId));
ancestorNoteIds.add(baseNote.noteId);
let displayRewards = false;
@@ -270,7 +284,7 @@ async function findSimilarNotes(noteId: string) {
const lengthPenalization = 1 / Math.pow(text.length, 0.3);
for (const word of splitToWords(text)) {
const reward = (rewardMap.get(word) * factor * lengthPenalization) || 0;
const reward = rewardMap.get(word) * factor * lengthPenalization || 0;
if (displayRewards && reward > 0) {
console.log(`Reward ${Math.round(reward * 10) / 10} for word: ${word}`);
@@ -294,7 +308,6 @@ async function findSimilarNotes(noteId: string) {
for (const parentNote of note.parents) {
if (!ancestorNoteIds.has(parentNote.noteId)) {
if (displayRewards) {
console.log("Considering", parentNote.title);
}
@@ -304,8 +317,7 @@ async function findSimilarNotes(noteId: string) {
}
for (const branch of parentNote.getParentBranches()) {
score += gatherRewards(branch.prefix, 0.3)
+ gatherAncestorRewards(branch.parentNote);
score += gatherRewards(branch.prefix, 0.3) + gatherAncestorRewards(branch.parentNote);
}
}
}
@@ -317,8 +329,7 @@ async function findSimilarNotes(noteId: string) {
}
function computeScore(candidateNote: BNote) {
let score = gatherRewards(trimMime(candidateNote.mime))
+ gatherAncestorRewards(candidateNote);
let score = gatherRewards(trimMime(candidateNote.mime)) + gatherAncestorRewards(candidateNote);
if (candidateNote.isDecrypted) {
score += gatherRewards(candidateNote.title);
@@ -329,9 +340,7 @@ async function findSimilarNotes(noteId: string) {
}
for (const attr of candidateNote.getAttributes()) {
if (attr.name.startsWith('child:')
|| attr.name.startsWith('relation:')
|| attr.name.startsWith('label:')) {
if (attr.name.startsWith("child:") || attr.name.startsWith("relation:") || attr.name.startsWith("label:")) {
continue;
}
@@ -349,8 +358,7 @@ async function findSimilarNotes(noteId: string) {
if (!value.startsWith) {
log.info(`Unexpected falsy value for attribute ${JSON.stringify(attr.getPojo())}`);
continue;
}
else if (value.startsWith('http')) {
} else if (value.startsWith("http")) {
value = filterUrlValue(value);
// words in URLs are not that valuable
@@ -369,13 +377,13 @@ async function findSimilarNotes(noteId: string) {
}
/**
* We want to improve the standing of notes which have been created in similar time to each other since
* there's a good chance they are related.
*
* But there's an exception - if they were created really close to each other (within few seconds) then
* they are probably part of the import and not created by hand - these OTOH should not benefit.
*/
const {utcDateCreated} = candidateNote;
* We want to improve the standing of notes which have been created in similar time to each other since
* there's a good chance they are related.
*
* But there's an exception - if they were created really close to each other (within few seconds) then
* they are probably part of the import and not created by hand - these OTOH should not benefit.
*/
const { utcDateCreated } = candidateNote;
if (utcDateCreated < dateLimits.minExcludedDate || utcDateCreated > dateLimits.maxExcludedDate) {
if (utcDateCreated >= dateLimits.minDate && utcDateCreated <= dateLimits.maxDate) {
@@ -384,9 +392,7 @@ async function findSimilarNotes(noteId: string) {
}
score += 1;
}
else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10)
|| utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) {
} else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10) || utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) {
if (displayRewards) {
console.log("Adding reward for same day of creation");
}
@@ -400,9 +406,7 @@ async function findSimilarNotes(noteId: string) {
}
for (const candidateNote of Object.values(becca.notes)) {
if (candidateNote.noteId === baseNote.noteId
|| hasConnectingRelation(candidateNote, baseNote)
|| hasConnectingRelation(baseNote, candidateNote)) {
if (candidateNote.noteId === baseNote.noteId || hasConnectingRelation(candidateNote, baseNote) || hasConnectingRelation(baseNote, candidateNote)) {
continue;
}
@@ -420,7 +424,7 @@ async function findSimilarNotes(noteId: string) {
score -= 0.5; // archived penalization
}
results.push({score, notePath, noteId: candidateNote.noteId});
results.push({ score, notePath, noteId: candidateNote.noteId });
}
i++;
@@ -430,13 +434,13 @@ async function findSimilarNotes(noteId: string) {
}
}
results.sort((a, b) => a.score > b.score ? -1 : 1);
results.sort((a, b) => (a.score > b.score ? -1 : 1));
if (DEBUG) {
console.log("REWARD MAP", rewardMap);
if (results.length >= 1) {
for (const {noteId} of results) {
for (const { noteId } of results) {
const note = becca.notes[noteId];
displayRewards = true;

View File

@@ -1,9 +1,9 @@
import { Router } from 'express';
import { Router } from "express";
import appInfo from "../services/app_info.js";
import eu from "./etapi_utils.js";
function register(router: Router) {
eu.route(router, 'get', '/etapi/app-info', (req, res, next) => {
eu.route(router, "get", "/etapi/app-info", (req, res, next) => {
res.status(200).json(appInfo);
});
}

View File

@@ -3,21 +3,21 @@ import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import v from "./validators.js";
import utils from "../services/utils.js";
import { Router } from 'express';
import { AttachmentRow } from '../becca/entities/rows.js';
import { ValidatorMap } from './etapi-interface.js';
import { Router } from "express";
import type { AttachmentRow } from "../becca/entities/rows.js";
import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) {
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = {
'ownerId': [v.notNull, v.isNoteId],
'role': [v.notNull, v.isString],
'mime': [v.notNull, v.isString],
'title': [v.notNull, v.isString],
'position': [v.notNull, v.isInteger],
'content': [v.isString],
ownerId: [v.notNull, v.isNoteId],
role: [v.notNull, v.isString],
mime: [v.notNull, v.isString],
title: [v.notNull, v.isString],
position: [v.notNull, v.isInteger],
content: [v.isString]
};
eu.route(router, 'post', '/etapi/attachments', (req, res, next) => {
eu.route(router, "post", "/etapi/attachments", (req, res, next) => {
const _params: Partial<AttachmentRow> = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT);
const params = _params as AttachmentRow;
@@ -30,26 +30,25 @@ function register(router: Router) {
const attachment = note.saveAttachment(params);
res.status(201).json(mappers.mapAttachmentToPojo(attachment));
}
catch (e: any) {
} catch (e: any) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
}
});
eu.route(router, 'get', '/etapi/attachments/:attachmentId', (req, res, next) => {
eu.route(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
res.json(mappers.mapAttachmentToPojo(attachment));
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'role': [v.notNull, v.isString],
'mime': [v.notNull, v.isString],
'title': [v.notNull, v.isString],
'position': [v.notNull, v.isInteger],
role: [v.notNull, v.isString],
mime: [v.notNull, v.isString],
title: [v.notNull, v.isString],
position: [v.notNull, v.isInteger]
};
eu.route(router, 'patch', '/etapi/attachments/:attachmentId', (req, res, next) => {
eu.route(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -62,7 +61,7 @@ function register(router: Router) {
res.json(mappers.mapAttachmentToPojo(attachment));
});
eu.route(router, 'get', '/etapi/attachments/:attachmentId/content', (req, res, next) => {
eu.route(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -71,15 +70,15 @@ function register(router: Router) {
const filename = utils.formatDownloadTitle(attachment.title, attachment.role, attachment.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', attachment.mime);
res.setHeader("Content-Type", attachment.mime);
res.send(attachment.getContent());
});
eu.route(router, 'put', '/etapi/attachments/:attachmentId/content', (req, res, next) => {
eu.route(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -91,7 +90,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, 'delete', '/etapi/attachments/:attachmentId', (req, res, next) => {
eu.route(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {

View File

@@ -3,29 +3,29 @@ import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import attributeService from "../services/attributes.js";
import v from "./validators.js";
import { Router } from 'express';
import { AttributeRow } from '../becca/entities/rows.js';
import { ValidatorMap } from './etapi-interface.js';
import { Router } from "express";
import type { AttributeRow } from "../becca/entities/rows.js";
import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) {
eu.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => {
eu.route(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute));
});
const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE: ValidatorMap = {
'attributeId': [v.mandatory, v.notNull, v.isValidEntityId],
'noteId': [v.mandatory, v.notNull, v.isNoteId],
'type': [v.mandatory, v.notNull, v.isAttributeType],
'name': [v.mandatory, v.notNull, v.isString],
'value': [v.notNull, v.isString],
'isInheritable': [v.notNull, v.isBoolean],
'position': [v.notNull, v.isInteger]
attributeId: [v.mandatory, v.notNull, v.isValidEntityId],
noteId: [v.mandatory, v.notNull, v.isNoteId],
type: [v.mandatory, v.notNull, v.isAttributeType],
name: [v.mandatory, v.notNull, v.isString],
value: [v.notNull, v.isString],
isInheritable: [v.notNull, v.isBoolean],
position: [v.notNull, v.isInteger]
};
eu.route(router, 'post', '/etapi/attributes', (req, res, next) => {
if (req.body.type === 'relation') {
eu.route(router, "post", "/etapi/attributes", (req, res, next) => {
if (req.body.type === "relation") {
eu.getAndCheckNote(req.body.value);
}
@@ -37,27 +37,26 @@ function register(router: Router) {
const attr = attributeService.createAttribute(params);
res.status(201).json(mappers.mapAttributeToPojo(attr));
}
catch (e: any) {
} catch (e: any) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = {
'value': [v.notNull, v.isString],
'position': [v.notNull, v.isInteger]
value: [v.notNull, v.isString],
position: [v.notNull, v.isInteger]
};
const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = {
'position': [v.notNull, v.isInteger]
position: [v.notNull, v.isInteger]
};
eu.route(router, 'patch', '/etapi/attributes/:attributeId', (req, res, next) => {
eu.route(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
if (attribute.type === 'label') {
if (attribute.type === "label") {
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL);
} else if (attribute.type === 'relation') {
} else if (attribute.type === "relation") {
eu.getAndCheckNote(req.body.value);
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION);
@@ -68,7 +67,7 @@ function register(router: Router) {
res.json(mappers.mapAttributeToPojo(attribute));
});
eu.route(router, 'delete', '/etapi/attributes/:attributeId', (req, res, next) => {
eu.route(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute) {

View File

@@ -2,24 +2,24 @@ import becca from "../becca/becca.js";
import eu from "./etapi_utils.js";
import passwordEncryptionService from "../services/encryption/password_encryption.js";
import etapiTokenService from "../services/etapi_tokens.js";
import { RequestHandler, Router } from 'express';
import type { RequestHandler, Router } from "express";
function register(router: Router, loginMiddleware: RequestHandler[]) {
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', loginMiddleware, (req, res, next) => {
const {password, tokenName} = req.body;
eu.NOT_AUTHENTICATED_ROUTE(router, "post", "/etapi/auth/login", loginMiddleware, (req, res, next) => {
const { password, tokenName } = req.body;
if (!passwordEncryptionService.verifyPassword(password)) {
throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password.");
}
const {authToken} = etapiTokenService.createToken(tokenName || "ETAPI login");
const { authToken } = etapiTokenService.createToken(tokenName || "ETAPI login");
res.status(201).json({
authToken
});
});
eu.route(router, 'post', '/etapi/auth/logout', (req, res, next) => {
eu.route(router, "post", "/etapi/auth/logout", (req, res, next) => {
const parsed = etapiTokenService.parseAuthToken(req.headers.authorization);
if (!parsed || !parsed.etapiTokenId) {
@@ -41,4 +41,4 @@ function register(router: Router, loginMiddleware: RequestHandler[]) {
export default {
register
}
};

View File

@@ -4,7 +4,7 @@ import eu from "./etapi_utils.js";
import backupService from "../services/backup.js";
function register(router: Router) {
eu.route(router, 'put', '/etapi/backup/:backupName', async (req, res, next) => {
eu.route(router, "put", "/etapi/backup/:backupName", async (req, res, next) => {
await backupService.backupNow(req.params.backupName);
res.sendStatus(204);

View File

@@ -6,24 +6,24 @@ import mappers from "./mappers.js";
import BBranch from "../becca/entities/bbranch.js";
import entityChangesService from "../services/entity_changes.js";
import v from "./validators.js";
import { BranchRow } from "../becca/entities/rows.js";
import type { BranchRow } from "../becca/entities/rows.js";
function register(router: Router) {
eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
eu.route(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
res.json(mappers.mapBranchToPojo(branch));
});
const ALLOWED_PROPERTIES_FOR_CREATE_BRANCH = {
'noteId': [v.mandatory, v.notNull, v.isNoteId],
'parentNoteId': [v.mandatory, v.notNull, v.isNoteId],
'notePosition': [v.notNull, v.isInteger],
'prefix': [v.isString],
'isExpanded': [v.notNull, v.isBoolean]
noteId: [v.mandatory, v.notNull, v.isNoteId],
parentNoteId: [v.mandatory, v.notNull, v.isNoteId],
notePosition: [v.notNull, v.isInteger],
prefix: [v.isString],
isExpanded: [v.notNull, v.isBoolean]
};
eu.route(router, 'post', '/etapi/branches', (req, res, next) => {
eu.route(router, "post", "/etapi/branches", (req, res, next) => {
const _params = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH);
const params: BranchRow = _params as BranchRow;
@@ -49,12 +49,12 @@ function register(router: Router) {
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'notePosition': [v.notNull, v.isInteger],
'prefix': [v.isString],
'isExpanded': [v.notNull, v.isBoolean]
notePosition: [v.notNull, v.isInteger],
prefix: [v.isString],
isExpanded: [v.notNull, v.isBoolean]
};
eu.route(router, 'patch', '/etapi/branches/:branchId', (req, res, next) => {
eu.route(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
@@ -63,7 +63,7 @@ function register(router: Router) {
res.json(mappers.mapBranchToPojo(branch));
});
eu.route(router, 'delete', '/etapi/branches/:branchId', (req, res, next) => {
eu.route(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch) {
@@ -75,7 +75,7 @@ function register(router: Router) {
res.sendStatus(204);
});
eu.route(router, 'post', '/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
eu.route(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");

View File

@@ -1,3 +1,3 @@
export type ValidatorFunc = (obj: unknown) => (string | undefined);
export type ValidatorFunc = (obj: unknown) => string | undefined;
export type ValidatorMap = Record<string, ValidatorFunc[]>;

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,9 @@ import log from "../services/log.js";
import becca from "../becca/becca.js";
import etapiTokenService from "../services/etapi_tokens.js";
import config from "../services/config.js";
import { NextFunction, Request, RequestHandler, Response, Router } from 'express';
import { ValidatorMap } from './etapi-interface.js';
import { ApiRequestHandler } from "../routes/routes.js";
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import type { ValidatorMap } from "./etapi-interface.js";
import type { ApiRequestHandler } from "../routes/routes.js";
const GENERIC_CODE = "GENERIC";
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
@@ -30,20 +30,21 @@ class EtapiError extends Error {
function sendError(res: Response, statusCode: number, code: string, message: string) {
return res
.set('Content-Type', 'application/json')
.set("Content-Type", "application/json")
.status(statusCode)
.send(JSON.stringify({
"status": statusCode,
"code": code,
"message": message
}));
.send(
JSON.stringify({
status: statusCode,
code: code,
message: message
})
);
}
function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
}
else {
} else {
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
}
}
@@ -54,8 +55,8 @@ function processRequest(req: Request, res: Response, routeHandler: ApiRequestHan
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.set('componentId', "etapi");
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set("componentId", "etapi");
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
const cb = () => routeHandler(req, res, next);
@@ -85,19 +86,17 @@ function getAndCheckNote(noteId: string) {
if (note) {
return note;
}
else {
} else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
}
function getAndCheckAttachment(attachmentId: string) {
const attachment = becca.getAttachment(attachmentId, {includeContentLength: true});
const attachment = becca.getAttachment(attachmentId, { includeContentLength: true });
if (attachment) {
return attachment;
}
else {
} else {
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
}
@@ -107,8 +106,7 @@ function getAndCheckBranch(branchId: string) {
if (branch) {
return branch;
}
else {
} else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
}
@@ -118,8 +116,7 @@ function getAndCheckAttribute(attributeId: string) {
if (attribute) {
return attribute;
}
else {
} else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
}
@@ -128,8 +125,7 @@ function validateAndPatch(target: any, source: any, allowedProperties: Validator
for (const key of Object.keys(source)) {
if (!(key in allowedProperties)) {
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
}
else {
} else {
for (const validator of allowedProperties[key]) {
const validationResult = validator(source[key]);
@@ -157,4 +153,4 @@ export default {
getAndCheckBranch,
getAndCheckAttribute,
getAndCheckAttachment
}
};

View File

@@ -15,11 +15,11 @@ function mapNoteToPojo(note: BNote) {
dateModified: note.dateModified,
utcDateCreated: note.utcDateCreated,
utcDateModified: note.utcDateModified,
parentNoteIds: note.getParentNotes().map(p => p.noteId),
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
parentBranchIds: note.getParentBranches().map(p => p.branchId),
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
parentNoteIds: note.getParentNotes().map((p) => p.noteId),
childNoteIds: note.getChildNotes().map((ch) => ch.noteId),
parentBranchIds: note.getParentBranches().map((p) => p.branchId),
childBranchIds: note.getChildBranches().map((ch) => ch.branchId),
attributes: note.getAttributes().map((attr) => mapAttributeToPojo(attr))
};
}

View File

@@ -9,28 +9,28 @@ import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import zipExportService from "../services/export/zip.js";
import zipImportService from "../services/import/zip.js";
import { Request, Router } from 'express';
import { ParsedQs } from 'qs';
import { NoteParams } from '../services/note-interface.js';
import { SearchParams } from '../services/search/services/types.js';
import { ValidatorMap } from './etapi-interface.js';
import { type Request, Router } from "express";
import type { ParsedQs } from "qs";
import type { NoteParams } from "../services/note-interface.js";
import type { SearchParams } from "../services/search/services/types.js";
import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) {
eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
eu.route(router, "get", "/etapi/notes", (req, res, next) => {
const { search } = req.query;
if (typeof search !== "string" || !search?.trim()) {
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory.");
throw new eu.EtapiError(400, "SEARCH_QUERY_PARAM_MANDATORY", "'search' query parameter is mandatory.");
}
const searchParams = parseSearchParams(req);
const searchContext = new SearchContext(searchParams);
const searchResults = searchService.findResultsWithQuery(search, searchContext);
const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]);
const foundNotes = searchResults.map((sr) => becca.notes[sr.noteId]);
const resp: any = {
results: foundNotes.map(note => mappers.mapNoteToPojo(note)),
results: foundNotes.map((note) => mappers.mapNoteToPojo(note))
};
if (searchContext.debugInfo) {
@@ -40,27 +40,27 @@ function register(router: Router) {
res.json(resp);
});
eu.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
eu.route(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
res.json(mappers.mapNoteToPojo(note));
});
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = {
'parentNoteId': [v.mandatory, v.notNull, v.isNoteId],
'title': [v.mandatory, v.notNull, v.isString],
'type': [v.mandatory, v.notNull, v.isNoteType],
'mime': [v.notNull, v.isString],
'content': [v.notNull, v.isString],
'notePosition': [v.notNull, v.isInteger],
'prefix': [v.notNull, v.isString],
'isExpanded': [v.notNull, v.isBoolean],
'noteId': [v.notNull, v.isValidEntityId],
'dateCreated': [v.notNull, v.isString, v.isLocalDateTime],
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
parentNoteId: [v.mandatory, v.notNull, v.isNoteId],
title: [v.mandatory, v.notNull, v.isString],
type: [v.mandatory, v.notNull, v.isNoteType],
mime: [v.notNull, v.isString],
content: [v.notNull, v.isString],
notePosition: [v.notNull, v.isInteger],
prefix: [v.notNull, v.isString],
isExpanded: [v.notNull, v.isBoolean],
noteId: [v.notNull, v.isValidEntityId],
dateCreated: [v.notNull, v.isString, v.isLocalDateTime],
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
};
eu.route(router, 'post', '/etapi/create-note', (req, res, next) => {
eu.route(router, "post", "/etapi/create-note", (req, res, next) => {
const _params = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
const params = _params as NoteParams;
@@ -72,21 +72,20 @@ function register(router: Router) {
note: mappers.mapNoteToPojo(resp.note),
branch: mappers.mapBranchToPojo(resp.branch)
});
}
catch (e: any) {
} catch (e: any) {
return eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'title': [v.notNull, v.isString],
'type': [v.notNull, v.isString],
'mime': [v.notNull, v.isString],
'dateCreated': [v.notNull, v.isString, v.isLocalDateTime],
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
title: [v.notNull, v.isString],
type: [v.notNull, v.isString],
mime: [v.notNull, v.isString],
dateCreated: [v.notNull, v.isString, v.isLocalDateTime],
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
};
eu.route(router, 'patch', '/etapi/notes/:noteId', (req, res, next) => {
eu.route(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -99,7 +98,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'delete', '/etapi/notes/:noteId', (req, res, next) => {
eu.route(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
const { noteId } = req.params;
const note = becca.getNote(noteId);
@@ -108,12 +107,12 @@ function register(router: Router) {
return res.sendStatus(204);
}
note.deleteNote(null, new TaskContext('no-progress-reporting'));
note.deleteNote(null, new TaskContext("no-progress-reporting"));
res.sendStatus(204);
});
eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
eu.route(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -122,15 +121,15 @@ function register(router: Router) {
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.setHeader("Content-Type", note.mime);
res.send(note.getContent());
});
eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
eu.route(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -144,7 +143,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, 'get', '/etapi/notes/:noteId/export', (req, res, next) => {
eu.route(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const format = req.query.format || "html";
@@ -152,7 +151,7 @@ function register(router: Router) {
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
}
const taskContext = new TaskContext('no-progress-reporting');
const taskContext = new TaskContext("no-progress-reporting");
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
@@ -161,19 +160,19 @@ function register(router: Router) {
zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res);
});
eu.route(router, 'post', '/etapi/notes/:noteId/import', (req, res, next) => {
eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext('no-progress-reporting');
const taskContext = new TaskContext("no-progress-reporting");
zipImportService.importZip(taskContext, req.body, note).then(importedNote => {
zipImportService.importZip(taskContext, req.body, note).then((importedNote) => {
res.status(201).json({
note: mappers.mapNoteToPojo(importedNote),
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0]),
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0])
});
}); // we need better error handling here, async errors won't be properly processed.
});
eu.route(router, 'post', '/etapi/notes/:noteId/revision', (req, res, next) => {
eu.route(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.saveRevision();
@@ -181,27 +180,25 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => {
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments({ includeContentLength: true })
const attachments = note.getAttachments({ includeContentLength: true });
res.json(
attachments.map(attachment => mappers.mapAttachmentToPojo(attachment))
);
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
});
}
function parseSearchParams(req: Request) {
const rawSearchParams: SearchParams = {
fastSearch: parseBoolean(req.query, 'fastSearch'),
includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'),
ancestorNoteId: parseString(req.query['ancestorNoteId']),
ancestorDepth: parseString(req.query['ancestorDepth']), // e.g. "eq5"
orderBy: parseString(req.query['orderBy']),
fastSearch: parseBoolean(req.query, "fastSearch"),
includeArchivedNotes: parseBoolean(req.query, "includeArchivedNotes"),
ancestorNoteId: parseString(req.query["ancestorNoteId"]),
ancestorDepth: parseString(req.query["ancestorDepth"]), // e.g. "eq5"
orderBy: parseString(req.query["orderBy"]),
// TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
orderDirection: parseOrderDirection(req.query, 'orderDirection') as unknown as string,
limit: parseInteger(req.query, 'limit'),
debug: parseBoolean(req.query, 'debug')
orderDirection: parseOrderDirection(req.query, "orderDirection") as unknown as string,
limit: parseInteger(req.query, "limit"),
debug: parseBoolean(req.query, "debug")
};
const searchParams: SearchParams = {};
@@ -230,11 +227,11 @@ function parseBoolean(obj: any, name: string) {
return undefined;
}
if (!['true', 'false'].includes(obj[name])) {
if (!["true", "false"].includes(obj[name])) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`);
}
return obj[name] === 'true';
return obj[name] === "true";
}
function parseOrderDirection(obj: any, name: string) {
@@ -244,7 +241,7 @@ function parseOrderDirection(obj: any, name: string) {
const integer = parseInt(obj[name]);
if (!['asc', 'desc'].includes(obj[name])) {
if (!["asc", "desc"].includes(obj[name])) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`);
}

View File

@@ -4,16 +4,16 @@ import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'etapi.openapi.yaml');
const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "etapi.openapi.yaml");
let spec: string | null = null;
function register(router: Router) {
router.get('/etapi/etapi.openapi.yaml', (req, res, next) => {
router.get("/etapi/etapi.openapi.yaml", (req, res, next) => {
if (!spec) {
spec = fs.readFileSync(specPath, 'utf8');
spec = fs.readFileSync(specPath, "utf8");
}
res.header('Content-Type', 'text/plain'); // so that it displays in browser
res.header("Content-Type", "text/plain"); // so that it displays in browser
res.status(200).send(spec);
});
}

View File

@@ -2,10 +2,10 @@ import specialNotesService from "../services/special_notes.js";
import dateNotesService from "../services/date_notes.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import { Router } from 'express';
import { Router } from "express";
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getMonthInvalidError = (month: string)=> new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
function isValidDate(date: string) {
@@ -17,7 +17,7 @@ function isValidDate(date: string) {
}
function register(router: Router) {
eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -28,7 +28,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/days/:date', (req, res, next) => {
eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -39,7 +39,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => {
eu.route(router, "get", "/etapi/calendar/weeks/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -50,7 +50,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => {
eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => {
const { month } = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
@@ -61,7 +61,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => {
eu.route(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
const { year } = req.params;
if (!/[0-9]{4}/.test(year)) {

View File

@@ -19,7 +19,7 @@ function isString(obj: unknown) {
return;
}
if (typeof obj !== 'string') {
if (typeof obj !== "string") {
return `'${obj}' is not a string`;
}
}
@@ -45,7 +45,7 @@ function isBoolean(obj: unknown) {
return;
}
if (typeof obj !== 'boolean') {
if (typeof obj !== "boolean") {
return `'${obj}' is not a boolean`;
}
}
@@ -65,7 +65,7 @@ function isNoteId(obj: unknown) {
return;
}
if (typeof obj !== 'string') {
if (typeof obj !== "string") {
return `'${obj}' is not a valid noteId`;
}
@@ -91,7 +91,7 @@ function isAttributeType(obj: unknown) {
return;
}
if (typeof obj !== "string" || !['label', 'relation'].includes(obj)) {
if (typeof obj !== "string" || !["label", "relation"].includes(obj)) {
return `'${obj}' is not a valid attribute type, allowed types are: label, relation`;
}
}
@@ -101,7 +101,7 @@ function isValidEntityId(obj: unknown) {
return;
}
if (typeof obj !== 'string' || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) {
if (typeof obj !== "string" || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) {
return `'${obj}' is not a valid entityId. Only alphanumeric characters are allowed of length 4 to 32.`;
}
}

6
src/express.d.ts vendored
View File

@@ -4,18 +4,18 @@ export declare module "express-serve-static-core" {
interface Request {
session: Session & {
loggedIn: boolean;
},
};
headers: {
"x-local-date"?: string;
"x-labels"?: string;
"authorization"?: string;
authorization?: string;
"trilium-cred"?: string;
"x-csrf-token"?: string;
"trilium-component-id"?: string;
"trilium-local-now-datetime"?: string;
"trilium-hoisted-note-id"?: string;
}
};
}
}

View File

@@ -1,28 +1,28 @@
import froca from "../services/froca.js";
import bundleService from "../services/bundle.js";
import RootCommandExecutor from "./root_command_executor.js";
import Entrypoints, { SqlExecuteResults } from "./entrypoints.js";
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import zoomComponent from "./zoom.js";
import TabManager from "./tab_manager.js";
import Component from "./component.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import linkService, { ViewScope } from "../services/link.js";
import MobileScreenSwitcherExecutor, { Screen } from "./mobile_screen_switcher.js";
import linkService, { type ViewScope } from "../services/link.js";
import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js";
import ShortcutComponent from "./shortcut_component.js";
import { t, initLocale } from "../services/i18n.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
import { Node } from "../services/tree.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
import type { Node } from "../services/tree.js";
import LoadResults from "../services/load_results.js";
import { Attribute } from "../services/attribute_parser.js";
import type { Attribute } from "../services/attribute_parser.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteContext, { GetTextEditorCallback } from "./note_context.js";
import NoteContext, { type GetTextEditorCallback } from "./note_context.js";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@@ -98,7 +98,7 @@ export type CommandMappings = {
showInfoDialog: ConfirmWithMessageOptions;
showConfirmDialog: ConfirmWithMessageOptions;
openNewNoteSplit: NoteCommandData;
openInWindow: NoteCommandData,
openInWindow: NoteCommandData;
openNoteInNewTab: CommandData;
openNoteInNewSplit: CommandData;
openNoteInNewWindow: CommandData;
@@ -139,11 +139,12 @@ export type CommandMappings = {
resetLauncher: ContextMenuCommandData;
executeInActiveNoteDetailWidget: CommandData & {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void
};
executeWithTextEditor: CommandData & ExecuteCommandData & {
callback?: GetTextEditorCallback;
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
};
executeWithTextEditor: CommandData &
ExecuteCommandData & {
callback?: GetTextEditorCallback;
};
executeWithCodeEditor: CommandData & ExecuteCommandData;
executeWithContentElement: CommandData & ExecuteCommandData;
executeWithTypeWidget: CommandData & ExecuteCommandData;
@@ -177,8 +178,21 @@ export type CommandMappings = {
/** Sets the active {@link Screen} (e.g. to toggle the tree sidebar). It triggers the {@link EventMappings.activeScreenChanged} event, but only if the provided <em>screen</em> is different than the current one. */
setActiveScreen: CommandData & {
screen: Screen;
};
closeTab: CommandData;
closeOtherTabs: CommandData;
closeRightTabs: CommandData;
closeAllTabs: CommandData;
reopenLastTab: CommandData;
moveTabToNewWindow: CommandData;
copyTabToNewWindow: CommandData;
closeActiveTab: CommandData & {
$el: JQuery<HTMLElement>
},
setZoomFactorAndSave: {
zoomFactor: string;
}
}
};
type EventMappings = {
initialRenderComplete: {};
@@ -195,57 +209,74 @@ type EventMappings = {
messages: string[];
};
entitiesReloaded: {
loadResults: LoadResults
loadResults: LoadResults;
};
addNewLabel: CommandData;
addNewRelation: CommandData;
sqlQueryResults: CommandData & {
results: SqlExecuteResults;
},
};
readOnlyTemporarilyDisabled: {
noteContext: NoteContext
},
noteContext: NoteContext;
};
/** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */
activeScreenChanged: {
activeScreen: Screen;
},
};
activeContextChanged: {
noteContext: NoteContext;
},
};
noteSwitched: {
noteContext: NoteContext;
notePath: string;
},
};
noteSwitchedAndActivatedEvent: {
noteContext: NoteContext;
notePath: string;
},
};
setNoteContext: {
noteContext: NoteContext;
},
};
noteTypeMimeChangedEvent: {
noteId: string;
},
};
reEvaluateHighlightsListWidgetVisibility: {
noteId: string | undefined;
},
};
showHighlightsListWidget: {
noteId: string;
};
hoistedNoteChanged: {
ntxId: string;
};
contextsReopenedEvent: {
mainNtxId: string;
tabPosition: number;
};
noteContextReorderEvent: {
oldMainNtxId: string;
newMainNtxId: string;
};
newNoteContextCreated: {
noteContext: NoteContext;
};
noteContextRemovedEvent: {
ntxIds: string[];
}
}
};
export type EventListener<T extends EventNames> = {
[key in T as `${key}Event`]: (data: EventData<T>) => void
}
[key in T as `${key}Event`]: (data: EventData<T>) => void;
};
export type CommandListener<T extends CommandNames> = {
[key in T as `${key}Command`]: (data: CommandListenerData<T>) => void
}
[key in T as `${key}Command`]: (data: CommandListenerData<T>) => void;
};
export type CommandListenerData<T extends CommandNames> = CommandMappings[T];
export type EventData<T extends EventNames> = EventMappings[T];
type CommandAndEventMappings = (CommandMappings & EventMappings);
type CommandAndEventMappings = CommandMappings & EventMappings;
/**
* This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}.
@@ -253,7 +284,7 @@ type CommandAndEventMappings = (CommandMappings & EventMappings);
export type CommandNames = keyof CommandMappings;
type EventNames = keyof EventMappings;
type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never; }[keyof T];
type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never }[keyof T];
/**
* Generic which filters {@link CommandNames} to provide only those commands that take in as data the desired implementation of {@link CommandData}. Mostly useful for contextual menu, to enforce consistency in the commands.
@@ -261,7 +292,6 @@ type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
class AppContext extends Component {
isMainWindow: boolean;
components: Component[];
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
@@ -304,13 +334,7 @@ class AppContext extends Component {
initComponents() {
this.tabManager = new TabManager();
this.components = [
this.tabManager,
new RootCommandExecutor(),
new Entrypoints(),
new MainTreeExecutors(),
new ShortcutComponent()
];
this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()];
if (utils.isMobile()) {
this.components.push(new MobileScreenSwitcherExecutor());
@@ -337,21 +361,21 @@ class AppContext extends Component {
$("body").append($renderedWidget);
$renderedWidget.on('click', "[data-trigger-command]", function() {
$renderedWidget.on("click", "[data-trigger-command]", function () {
if ($(this).hasClass("disabled")) {
return;
}
const commandName = $(this).attr('data-trigger-command');
const commandName = $(this).attr("data-trigger-command");
const $component = $(this).closest(".component");
const component = $component.prop("component");
component.triggerCommand(commandName, {$el: $(this)});
component.triggerCommand(commandName, { $el: $(this) });
});
this.child(rootWidget);
this.triggerEvent('initialRenderComplete');
this.triggerEvent("initialRenderComplete");
}
// TODO: Remove ignore once all commands are mapped out.
@@ -378,7 +402,7 @@ class AppContext extends Component {
}
getComponentByEl(el: HTMLElement) {
return $(el).closest(".component").prop('component');
return $(el).closest(".component").prop("component");
}
addBeforeUnloadListener(obj: BeforeUploadListener) {
@@ -394,10 +418,10 @@ class AppContext extends Component {
const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed
$(window).on('beforeunload', () => {
$(window).on("beforeunload", () => {
let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
for (const weakRef of appContext.beforeUnloadListeners) {
const component = weakRef.deref();
@@ -420,8 +444,8 @@ $(window).on('beforeunload', () => {
}
});
$(window).on('hashchange', function() {
const {notePath, ntxId, viewScope} = linkService.parseNavigationStateFromUrl(window.location.href);
$(window).on("hashchange", function () {
const { notePath, ntxId, viewScope } = linkService.parseNavigationStateFromUrl(window.location.href);
if (notePath || ntxId) {
appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope);

View File

@@ -1,5 +1,5 @@
import utils from '../services/utils.js';
import { CommandMappings, CommandNames } from './app_context.js';
import utils from "../services/utils.js";
import type { CommandMappings, CommandNames } from "./app_context.js";
/**
* Abstract class for all components in the Trilium's frontend.
@@ -28,7 +28,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
get sanitizedClassName() {
// webpack mangles names and sometimes uses unsafe characters
return this.constructor.name.replace(/[^A-Z0-9]/ig, "_");
return this.constructor.name.replace(/[^A-Z0-9]/gi, "_");
}
setParent(parent: TypedComponent<any>) {
@@ -48,18 +48,13 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
handleEvent(name: string, data: unknown): Promise<unknown> | null {
try {
const callMethodPromise = this.initialized
? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data))
: this.callMethod((this as any)[`${name}Event`], data);
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
const childrenPromise = this.handleEventInChildren(name, data);
// don't create promises if not needed (optimization)
return callMethodPromise && childrenPromise
? Promise.all([callMethodPromise, childrenPromise])
: (callMethodPromise || childrenPromise);
}
catch (e: any) {
return callMethodPromise && childrenPromise ? Promise.all([callMethodPromise, childrenPromise]) : callMethodPromise || childrenPromise;
} catch (e: any) {
console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error ${e.message} ${e.stack}`);
return null;
@@ -101,7 +96,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
}
callMethod(fun: (arg: unknown) => Promise<unknown>, data: unknown) {
if (typeof fun !== 'function') {
if (typeof fun !== "function") {
return;
}
@@ -111,7 +106,8 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
const took = Date.now() - startTime;
if (glob.isDev && took > 20) { // measuring only sync handlers
if (glob.isDev && took > 20) {
// measuring only sync handlers
console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`);
}

View File

@@ -1,8 +1,8 @@
import utils from "../services/utils.js";
import dateNoteService from "../services/date_notes.js";
import protectedSessionHolder from '../services/protected_session_holder.js';
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import appContext, { NoteCommandData } from "./app_context.js";
import appContext, { type NoteCommandData } from "./app_context.js";
import Component from "./component.js";
import toastService from "../services/toast.js";
import ws from "../services/ws.js";
@@ -41,7 +41,7 @@ export default class Entrypoints extends Component {
openDevToolsCommand() {
if (utils.isElectron()) {
utils.dynamicRequire('@electron/remote').getCurrentWindow().toggleDevTools();
utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
}
}
@@ -52,20 +52,20 @@ export default class Entrypoints extends Component {
return;
}
const {note} = await server.post<CreateChildrenResponse>(`notes/${inboxNote.noteId}/children?target=into`, {
content: '',
type: 'text',
const { note } = await server.post<CreateChildrenResponse>(`notes/${inboxNote.noteId}/children?target=into`, {
content: "",
type: "text",
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
});
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, {activate: true});
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, { activate: true });
appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true});
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
}
async toggleNoteHoistingCommand({noteId = appContext.tabManager.getActiveContextNoteId()}) {
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
if (!noteId) {
return;
}
@@ -75,12 +75,12 @@ export default class Entrypoints extends Component {
if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) {
await activeNoteContext.unhoist();
} else if (noteToHoist?.type !== 'search') {
} else if (noteToHoist?.type !== "search") {
await activeNoteContext.setHoistedNoteId(noteId);
}
}
async hoistNoteCommand({noteId}: { noteId: string }) {
async hoistNoteCommand({ noteId }: { noteId: string }) {
const noteContext = appContext.tabManager.getActiveContext();
if (noteContext.hoistedNoteId !== noteId) {
@@ -102,7 +102,7 @@ export default class Entrypoints extends Component {
toggleFullscreenCommand() {
if (utils.isElectron()) {
const win = utils.dynamicRequire('@electron/remote').getCurrentWindow();
const win = utils.dynamicRequire("@electron/remote").getCurrentWindow();
if (win.isFullScreenable()) {
win.setFullScreen(!win.isFullScreen());
@@ -115,22 +115,20 @@ export default class Entrypoints extends Component {
}
logoutCommand() {
const $logoutForm = $('<form action="logout" method="POST">')
.append($(`<input type='_hidden' name="_csrf" value="${glob.csrfToken}"/>`));
const $logoutForm = $('<form action="logout" method="POST">').append($(`<input type='_hidden' name="_csrf" value="${glob.csrfToken}"/>`));
$("body").append($logoutForm);
$logoutForm.trigger('submit');
$logoutForm.trigger("submit");
}
backInNoteHistoryCommand() {
if (utils.isElectron()) {
// standard JS version does not work completely correctly in electron
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
webContents.goToIndex(activeIndex - 1);
}
else {
} else {
window.history.back();
}
}
@@ -138,52 +136,50 @@ export default class Entrypoints extends Component {
forwardInNoteHistoryCommand() {
if (utils.isElectron()) {
// standard JS version does not work completely correctly in electron
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
webContents.goToIndex(activeIndex + 1);
}
else {
} else {
window.history.forward();
}
}
async switchToDesktopVersionCommand() {
utils.setCookie('trilium-device', 'desktop');
utils.setCookie("trilium-device", "desktop");
utils.reloadFrontendApp("Switching to desktop version");
}
async switchToMobileVersionCommand() {
utils.setCookie('trilium-device', 'mobile');
utils.setCookie("trilium-device", "mobile");
utils.reloadFrontendApp("Switching to mobile version");
}
async openInWindowCommand({notePath, hoistedNoteId, viewScope}: NoteCommandData) {
const extraWindowHash = linkService.calculateHash({notePath, hoistedNoteId, viewScope});
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
if (utils.isElectron()) {
const {ipcRenderer} = utils.dynamicRequire('electron');
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send('create-extra-window', { extraWindowHash });
}
else {
ipcRenderer.send("create-extra-window", { extraWindowHash });
} else {
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
window.open(url, '', 'width=1000,height=800');
window.open(url, "", "width=1000,height=800");
}
}
async openNewWindowCommand() {
this.openInWindowCommand({notePath: '', hoistedNoteId: 'root'});
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
}
async runActiveNoteCommand() {
const {ntxId, note} = appContext.tabManager.getActiveContext();
const { ntxId, note } = appContext.tabManager.getActiveContext();
// ctrl+enter is also used elsewhere, so make sure we're running only when appropriate
if (!note || note.type !== 'code') {
if (!note || note.type !== "code") {
return;
}
@@ -192,14 +188,14 @@ export default class Entrypoints extends Component {
await bundleService.getAndExecuteBundle(note.noteId);
} else if (note.mime.endsWith("env=backend")) {
await server.post(`script/run/${note.noteId}`);
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
} else if (note.mime === "text/x-sqlite;schema=trilium") {
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
if (!resp.success) {
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
}
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results });
}
toastService.showMessage(t("entrypoints.note-executed"));

View File

@@ -1,5 +1,5 @@
import { MenuCommandItem } from "../menus/context_menu.js";
import { CommandNames } from "./app_context.js";
import type { MenuCommandItem } from "../menus/context_menu.js";
import type { CommandNames } from "./app_context.js";
type ListenerReturnType = void | Promise<void>;

View File

@@ -19,9 +19,9 @@ export default class MainTreeExecutors extends Component {
return;
}
const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map(node => node.data.noteId);
const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.noteId);
this.triggerCommand('cloneNoteIdsTo', {noteIds: selectedOrActiveNoteIds});
this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds });
}
async moveNotesToCommand() {
@@ -29,9 +29,9 @@ export default class MainTreeExecutors extends Component {
return;
}
const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map(node => node.data.branchId);
const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.branchId);
this.triggerCommand('moveBranchIdsTo', {branchIds: selectedOrActiveBranchIds});
this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds });
}
async createNoteIntoCommand() {
@@ -61,12 +61,12 @@ export default class MainTreeExecutors extends Component {
const parentNotePath = treeService.getNotePath(node.getParent());
const isProtected = treeService.getParentProtectedStatus(node);
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
if (node.data.noteId === "root" || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
return;
}
await noteCreateService.createNote(parentNotePath, {
target: 'after',
target: "after",
targetBranchId: node.data.branchId,
isProtected: isProtected,
saveSelection: false

View File

@@ -3,15 +3,13 @@ import type { CommandListener, CommandListenerData } from "./app_context.js";
export type Screen = "detail" | "tree";
export default class MobileScreenSwitcherExecutor extends Component
implements CommandListener<"setActiveScreen">
{
export default class MobileScreenSwitcherExecutor extends Component implements CommandListener<"setActiveScreen"> {
private activeScreen?: Screen;
setActiveScreenCommand({screen}: CommandListenerData<"setActiveScreen">) {
setActiveScreenCommand({ screen }: CommandListenerData<"setActiveScreen">) {
if (screen !== this.activeScreen) {
this.activeScreen = screen;
this.triggerEvent('activeScreenChanged', {activeScreen: screen});
this.triggerEvent("activeScreenChanged", { activeScreen: screen });
}
}
}

View File

@@ -1,13 +1,13 @@
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import appContext, { EventData, EventListener } from "./app_context.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import treeService from "../services/tree.js";
import Component from "./component.js";
import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js";
import { ViewScope } from "../services/link.js";
import type { ViewScope } from "../services/link.js";
import FNote from "../entities/fnote.js";
interface SetNoteOpts {
@@ -17,20 +17,17 @@ interface SetNoteOpts {
export type GetTextEditorCallback = () => void;
class NoteContext extends Component
implements EventListener<"entitiesReloaded">
{
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
ntxId: string | null;
hoistedNoteId: string;
private mainNtxId: string | null;
mainNtxId: string | null;
notePath?: string | null;
private noteId?: string | null;
noteId?: string | null;
private parentNoteId?: string | null;
viewScope?: ViewScope;
constructor(ntxId: string | null = null, hoistedNoteId: string = 'root', mainNtxId: string | null = null) {
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
super();
this.ntxId = ntxId || NoteContext.generateNtxId();
@@ -50,7 +47,7 @@ class NoteContext extends Component
this.parentNoteId = null;
// hoisted note is kept intentionally
this.triggerEvent('noteSwitched', {
this.triggerEvent("noteSwitched", {
noteContext: this,
notePath: this.notePath
});
@@ -81,20 +78,20 @@ class NoteContext extends Component
return;
}
await this.triggerEvent('beforeNoteSwitch', {noteContext: this});
await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
utils.closeActiveDialog();
this.notePath = resolvedNotePath;
this.viewScope = opts.viewScope;
({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
this.saveToRecentNotes(resolvedNotePath);
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
if (opts.triggerSwitchEvent) {
await this.triggerEvent('noteSwitched', {
await this.triggerEvent("noteSwitched", {
noteContext: this,
notePath: this.notePath
});
@@ -103,23 +100,20 @@ class NoteContext extends Component
await this.setHoistedNoteIfNeeded();
if (utils.isMobile()) {
this.triggerCommand('setActiveScreen', {screen: 'detail'});
this.triggerCommand("setActiveScreen", { screen: "detail" });
}
}
async setHoistedNoteIfNeeded() {
if (this.hoistedNoteId === 'root'
&& this.notePath?.startsWith("root/_hidden")
&& !this.note?.isLabelTruthy("keepCurrentHoisting")
) {
if (this.hoistedNoteId === "root" && 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';
let hoistedNoteId = "_hidden";
if (this.note?.isLaunchBarConfig()) {
hoistedNoteId = '_lbRoot';
hoistedNoteId = "_lbRoot";
} else if (this.note?.isOptions()) {
hoistedNoteId = '_options';
hoistedNoteId = "_options";
}
await this.setHoistedNoteId(hoistedNoteId);
@@ -127,7 +121,7 @@ class NoteContext extends Component
}
getSubContexts() {
return appContext.tabManager.noteContexts.filter(nc => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId);
return appContext.tabManager.noteContexts.filter((nc) => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId);
}
/**
@@ -152,13 +146,11 @@ class NoteContext extends Component
if (this.mainNtxId) {
try {
return appContext.tabManager.getNoteContextById(this.mainNtxId);
}
catch (e) {
} catch (e) {
this.mainNtxId = null;
return this;
}
}
else {
} else {
return this;
}
}
@@ -167,7 +159,7 @@ class NoteContext extends Component
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', {
await server.post("recent-notes", {
noteId: this.note?.noteId,
notePath: this.notePath
});
@@ -183,7 +175,7 @@ class NoteContext extends Component
return;
}
if (await hoistedNoteService.checkNoteAccess(resolvedNotePath, this) === false) {
if ((await hoistedNoteService.checkNoteAccess(resolvedNotePath, this)) === false) {
return; // note is outside of hoisted subtree and user chose not to unhoist
}
@@ -200,7 +192,7 @@ class NoteContext extends Component
/** @returns {string[]} */
get notePathArray() {
return this.notePath ? this.notePath.split('/') : [];
return this.notePath ? this.notePath.split("/") : [];
}
isActive() {
@@ -208,7 +200,7 @@ class NoteContext extends Component
}
getPojoState() {
if (this.hoistedNoteId !== 'root') {
if (this.hoistedNoteId !== "root") {
// keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config)
if (!this.notePath && this.getSubContexts().length === 0) {
@@ -223,11 +215,11 @@ class NoteContext extends Component
hoistedNoteId: this.hoistedNoteId,
active: this.isActive(),
viewScope: this.viewScope
}
};
}
async unhoist() {
await this.setHoistedNoteId('root');
await this.setHoistedNoteId("root");
}
async setHoistedNoteId(noteIdToHoist: string) {
@@ -241,7 +233,7 @@ class NoteContext extends Component
await this.setNote(noteIdToHoist);
}
await this.triggerEvent('hoistedNoteChanged', {
await this.triggerEvent("hoistedNoteChanged", {
noteId: noteIdToHoist,
ntxId: this.ntxId
});
@@ -254,15 +246,15 @@ class NoteContext extends Component
}
// "readOnly" is a state valid only for text/code notes
if (!this.note || (this.note.type !== 'text' && this.note.type !== 'code')) {
if (!this.note || (this.note.type !== "text" && this.note.type !== "code")) {
return false;
}
if (this.note.isLabelTruthy('readOnly')) {
if (this.note.isLabelTruthy("readOnly")) {
return true;
}
if (this.viewScope?.viewMode === 'source') {
if (this.viewScope?.viewMode === "source") {
return true;
}
@@ -271,24 +263,20 @@ class NoteContext extends Component
return false;
}
const sizeLimit = this.note.type === 'text'
? options.getInt('autoReadonlySizeText')
: options.getInt('autoReadonlySizeCode');
const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode");
return sizeLimit
&& blob.contentLength > sizeLimit
&& !this.note.isLabelTruthy('autoReadOnlyDisabled');
return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled");
}
async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
const noteRow = loadResults.getEntityRow('notes', this.noteId);
const noteRow = loadResults.getEntityRow("notes", this.noteId);
if (noteRow.isDeleted) {
this.noteId = null;
this.notePath = null;
this.triggerEvent('noteSwitched', {
this.triggerEvent("noteSwitched", {
noteContext: this,
notePath: this.notePath
});
@@ -297,48 +285,63 @@ class NoteContext extends Component
}
hasNoteList() {
return this.note
&& 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');
return (
this.note &&
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?: GetTextEditorCallback) {
return this.timeout<TextEditor>(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', {
callback,
resolve,
ntxId: this.ntxId
})));
return this.timeout<TextEditor>(
new Promise((resolve) =>
appContext.triggerCommand("executeWithTextEditor", {
callback,
resolve,
ntxId: this.ntxId
})
)
);
}
async getCodeEditor() {
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithCodeEditor', {
resolve,
ntxId: this.ntxId
})));
return this.timeout(
new Promise((resolve) =>
appContext.triggerCommand("executeWithCodeEditor", {
resolve,
ntxId: this.ntxId
})
)
);
}
async getContentElement() {
return this.timeout<JQuery<HTMLElement>>(new Promise(resolve => appContext.triggerCommand('executeWithContentElement', {
resolve,
ntxId: this.ntxId
})));
return this.timeout<JQuery<HTMLElement>>(
new Promise((resolve) =>
appContext.triggerCommand("executeWithContentElement", {
resolve,
ntxId: this.ntxId
})
)
);
}
async getTypeWidget() {
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTypeWidget', {
resolve,
ntxId: this.ntxId
})));
return this.timeout(
new Promise((resolve) =>
appContext.triggerCommand("executeWithTypeWidget", {
resolve,
ntxId: this.ntxId
})
)
);
}
timeout<T>(promise: Promise<T | null>) {
return Promise.race([
promise,
new Promise(res => setTimeout(() => res(null), 200))
]) as Promise<T>;
return Promise.race([promise, new Promise((res) => setTimeout(() => res(null), 200))]) as Promise<T>;
}
resetViewScope() {
@@ -355,9 +358,7 @@ class NoteContext extends Component
const { note, viewScope } = this;
let title = viewScope?.viewMode === 'default'
? note.title
: `${note.title}: ${viewScope?.viewMode}`;
let title = viewScope?.viewMode === "default" ? note.title : `${note.title}: ${viewScope?.viewMode}`;
if (viewScope?.attachmentId) {
// assuming the attachment has been already loaded

View File

@@ -1,5 +1,5 @@
import Component from "./component.js";
import appContext, { CommandData, CommandListenerData } from "./app_context.js";
import appContext, { type CommandData, type CommandListenerData } from "./app_context.js";
import dateNoteService from "../services/date_notes.js";
import treeService from "../services/tree.js";
import openService from "../services/open.js";
@@ -25,11 +25,11 @@ export default class RootCommandExecutor extends Component {
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(sqlConsoleNote.noteId, { activate: true });
appContext.triggerEvent('focusOnDetail', {ntxId: noteContext.ntxId});
appContext.triggerEvent("focusOnDetail", { ntxId: noteContext.ntxId });
}
async searchNotesCommand({searchString, ancestorNoteId}: CommandListenerData<"searchNotes">) {
const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId});
async searchNotesCommand({ searchString, ancestorNoteId }: CommandListenerData<"searchNotes">) {
const searchNote = await dateNoteService.createSearchNote({ searchString, ancestorNoteId });
if (!searchNote) {
return;
}
@@ -41,13 +41,13 @@ export default class RootCommandExecutor extends Component {
activate: true
});
appContext.triggerCommand('focusOnSearchDefinition', {ntxId: noteContext.ntxId});
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
}
async searchInSubtreeCommand({notePath}: CommandListenerData<"searchInSubtree">) {
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
const noteId = treeService.getNoteIdFromUrl(notePath);
this.searchNotesCommand({ancestorNoteId: noteId});
this.searchNotesCommand({ ancestorNoteId: noteId });
}
openNoteExternallyCommand() {
@@ -83,11 +83,11 @@ export default class RootCommandExecutor extends Component {
}
toggleLeftPaneCommand() {
options.toggle('leftPaneVisible');
options.toggle("leftPaneVisible");
}
async showBackendLogCommand() {
await appContext.tabManager.openTabWithNoteWithHoisting('_backendLog', { activate: true });
await appContext.tabManager.openTabWithNoteWithHoisting("_backendLog", { activate: true });
}
async showLaunchBarSubtreeCommand() {
@@ -97,26 +97,26 @@ export default class RootCommandExecutor extends Component {
}
async showShareSubtreeCommand() {
await this.showAndHoistSubtree('_share');
await this.showAndHoistSubtree("_share");
}
async showHiddenSubtreeCommand() {
await this.showAndHoistSubtree('_hidden');
await this.showAndHoistSubtree("_hidden");
}
async showOptionsCommand({section}: CommandListenerData<"showOptions">) {
await appContext.tabManager.openContextWithNote(section || '_options', {
async showOptionsCommand({ section }: CommandListenerData<"showOptions">) {
await appContext.tabManager.openContextWithNote(section || "_options", {
activate: true,
hoistedNoteId: '_options'
hoistedNoteId: "_options"
});
}
async showSQLConsoleHistoryCommand() {
await this.showAndHoistSubtree('_sqlConsole');
await this.showAndHoistSubtree("_sqlConsole");
}
async showSearchHistoryCommand() {
await this.showAndHoistSubtree('_search');
await this.showAndHoistSubtree("_search");
}
async showAndHoistSubtree(subtreeNoteId: string) {
@@ -133,7 +133,7 @@ export default class RootCommandExecutor extends Component {
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: true,
viewScope: {
viewMode: 'source'
viewMode: "source"
}
});
}
@@ -146,7 +146,7 @@ export default class RootCommandExecutor extends Component {
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: true,
viewScope: {
viewMode: 'attachments'
viewMode: "attachments"
}
});
}
@@ -159,7 +159,7 @@ export default class RootCommandExecutor extends Component {
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: true,
viewScope: {
viewMode: 'attachments'
viewMode: "attachments"
}
});
}
@@ -167,23 +167,43 @@ export default class RootCommandExecutor extends Component {
toggleTrayCommand() {
if (!utils.isElectron()) return;
const {BrowserWindow} = utils.dynamicRequire('@electron/remote');
const windows = (BrowserWindow.getAllWindows()) as Electron.BaseWindow[];
const isVisible = windows.every(w => w.isVisible());
const action = isVisible ? "hide" : "show"
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
const isVisible = windows.every((w) => w.isVisible());
const action = isVisible ? "hide" : "show";
for (const window of windows) window[action]();
}
firstTabCommand() { this.#goToTab(1); }
secondTabCommand() { this.#goToTab(2); }
thirdTabCommand() { this.#goToTab(3); }
fourthTabCommand() { this.#goToTab(4); }
fifthTabCommand() { this.#goToTab(5); }
sixthTabCommand() { this.#goToTab(6); }
seventhTabCommand() { this.#goToTab(7); }
eigthTabCommand() { this.#goToTab(8); }
ninthTabCommand() { this.#goToTab(9); }
lastTabCommand() { this.#goToTab(Number.POSITIVE_INFINITY); }
firstTabCommand() {
this.#goToTab(1);
}
secondTabCommand() {
this.#goToTab(2);
}
thirdTabCommand() {
this.#goToTab(3);
}
fourthTabCommand() {
this.#goToTab(4);
}
fifthTabCommand() {
this.#goToTab(5);
}
sixthTabCommand() {
this.#goToTab(6);
}
seventhTabCommand() {
this.#goToTab(7);
}
eigthTabCommand() {
this.#goToTab(8);
}
ninthTabCommand() {
this.#goToTab(9);
}
lastTabCommand() {
this.#goToTab(Number.POSITIVE_INFINITY);
}
#goToTab(tabNumber: number) {
const mainNoteContexts = appContext.tabManager.getMainNoteContexts();

View File

@@ -1,17 +1,15 @@
import appContext, { EventData, EventListener } from "./app_context.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import shortcutService from "../services/shortcuts.js";
import server from "../services/server.js";
import Component from "./component.js";
import froca from "../services/froca.js";
import { AttributeRow } from "../services/load_results.js";
import type { AttributeRow } from "../services/load_results.js";
export default class ShortcutComponent extends Component
implements EventListener<"entitiesReloaded">
{
export default class ShortcutComponent extends Component implements EventListener<"entitiesReloaded"> {
constructor() {
super();
server.get<AttributeRow[]>('keyboard-shortcuts-for-notes').then(shortcutAttributes => {
server.get<AttributeRow[]>("keyboard-shortcuts-for-notes").then((shortcutAttributes) => {
for (const attr of shortcutAttributes) {
this.bindNoteShortcutHandler(attr);
}
@@ -22,7 +20,8 @@ export default class ShortcutComponent extends Component
const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId);
const namespace = labelOrRow.attributeId;
if (labelOrRow.isDeleted) { // only applicable if row
if (labelOrRow.isDeleted) {
// only applicable if row
if (namespace) {
shortcutService.removeGlobalShortcut(namespace);
}
@@ -31,12 +30,12 @@ export default class ShortcutComponent extends Component
}
}
async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
for (const attr of loadResults.getAttributeRows()) {
if (attr.type === 'label' && attr.name === 'keyboardShortcut' && attr.noteId) {
if (attr.type === "label" && attr.name === "keyboardShortcut" && attr.noteId) {
const note = await froca.getNote(attr.noteId);
// launcher shortcuts are handled specifically
if (note && attr && note.type !== 'launcher') {
if (note && attr && note.type !== "launcher") {
this.bindNoteShortcutHandler(attr);
}
}

View File

@@ -28,11 +28,9 @@ export default class TabManager extends Component {
return;
}
const openNoteContexts = this.noteContexts
.map(nc => nc.getPojoState())
.filter(t => !!t);
const openNoteContexts = this.noteContexts.map((nc) => nc.getPojoState()).filter((t) => !!t);
await server.put('options', {
await server.put("options", {
openNoteContexts: JSON.stringify(openNoteContexts)
});
});
@@ -47,21 +45,17 @@ export default class TabManager extends Component {
/** @type {NoteContext[]} */
get mainNoteContexts() {
return this.noteContexts.filter(nc => !nc.mainNtxId)
return this.noteContexts.filter((nc) => !nc.mainNtxId);
}
async loadTabs() {
try {
const noteContextsToOpen = (appContext.isMainWindow && options.getJson('openNoteContexts')) || [];
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
// preload all notes at once
await froca.getNotes([
...noteContextsToOpen.flatMap(tab =>
[ treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId]
),
], true);
await froca.getNotes([...noteContextsToOpen.flatMap((tab) => [treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true);
const filteredNoteContexts = noteContextsToOpen.filter(openTab => {
const filteredNoteContexts = noteContextsToOpen.filter((openTab) => {
const noteId = treeService.getNoteIdFromUrl(openTab.notePath);
if (!(noteId in froca.notes)) {
// note doesn't exist so don't try to open tab for it
@@ -69,7 +63,7 @@ export default class TabManager extends Component {
}
if (!(openTab.hoistedNoteId in froca.notes)) {
openTab.hoistedNoteId = 'root';
openTab.hoistedNoteId = "root";
}
return true;
@@ -82,13 +76,13 @@ export default class TabManager extends Component {
parsedFromUrl.ntxId = parsedFromUrl.ntxId || NoteContext.generateNtxId(); // generate already here, so that we later know which one to activate
filteredNoteContexts.push({
notePath: parsedFromUrl.notePath || 'root',
notePath: parsedFromUrl.notePath || "root",
ntxId: parsedFromUrl.ntxId,
active: true,
hoistedNoteId: parsedFromUrl.hoistedNoteId || 'root',
hoistedNoteId: parsedFromUrl.hoistedNoteId || "root",
viewScope: parsedFromUrl.viewScope || {}
});
} else if (!filteredNoteContexts.find(tab => tab.active)) {
} else if (!filteredNoteContexts.find((tab) => tab.active)) {
filteredNoteContexts[0].active = true;
}
@@ -107,27 +101,21 @@ export default class TabManager extends Component {
// if there's a notePath in the URL, make sure it's open and active
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
if (parsedFromUrl.notePath) {
await appContext.tabManager.switchToNoteContext(
parsedFromUrl.ntxId,
parsedFromUrl.notePath,
parsedFromUrl.viewScope,
parsedFromUrl.hoistedNoteId
);
await appContext.tabManager.switchToNoteContext(parsedFromUrl.ntxId, parsedFromUrl.notePath, parsedFromUrl.viewScope, parsedFromUrl.hoistedNoteId);
} else if (parsedFromUrl.searchString) {
await appContext.triggerCommand('searchNotes', {
await appContext.triggerCommand("searchNotes", {
searchString: parsedFromUrl.searchString
});
}
}
catch (e) {
logError(`Loading note contexts '${options.get('openNoteContexts')}' failed: ${e.message} ${e.stack}`);
} catch (e) {
logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`);
// try to recover
await this.openEmptyTab();
}
}
noteSwitchedEvent({noteContext}) {
noteSwitchedEvent({ noteContext }) {
if (noteContext.isActive()) {
this.setCurrentNavigationStateToHash();
}
@@ -147,7 +135,7 @@ export default class TabManager extends Component {
const activeNoteContext = this.getActiveContext();
this.updateDocumentTitle(activeNoteContext);
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
this.triggerEvent("activeNoteChanged"); // trigger this even in on popstate event
}
calculateHash() {
@@ -174,12 +162,12 @@ export default class TabManager extends Component {
* @returns {NoteContext[]}
*/
getMainNoteContexts() {
return this.noteContexts.filter(nc => nc.isMainContext());
return this.noteContexts.filter((nc) => nc.isMainContext());
}
/** @returns {NoteContext} */
getNoteContextById(ntxId) {
const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId);
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId);
if (!noteContext) {
throw new Error(`Cannot find noteContext id='${ntxId}'`);
@@ -194,9 +182,7 @@ export default class TabManager extends Component {
* @returns {NoteContext}
*/
getActiveContext() {
return this.activeNtxId
? this.getNoteContextById(this.activeNtxId)
: null;
return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null;
}
/**
@@ -205,9 +191,7 @@ export default class TabManager extends Component {
* @returns {NoteContext}
*/
getActiveMainContext() {
return this.activeNtxId
? this.getNoteContextById(this.activeNtxId).getMainContext()
: null;
return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null;
}
/** @returns {string|null} */
@@ -243,8 +227,7 @@ export default class TabManager extends Component {
}
async switchToNoteContext(ntxId, notePath, viewScope = {}, hoistedNoteId = null) {
const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId)
|| await this.openEmptyTab();
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || (await this.openEmptyTab());
await this.activateNoteContext(noteContext.ntxId);
@@ -265,10 +248,10 @@ export default class TabManager extends Component {
await noteContext.setEmpty();
}
async openEmptyTab(ntxId = null, hoistedNoteId = 'root', mainNtxId = null) {
async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId = null) {
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
const existingNoteContext = this.children.find(nc => nc.ntxId === noteContext.ntxId);
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);
if (existingNoteContext) {
await existingNoteContext.setHoistedNoteId(hoistedNoteId);
@@ -278,7 +261,7 @@ export default class TabManager extends Component {
this.child(noteContext);
await this.triggerEvent('newNoteContextCreated', {noteContext});
await this.triggerEvent("newNoteContextCreated", { noteContext });
return noteContext;
}
@@ -300,12 +283,12 @@ export default class TabManager extends Component {
*/
async openTabWithNoteWithHoisting(notePath, opts = {}) {
const noteContext = this.getActiveContext();
let hoistedNoteId = 'root';
let hoistedNoteId = "root";
if (noteContext) {
const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId);
if (resolvedNotePath.includes(noteContext.hoistedNoteId) || resolvedNotePath.includes('_hidden')) {
if (resolvedNotePath.includes(noteContext.hoistedNoteId) || resolvedNotePath.includes("_hidden")) {
hoistedNoteId = noteContext.hoistedNoteId;
}
}
@@ -319,7 +302,7 @@ export default class TabManager extends Component {
const activate = !!opts.activate;
const ntxId = opts.ntxId || null;
const mainNtxId = opts.mainNtxId || null;
const hoistedNoteId = opts.hoistedNoteId || 'root';
const hoistedNoteId = opts.hoistedNoteId || "root";
const viewScope = opts.viewScope || { viewMode: "default" };
const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId);
@@ -335,7 +318,7 @@ export default class TabManager extends Component {
if (activate) {
this.activateNoteContext(noteContext.ntxId, false);
await this.triggerEvent('noteSwitchedAndActivated', {
await this.triggerEvent("noteSwitchedAndActivated", {
noteContext,
notePath: noteContext.notePath // resolved note path
});
@@ -366,7 +349,7 @@ export default class TabManager extends Component {
this.activeNtxId = ntxId;
if (triggerEvent) {
await this.triggerEvent('activeContextChanged', {
await this.triggerEvent("activeContextChanged", {
noteContext: this.getNoteContextById(ntxId)
});
}
@@ -388,14 +371,13 @@ export default class TabManager extends Component {
try {
noteContextToRemove = this.getNoteContextById(ntxId);
}
catch {
} catch {
// note context not found
return false;
}
if (noteContextToRemove.isMainContext()) {
const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext());
const mainNoteContexts = this.getNoteContexts().filter((nc) => nc.isMainContext());
if (mainNoteContexts.length === 1) {
if (noteContextToRemove.isEmpty()) {
@@ -415,28 +397,25 @@ export default class TabManager extends Component {
}
const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
await this.triggerEvent('beforeNoteContextRemove', { ntxIds: ntxIdsToRemove });
await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove });
if (!noteContextToRemove.isMainContext()) {
const siblings = noteContextToRemove.getMainContext().getSubContexts();
const idx = siblings.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId);
const idx = siblings.findIndex((nc) => nc.ntxId === noteContextToRemove.ntxId);
const contextToActivateIdx = idx === siblings.length - 1 ? idx - 1 : idx + 1;
const contextToActivate = siblings[contextToActivateIdx];
await this.activateNoteContext(contextToActivate.ntxId);
}
else if (this.mainNoteContexts.length <= 1) {
} else if (this.mainNoteContexts.length <= 1) {
await this.openAndActivateEmptyTab();
}
else if (ntxIdsToRemove.includes(this.activeNtxId)) {
const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId);
} else if (ntxIdsToRemove.includes(this.activeNtxId)) {
const idx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === noteContextToRemove.ntxId);
if (idx === this.mainNoteContexts.length - 1) {
await this.activatePreviousTabCommand();
}
else {
} else {
await this.activateNextTabCommand();
}
}
@@ -448,15 +427,15 @@ export default class TabManager extends Component {
}
removeNoteContexts(noteContextsToRemove) {
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
const position = this.noteContexts.findIndex(nc => ntxIdsToRemove.includes(nc.ntxId));
const position = this.noteContexts.findIndex((nc) => ntxIdsToRemove.includes(nc.ntxId));
this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId));
this.children = this.children.filter((nc) => !ntxIdsToRemove.includes(nc.ntxId));
this.addToRecentlyClosedTabs(noteContextsToRemove, position);
this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove});
this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove });
this.tabsUpdate.scheduleUpdate();
}
@@ -466,10 +445,10 @@ export default class TabManager extends Component {
return;
}
this.recentlyClosedTabs.push({contexts: noteContexts, position: position});
this.recentlyClosedTabs.push({ contexts: noteContexts, position: position });
}
tabReorderEvent({ntxIdsInOrder}) {
tabReorderEvent({ ntxIdsInOrder }) {
const order = {};
let i = 0;
@@ -480,18 +459,18 @@ export default class TabManager extends Component {
}
}
this.children.sort((a, b) => order[a.ntxId] < order[b.ntxId] ? -1 : 1);
this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1));
this.tabsUpdate.scheduleUpdate();
}
noteContextReorderEvent({ntxIdsInOrder, oldMainNtxId, newMainNtxId}) {
noteContextReorderEvent({ ntxIdsInOrder, oldMainNtxId, newMainNtxId }) {
const order = Object.fromEntries(ntxIdsInOrder.map((v, i) => [v, i]));
this.children.sort((a, b) => order[a.ntxId] < order[b.ntxId] ? -1 : 1);
this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1));
if (oldMainNtxId && newMainNtxId) {
this.children.forEach(c => {
this.children.forEach((c) => {
if (c.ntxId === newMainNtxId) {
// new main context has null mainNtxId
c.mainNtxId = null;
@@ -508,7 +487,7 @@ export default class TabManager extends Component {
async activateNextTabCommand() {
const activeMainNtxId = this.getActiveMainContext().ntxId;
const oldIdx = this.mainNoteContexts.findIndex(nc => nc.ntxId === activeMainNtxId);
const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId);
const newActiveNtxId = this.mainNoteContexts[oldIdx === this.mainNoteContexts.length - 1 ? 0 : oldIdx + 1].ntxId;
await this.activateNoteContext(newActiveNtxId);
@@ -517,7 +496,7 @@ export default class TabManager extends Component {
async activatePreviousTabCommand() {
const activeMainNtxId = this.getActiveMainContext().ntxId;
const oldIdx = this.mainNoteContexts.findIndex(nc => nc.ntxId === activeMainNtxId);
const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId);
const newActiveNtxId = this.mainNoteContexts[oldIdx === 0 ? this.mainNoteContexts.length - 1 : oldIdx - 1].ntxId;
await this.activateNoteContext(newActiveNtxId);
@@ -538,21 +517,21 @@ export default class TabManager extends Component {
}
async closeAllTabsCommand() {
for (const ntxIdToRemove of this.mainNoteContexts.map(nc => nc.ntxId)) {
for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) {
await this.removeNoteContext(ntxIdToRemove);
}
}
async closeOtherTabsCommand({ntxId}) {
for (const ntxIdToRemove of this.mainNoteContexts.map(nc => nc.ntxId)) {
async closeOtherTabsCommand({ ntxId }) {
for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) {
if (ntxIdToRemove !== ntxId) {
await this.removeNoteContext(ntxIdToRemove);
}
}
}
async closeRightTabsCommand({ntxId}) {
const ntxIds = this.mainNoteContexts.map(nc => nc.ntxId);
async closeRightTabsCommand({ ntxId }) {
const ntxIds = this.mainNoteContexts.map((nc) => nc.ntxId);
const index = ntxIds.indexOf(ntxId);
if (index !== -1) {
@@ -563,23 +542,23 @@ export default class TabManager extends Component {
}
}
async closeTabCommand({ntxId}) {
async closeTabCommand({ ntxId }) {
await this.removeNoteContext(ntxId);
}
async moveTabToNewWindowCommand({ntxId}) {
const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId);
async moveTabToNewWindowCommand({ ntxId }) {
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
const removed = await this.removeNoteContext(ntxId);
if (removed) {
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
}
}
async copyTabToNewWindowCommand({ntxId}) {
const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId);
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
async copyTabToNewWindowCommand({ ntxId }) {
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
}
async reopenLastTabCommand() {
@@ -601,40 +580,38 @@ export default class TabManager extends Component {
for (const noteContext of noteContexts) {
this.child(noteContext);
await this.triggerEvent('newNoteContextCreated', {noteContext});
await this.triggerEvent("newNoteContextCreated", { noteContext });
}
// restore last position of contexts stored in tab manager
const ntxsInOrder = [
...this.noteContexts.slice(0, lastClosedTab.position),
...this.noteContexts.slice(-noteContexts.length),
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length),
]
await this.noteContextReorderEvent({ntxIdsInOrder: ntxsInOrder.map(nc => nc.ntxId)});
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
];
await this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId) });
let mainNtx = noteContexts.find(nc => nc.isMainContext());
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
if (mainNtx) {
// reopened a tab, need to reorder new tab widget in tab row
await this.triggerEvent('contextsReopened', {
await this.triggerEvent("contextsReopened", {
mainNtxId: mainNtx.ntxId,
tabPosition: ntxsInOrder.filter(nc => nc.isMainContext()).findIndex(nc => nc.ntxId === mainNtx.ntxId)
tabPosition: ntxsInOrder.filter((nc) => nc.isMainContext()).findIndex((nc) => nc.ntxId === mainNtx.ntxId)
});
} else {
// reopened a single split, need to reorder the pane widget in split note container
await this.triggerEvent('contextsReopened', {
await this.triggerEvent("contextsReopened", {
ntxId: ntxsInOrder[lastClosedTab.position].ntxId,
// this is safe since lastClosedTab.position can never be 0 in this case
afterNtxId: ntxsInOrder[lastClosedTab.position - 1].ntxId
});
}
const noteContextToActivate = noteContexts.length === 1
? noteContexts[0]
: noteContexts.find(nc => nc.isMainContext());
const noteContextToActivate = noteContexts.length === 1 ? noteContexts[0] : noteContexts.find((nc) => nc.isMainContext());
await this.activateNoteContext(noteContextToActivate.ntxId);
await this.triggerEvent('noteSwitched', {
await this.triggerEvent("noteSwitched", {
noteContext: noteContextToActivate,
notePath: noteContextToActivate.notePath
});
@@ -659,7 +636,7 @@ export default class TabManager extends Component {
document.title = titleFragments.join(" - ");
}
async entitiesReloadedEvent({loadResults}) {
async entitiesReloadedEvent({ loadResults }) {
const activeContext = this.getActiveContext();
if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) {

View File

@@ -11,13 +11,13 @@ class ZoomComponent extends Component {
if (utils.isElectron()) {
options.initializedPromise.then(() => {
const zoomFactor = options.getFloat('zoomFactor');
const zoomFactor = options.getFloat("zoomFactor");
if (zoomFactor) {
this.setZoomFactor(zoomFactor);
}
});
window.addEventListener("wheel", event => {
window.addEventListener("wheel", (event) => {
if (event.ctrlKey) {
this.setZoomFactorAndSave(this.getCurrentZoom() - event.deltaY * 0.001);
}
@@ -26,8 +26,8 @@ class ZoomComponent extends Component {
}
setZoomFactor(zoomFactor: string | number) {
const parsedZoomFactor = (typeof zoomFactor !== "number" ? parseFloat(zoomFactor) : zoomFactor);
const webFrame = utils.dynamicRequire('electron').webFrame;
const parsedZoomFactor = typeof zoomFactor !== "number" ? parseFloat(zoomFactor) : zoomFactor;
const webFrame = utils.dynamicRequire("electron").webFrame;
webFrame.setZoomFactor(parsedZoomFactor);
}
@@ -37,15 +37,14 @@ class ZoomComponent extends Component {
this.setZoomFactor(zoomFactor);
await options.save('zoomFactor', zoomFactor);
}
else {
await options.save("zoomFactor", zoomFactor);
} else {
console.log(`Zoom factor ${zoomFactor} outside of the range, ignored.`);
}
}
getCurrentZoom() {
return utils.dynamicRequire('electron').webFrame.getZoomFactor();
return utils.dynamicRequire("electron").webFrame.getZoomFactor();
}
zoomOutEvent() {
@@ -58,7 +57,7 @@ class ZoomComponent extends Component {
zoomResetEvent() {
this.setZoomFactorAndSave(1);
}
setZoomFactorAndSaveEvent({ zoomFactor }: { zoomFactor: number }) {
this.setZoomFactorAndSave(zoomFactor);
}

View File

@@ -1,10 +1,10 @@
import appContext from "./components/app_context.js";
import utils from './services/utils.js';
import noteTooltipService from './services/note_tooltip.js';
import utils from "./services/utils.js";
import noteTooltipService from "./services/note_tooltip.js";
import bundleService from "./services/bundle.js";
import toastService from "./services/toast.js";
import noteAutocompleteService from './services/note_autocomplete.js';
import macInit from './services/mac_init.js';
import noteAutocompleteService from "./services/note_autocomplete.js";
import macInit from "./services/mac_init.js";
import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
@@ -12,20 +12,19 @@ import options from "./services/options.js";
await appContext.earlyInit();
bundleService.getWidgetBundlesByParent().then(async widgetBundles => {
bundleService.getWidgetBundlesByParent().then(async (widgetBundles) => {
// A dynamic import is required for layouts since they initialize components which require translations.
const DesktopLayout = (await import("./layouts/desktop_layout.js")).default;
appContext.setLayout(new DesktopLayout(widgetBundles));
appContext.start()
.catch((e) => {
toastService.showPersistent({
title: t("toast.critical-error.title"),
icon: "alert",
message: t("toast.critical-error.message", { message: e.message }),
});
console.error("Critical error occured", e);
appContext.start().catch((e) => {
toastService.showPersistent({
title: t("toast.critical-error.title"),
icon: "alert",
message: t("toast.critical-error.message", { message: e.message })
});
console.error("Critical error occured", e);
});
});
glob.setupGlobs();
@@ -45,8 +44,8 @@ if (utils.isElectron()) {
}
function initOnElectron() {
const electron = utils.dynamicRequire('electron');
electron.ipcRenderer.on('globalShortcut', async (event, actionName) => appContext.triggerCommand(actionName));
const electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
const electronRemote = utils.dynamicRequire("@electron/remote");
const currentWindow = electronRemote.getCurrentWindow();
@@ -72,8 +71,7 @@ function initTitleBarButtons(style, currentWindow) {
applyWindowsOverlay();
// Register for changes to the native title bar colors.
window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", applyWindowsOverlay);
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyWindowsOverlay);
}
if (window.glob.platform === "darwin") {

View File

@@ -1,4 +1,4 @@
import { Froca } from "../services/froca-interface.js";
import type { Froca } from "../services/froca-interface.js";
export interface FAttachmentRow {
attachmentId: string;
@@ -23,12 +23,12 @@ class FAttachment {
role!: string;
mime!: string;
title!: string;
isProtected!: boolean; // TODO: Is this used?
isProtected!: boolean; // TODO: Is this used?
private dateModified!: string;
utcDateModified!: string;
private utcDateScheduledForErasureSince!: string;
/**
* optionally added to the entity
* optionally added to the entity
*/
private contentLength!: number;
@@ -58,7 +58,7 @@ class FAttachment {
}
async getBlob() {
return await this.froca.getBlob('attachments', this.attachmentId);
return await this.froca.getBlob("attachments", this.attachmentId);
}
}

View File

@@ -1,5 +1,5 @@
import { Froca } from '../services/froca-interface.js';
import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js';
import type { Froca } from "../services/froca-interface.js";
import promotedAttributeDefinitionParser from "../services/promoted_attribute_definition_parser.js";
/**
* There are currently only two types of attributes, labels or relations.
@@ -16,7 +16,6 @@ export interface FAttributeRow {
isInheritable: boolean;
}
/**
* Attribute is an abstract concept which has two real uses - label (key - value pair)
* and relation (representing named relationship between source and target note)
@@ -57,8 +56,9 @@ class FAttribute {
return await this.froca.getNote(targetNoteId, true);
}
get targetNoteId() { // alias
if (this.type !== 'relation') {
get targetNoteId() {
// alias
if (this.type !== "relation") {
throw new Error(`Attribute ${this.attributeId} is not a relation`);
}
@@ -66,7 +66,7 @@ class FAttribute {
}
get isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
get toString() {
@@ -74,7 +74,7 @@ class FAttribute {
}
isDefinition() {
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:"));
}
getDefinition() {
@@ -82,7 +82,7 @@ class FAttribute {
}
isDefinitionFor(attr: FAttribute) {
return this.type === 'label' && this.name === `${attr.type}:${attr.name}`;
return this.type === "label" && this.name === `${attr.type}:${attr.name}`;
}
get dto(): Omit<FAttribute, "froca"> {

View File

@@ -1,4 +1,3 @@
export interface FBlobRow {
blobId: string;
content: string;
@@ -8,7 +7,6 @@ export interface FBlobRow {
}
export default class FBlob {
blobId: string;
/**
* can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
@@ -40,8 +38,7 @@ export default class FBlob {
getJsonContentSafely(): unknown | null {
try {
return this.getJsonContent();
}
catch (e) {
} catch (e) {
return null;
}
}

View File

@@ -1,4 +1,4 @@
import { Froca } from "../services/froca-interface.js";
import type { Froca } from "../services/froca-interface.js";
export interface FBranchRow {
branchId: string;
@@ -61,7 +61,7 @@ class FBranch {
/** @returns true if it's top level, meaning its parent is the root note */
isTopLevel() {
return this.parentNoteId === 'root';
return this.parentNoteId === "root";
}
get toString() {
@@ -69,7 +69,7 @@ class FBranch {
}
get pojo(): Omit<FBranch, "froca"> {
const pojo = {...this} as any;
const pojo = { ...this } as any;
delete pojo.froca;
return pojo;
}

View File

@@ -1,33 +1,33 @@
import server from '../services/server.js';
import server from "../services/server.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js";
import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js";
import { Froca } from '../services/froca-interface.js';
import FAttachment from './fattachment.js';
import FAttribute, { AttributeType } from './fattribute.js';
import utils from '../services/utils.js';
import type { Froca } from "../services/froca-interface.js";
import FAttachment from "./fattachment.js";
import FAttribute, { type AttributeType } from "./fattribute.js";
import utils from "../services/utils.js";
const LABEL = 'label';
const RELATION = 'relation';
const LABEL = "label";
const RELATION = "relation";
const NOTE_TYPE_ICONS = {
"file": "bx bx-file",
"image": "bx bx-image",
"code": "bx bx-code",
"render": "bx bx-extension",
"search": "bx bx-file-find",
"relationMap": "bx bxs-network-chart",
"book": "bx bx-book",
"noteMap": "bx bxs-network-chart",
"mermaid": "bx bx-selection",
"canvas": "bx bx-pen",
"webView": "bx bx-globe-alt",
"launcher": "bx bx-link",
"doc": "bx bxs-file-doc",
"contentWidget": "bx bxs-widget",
"mindMap": "bx bx-sitemap"
file: "bx bx-file",
image: "bx bx-image",
code: "bx bx-code",
render: "bx bx-extension",
search: "bx bx-file-find",
relationMap: "bx bxs-network-chart",
book: "bx bx-book",
noteMap: "bx bxs-network-chart",
mermaid: "bx bx-selection",
canvas: "bx bx-pen",
webView: "bx bx-globe-alt",
launcher: "bx bx-link",
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap"
};
/**
@@ -65,7 +65,6 @@ export interface NoteMetaData {
* Note is the main node and concept in Trilium.
*/
class FNote {
private froca: Froca;
noteId!: string;
@@ -119,7 +118,7 @@ class FNote {
}
addParent(parentNoteId: string, branchId: string, sort = true) {
if (parentNoteId === 'none') {
if (parentNoteId === "none") {
return;
}
@@ -179,8 +178,7 @@ class FNote {
try {
return JSON.parse(content);
}
catch (e: any) {
} catch (e: any) {
console.log(`Cannot parse content of note '${this.noteId}': `, e.message);
return null;
@@ -217,7 +215,7 @@ class FNote {
getChildBranches() {
// don't use Object.values() to guarantee order
const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]);
const branchIds = this.children.map((childNoteId) => this.childToBranch[childNoteId]);
return this.froca.getBranches(branchIds);
}
@@ -236,7 +234,7 @@ class FNote {
this.parents.sort((aNoteId, bNoteId) => {
const aBranchId = this.parentToBranch[aNoteId];
if (aBranchId && aBranchId.startsWith('virt-')) {
if (aBranchId && aBranchId.startsWith("virt-")) {
return 1;
}
@@ -251,7 +249,7 @@ class FNote {
}
get isArchived() {
return this.hasAttribute('label', 'archived');
return this.hasAttribute("label", "archived");
}
getChildNoteIds() {
@@ -271,22 +269,21 @@ class FNote {
}
async getAttachmentsByRole(role: string) {
return (await this.getAttachments())
.filter(attachment => attachment.role === role);
return (await this.getAttachments()).filter((attachment) => attachment.role === role);
}
async getAttachmentById(attachmentId: string) {
const attachments = await this.getAttachments();
return attachments.find(att => att.attachmentId === attachmentId);
return attachments.find((att) => att.attachmentId === attachmentId);
}
isEligibleForConversionToAttachment() {
if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) {
if (this.type !== "image" || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) {
return false;
}
const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink');
const targetRelations = this.getTargetRelations().filter((relation) => relation.name === "imageLink");
if (targetRelations.length > 1) {
return false;
@@ -297,7 +294,7 @@ class FNote {
if (referencingNote && referencingNote !== parentNote) {
return false;
} else if (parentNote.type !== 'text' || !parentNote.isContentAvailable()) {
} else if (parentNote.type !== "text" || !parentNote.isContentAvailable()) {
return false;
}
@@ -310,9 +307,7 @@ class FNote {
* @returns all note's attributes, including inherited ones
*/
getOwnedAttributes(type?: AttributeType, name?: string) {
const attrs = this.attributes
.map(attributeId => this.froca.attributes[attributeId])
.filter(Boolean); // filter out nulls;
const attrs = this.attributes.map((attributeId) => this.froca.attributes[attributeId]).filter(Boolean); // filter out nulls;
return this.__filterAttrs(attrs, type, name);
}
@@ -338,26 +333,27 @@ class FNote {
if (!(this.noteId in noteAttributeCache.attributes)) {
const newPath = [...path, this.noteId];
const attrArrs = [ this.getOwnedAttributes() ];
const attrArrs = [this.getOwnedAttributes()];
// inheritable attrs on root are typically not intended to be applied to hidden subtree #3537
if (this.noteId !== 'root' && this.noteId !== '_hidden') {
if (this.noteId !== "root" && this.noteId !== "_hidden") {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into froca
if (parentNote.type !== 'search') {
if (parentNote.type !== "search") {
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
}
}
for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' && ['template', 'inherit'].includes(attr.name))) {
for (const templateAttr of attrArrs.flat().filter((attr) => attr.type === "relation" && ["template", "inherit"].includes(attr.name))) {
const templateNote = this.froca.notes[templateAttr.value];
if (templateNote && templateNote.noteId !== this.noteId) {
attrArrs.push(
templateNote.__getCachedAttributes(newPath)
templateNote
.__getCachedAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited
.filter(attr => !(attr.type === 'label' && (attr.name === 'template' || attr.name === 'workspacetemplate')))
.filter((attr) => !(attr.type === "label" && (attr.name === "template" || attr.name === "workspacetemplate")))
);
}
}
@@ -378,7 +374,7 @@ class FNote {
}
isRoot() {
return this.noteId === 'root';
return this.noteId === "root";
}
/**
@@ -387,15 +383,16 @@ class FNote {
* @returns array of notePaths (each represented by array of noteIds constituting the particular note path)
*/
getAllNotePaths(): string[][] {
if (this.noteId === 'root') {
return [['root']];
if (this.noteId === "root") {
return [["root"]];
}
const parentNotes = this.getParentNotes().filter(note => note.type !== 'search');
const parentNotes = this.getParentNotes().filter((note) => note.type !== "search");
const notePaths = parentNotes.length === 1
? parentNotes[0].getAllNotePaths() // optimization for the most common case
: parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
const notePaths =
parentNotes.length === 1
? parentNotes[0].getAllNotePaths() // optimization for the most common case
: parentNotes.flatMap((parentNote) => parentNote.getAllNotePaths());
for (const notePath of notePaths) {
notePath.push(this.noteId);
@@ -404,15 +401,15 @@ class FNote {
return notePaths;
}
getSortedNotePathRecords(hoistedNoteId = 'root') {
const isHoistedRoot = hoistedNoteId === 'root';
getSortedNotePathRecords(hoistedNoteId = "root") {
const isHoistedRoot = hoistedNoteId === "root";
const notePaths = this.getAllNotePaths().map(path => ({
const notePaths = 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'),
isHidden: path.includes('_hidden')
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden")
}));
notePaths.sort((a, b) => {
@@ -438,7 +435,7 @@ class FNote {
* @param {string} [hoistedNoteId='root']
* @return {string[]} array of noteIds constituting the particular note path
*/
getBestNotePath(hoistedNoteId = 'root') {
getBestNotePath(hoistedNoteId = "root") {
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
}
@@ -448,7 +445,7 @@ class FNote {
* @param {string} [hoistedNoteId='root']
* @return {string} serialized note path (e.g. 'root/a1h315/js725h')
*/
getBestNotePathString(hoistedNoteId = 'root') {
getBestNotePathString(hoistedNoteId = "root") {
const notePath = this.getBestNotePath(hoistedNoteId);
return notePath?.join("/");
@@ -458,16 +455,16 @@ class FNote {
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
*/
isHiddenCompletely() {
if (this.noteId === '_hidden') {
if (this.noteId === "_hidden") {
return true;
} else if (this.noteId === 'root') {
} else if (this.noteId === "root") {
return false;
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.noteId === 'root') {
if (parentNote.noteId === "root") {
return false;
} else if (parentNote.noteId === '_hidden' || parentNote.type === 'search') {
} else if (parentNote.noteId === "_hidden" || parentNote.type === "search") {
continue;
}
@@ -488,11 +485,11 @@ class FNote {
if (!type && !name) {
return attributes;
} else if (type && name) {
return attributes.filter(attr => attr.name === name && attr.type === type);
return attributes.filter((attr) => attr.name === name && attr.type === type);
} else if (type) {
return attributes.filter(attr => attr.type === type);
return attributes.filter((attr) => attr.type === type);
} else if (name) {
return attributes.filter(attr => attr.name === name);
return attributes.filter((attr) => attr.name === name);
}
return [];
@@ -501,17 +498,17 @@ class FNote {
__getInheritableAttributes(path: string[]) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
return attrs.filter((attr) => attr.isInheritable);
}
__validateTypeName(type?: string, name?: string) {
if (type && type !== 'label' && type !== 'relation') {
if (type && type !== "label" && type !== "relation") {
throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
}
if (name) {
const firstLetter = name.charAt(0);
if (firstLetter === '#' || firstLetter === '~') {
if (firstLetter === "#" || firstLetter === "~") {
throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`);
}
}
@@ -534,33 +531,27 @@ class FNote {
}
getIcon() {
const iconClassLabels = this.getLabels('iconClass');
const iconClassLabels = this.getLabels("iconClass");
const workspaceIconClass = this.getWorkspaceIconClass();
if (iconClassLabels && iconClassLabels.length > 0) {
return iconClassLabels[0].value;
}
else if (workspaceIconClass) {
} else if (workspaceIconClass) {
return workspaceIconClass;
}
else if (this.noteId === 'root') {
} else if (this.noteId === "root") {
return "bx bx-home-alt-2";
}
if (this.noteId === '_share') {
if (this.noteId === "_share") {
return "bx bx-share-alt";
}
else if (this.type === 'text') {
} else if (this.type === "text") {
if (this.isFolder()) {
return "bx bx-folder";
}
else {
} else {
return "bx bx-note";
}
}
else if (this.type === 'code' && this.mime.startsWith('text/x-sql')) {
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data";
}
else {
} else {
return NOTE_TYPE_ICONS[this.type];
}
}
@@ -571,8 +562,7 @@ class FNote {
}
isFolder() {
return this.type === 'search'
|| this.getFilteredChildBranches().length > 0;
return this.type === "search" || this.getFilteredChildBranches().length > 0;
}
getFilteredChildBranches() {
@@ -615,7 +605,7 @@ class FNote {
hasAttribute(type: AttributeType, name: string) {
const attributes = this.getAttributes();
return attributes.some(attr => attr.name === name && attr.type === type);
return attributes.some((attr) => attr.name === name && attr.type === type);
}
/**
@@ -635,7 +625,7 @@ class FNote {
getOwnedAttribute(type: AttributeType, name: string) {
const attributes = this.getOwnedAttributes();
return attributes.find(attr => attr.name === name && attr.type === type);
return attributes.find((attr) => attr.name === name && attr.type === type);
}
/**
@@ -646,7 +636,7 @@ class FNote {
getAttribute(type: AttributeType, name: string) {
const attributes = this.getAttributes();
return attributes.find(attr => attr.name === name && attr.type === type);
return attributes.find((attr) => attr.name === name && attr.type === type);
}
/**
@@ -683,7 +673,9 @@ class FNote {
* @param name - label name
* @returns true if label exists (including inherited)
*/
hasLabel(name: string) { return this.hasAttribute(LABEL, name); }
hasLabel(name: string) {
return this.hasAttribute(LABEL, name);
}
/**
* @param name - label name
@@ -696,68 +688,88 @@ class FNote {
return false;
}
return label && label.value !== 'false';
return label && label.value !== "false";
}
/**
* @param name - relation name
* @returns true if relation exists (excluding inherited)
*/
hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); }
hasOwnedRelation(name: string) {
return this.hasOwnedAttribute(RELATION, name);
}
/**
* @param name - relation name
* @returns true if relation exists (including inherited)
*/
hasRelation(name: string) { return this.hasAttribute(RELATION, name); }
hasRelation(name: string) {
return this.hasAttribute(RELATION, name);
}
/**
* @param name - label name
* @returns label if it exists, null otherwise
*/
getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); }
getOwnedLabel(name: string) {
return this.getOwnedAttribute(LABEL, name);
}
/**
* @param name - label name
* @returns label if it exists, null otherwise
*/
getLabel(name: string) { return this.getAttribute(LABEL, name); }
getLabel(name: string) {
return this.getAttribute(LABEL, name);
}
/**
* @param name - relation name
* @returns relation if it exists, null otherwise
*/
getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); }
getOwnedRelation(name: string) {
return this.getOwnedAttribute(RELATION, name);
}
/**
* @param name - relation name
* @returns relation if it exists, null otherwise
*/
getRelation(name: string) { return this.getAttribute(RELATION, name); }
getRelation(name: string) {
return this.getAttribute(RELATION, name);
}
/**
* @param name - label name
* @returns label value if label exists, null otherwise
*/
getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); }
getOwnedLabelValue(name: string) {
return this.getOwnedAttributeValue(LABEL, name);
}
/**
* @param name - label name
* @returns label value if label exists, null otherwise
*/
getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); }
getLabelValue(name: string) {
return this.getAttributeValue(LABEL, name);
}
/**
* @param name - relation name
* @returns relation value if relation exists, null otherwise
*/
getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); }
getOwnedRelationValue(name: string) {
return this.getOwnedAttributeValue(RELATION, name);
}
/**
* @param name - relation name
* @returns relation value if relation exists, null otherwise
*/
getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); }
getRelationValue(name: string) {
return this.getAttributeValue(RELATION, name);
}
/**
* @param name
@@ -784,22 +796,19 @@ class FNote {
}
getNotesToInheritAttributesFrom() {
const relations = [
...this.getRelations('template'),
...this.getRelations('inherit')
];
const relations = [...this.getRelations("template"), ...this.getRelations("inherit")];
return relations.map(rel => this.froca.notes[rel.value]);
return relations.map((rel) => this.froca.notes[rel.value]);
}
getPromotedDefinitionAttributes() {
if (this.isLabelTruthy('hidePromotedAttributes')) {
if (this.isLabelTruthy("hidePromotedAttributes")) {
return [];
}
const promotedAttrs = this.getAttributes()
.filter(attr => attr.isDefinition())
.filter(attr => {
.filter((attr) => attr.isDefinition())
.filter((attr) => {
const def = attr.getDefinition();
return def && def.isPromoted;
@@ -850,7 +859,7 @@ class FNote {
}
isInHiddenSubtree() {
return this.noteId === '_hidden' || this.hasAncestor('_hidden');
return this.noteId === "_hidden" || this.hasAncestor("_hidden");
}
/**
@@ -862,8 +871,7 @@ class FNote {
* Get relations which target this note
*/
getTargetRelations() {
return this.targetRelations
.map(attributeId => this.froca.attributes[attributeId]);
return this.targetRelations.map((attributeId) => this.froca.attributes[attributeId]);
}
/**
@@ -872,7 +880,7 @@ class FNote {
async getTargetRelationSourceNotes() {
const targetRelations = this.getTargetRelations();
return await this.froca.getNotes(targetRelations.map(tr => tr.noteId));
return await this.froca.getNotes(targetRelations.map((tr) => tr.noteId));
}
/**
@@ -883,7 +891,7 @@ class FNote {
}
async getBlob() {
return await this.froca.getBlob('notes', this.noteId);
return await this.froca.getBlob("notes", this.noteId);
}
toString() {
@@ -898,26 +906,26 @@ class FNote {
}
getCssClass() {
const labels = this.getLabels('cssClass');
return labels.map(l => l.value).join(' ');
const labels = this.getLabels("cssClass");
return labels.map((l) => l.value).join(" ");
}
getWorkspaceIconClass() {
const labels = this.getLabels('workspaceIconClass');
const labels = this.getLabels("workspaceIconClass");
return labels.length > 0 ? labels[0].value : "";
}
getWorkspaceTabBackgroundColor() {
const labels = this.getLabels('workspaceTabBackgroundColor');
const labels = this.getLabels("workspaceTabBackgroundColor");
return labels.length > 0 ? labels[0].value : "";
}
/** @returns true if this note is JavaScript (code or file) */
isJavaScript() {
return (this.type === "code" || this.type === "file" || this.type === 'launcher')
&& (this.mime.startsWith("application/javascript")
|| this.mime === "application/x-javascript"
|| this.mime === "text/javascript");
return (
(this.type === "code" || this.type === "file" || this.type === "launcher") &&
(this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript" || this.mime === "text/javascript")
);
}
/** @returns true if this note is HTML */
@@ -927,15 +935,15 @@ class FNote {
/** @returns JS script environment - either "frontend" or "backend" */
getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) {
return "frontend";
}
if (this.type === 'render') {
if (this.type === "render") {
return "frontend";
}
if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
if (this.isJavaScript() && this.mime.endsWith("env=backend")) {
return "backend";
}
@@ -961,17 +969,17 @@ class FNote {
isShared() {
for (const parentNoteId of this.parents) {
if (parentNoteId === 'root' || parentNoteId === 'none') {
if (parentNoteId === "root" || parentNoteId === "none") {
continue;
}
const parentNote = froca.notes[parentNoteId];
if (!parentNote || parentNote.type === 'search') {
if (!parentNote || parentNote.type === "search") {
continue;
}
if (parentNote.noteId === '_share' || parentNote.isShared()) {
if (parentNote.noteId === "_share" || parentNote.isShared()) {
return true;
}
}
@@ -980,11 +988,11 @@ class FNote {
}
isContentAvailable() {
return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable()
return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable();
}
isLaunchBarConfig() {
return this.type === 'launcher' || utils.isLaunchBarConfig(this.noteId);
return this.type === "launcher" || utils.isLaunchBarConfig(this.noteId);
}
isOptions() {

View File

@@ -94,139 +94,147 @@ export default class DesktopLayout {
getRootWidget(appContext) {
appContext.noteTreeWidget = new NoteTreeWidget();
const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
const launcherPaneIsHorizontal = options.get("layoutOrientation") === "horizontal";
const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
const isElectron = (utils.isElectron());
const isMac = (window.glob.platform === "darwin");
const isWindows = (window.glob.platform === "win32");
const hasNativeTitleBar = (window.glob.hasNativeTitleBar);
const isElectron = utils.isElectron();
const isMac = window.glob.platform === "darwin";
const isWindows = window.glob.platform === "win32";
const hasNativeTitleBar = window.glob.hasNativeTitleBar;
/**
* If true, the tab bar is displayed above the launcher pane with full width; if false (default), the tab bar is displayed in the rest pane.
* On macOS we need to force the full-width tab bar on Electron in order to allow the semaphore (window controls) enough space.
*/
const fullWidthTabBar = (launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac));
const customTitleBarButtons = (!hasNativeTitleBar && !isMac && !isWindows);
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
return new RootContainer(true)
.setParent(appContext)
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
.optChild(fullWidthTabBar, new FlexContainer('row')
.class("tab-row-container")
.child(new FlexContainer( "row").id("tab-row-left-spacer"))
.optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
.child(new TabRowWidget().class("full-width"))
.optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css('height', '40px')
.css('background-color', 'var(--launcher-pane-background-color)')
.setParent(appContext)
.optChild(
fullWidthTabBar,
new FlexContainer("row")
.class("tab-row-container")
.child(new FlexContainer("row").id("tab-row-left-spacer"))
.optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
.child(new TabRowWidget().class("full-width"))
.optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css("height", "40px")
.css("background-color", "var(--launcher-pane-background-color)")
.setParent(appContext)
)
.optChild(launcherPaneIsHorizontal, launcherPane)
.child(new FlexContainer('row')
.css("flex-grow", "1")
.id("horizontal-main-container")
.optChild(!launcherPaneIsHorizontal, launcherPane)
.child(new LeftPaneContainer()
.optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
.child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
)
.child(new FlexContainer('column')
.id('rest-pane')
.child(
new FlexContainer("row")
.css("flex-grow", "1")
.optChild(!fullWidthTabBar, new FlexContainer('row')
.child(new TabRowWidget())
.optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css('height', '40px')
.id("horizontal-main-container")
.optChild(!launcherPaneIsHorizontal, launcherPane)
.child(
new LeftPaneContainer()
.optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
.child(appContext.noteTreeWidget)
.child(...this.customWidgets.get("left-pane"))
)
.child(new FlexContainer('row')
.filling()
.collapsible()
.id("vertical-main-container")
.child(new FlexContainer('column')
.filling()
.collapsible()
.id('center-pane')
.child(new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(new FlexContainer('row').class('title-row')
.css("height", "50px")
.css("min-height", "50px")
.css('align-items', "center")
.cssBlock('.title-row > * { margin: 5px; }')
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new SpacerWidget(0, 1))
.child(new MovePaneButton(true))
.child(new MovePaneButton(false))
.child(new ClosePaneButton())
.child(new CreatePaneButton())
)
.child(
new RibbonContainer()
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget())
.ribbon(new BookPropertiesWidget())
.ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget())
.ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget())
.ribbon(new NotePathsWidget())
.ribbon(new NoteMapRibbonWidget())
.ribbon(new SimilarNotesWidget())
.ribbon(new NoteInfoWidget())
.button(new RevisionsButton())
.button(new NoteActionsWidget())
)
.child(new SharedInfoWidget())
.child(new WatchedFileUpdateStatusWidget())
.child(new FloatingButtons()
.child(new EditButton())
.child(new ShowTocWidgetButton())
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(
new ScrollingContainer()
.filling()
.child(new PromotedAttributesWidget())
.child(new SqlTableSchemasWidget())
.child(new NoteDetailWidget())
.child(new NoteListWidget())
.child(new SearchResultWidget())
.child(new SqlResultWidget())
.child(new ScrollPaddingWidget())
)
.child(new ApiLogWidget())
.child(new FindWidget())
.child(
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
...this.customWidgets.get('note-detail-pane')
)
)
.child(
new FlexContainer("column")
.id("rest-pane")
.css("flex-grow", "1")
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px"))
.child(
new FlexContainer("row")
.filling()
.collapsible()
.id("vertical-main-container")
.child(
new FlexContainer("column")
.filling()
.collapsible()
.id("center-pane")
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(
new FlexContainer("row")
.class("title-row")
.css("height", "50px")
.css("min-height", "50px")
.css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }")
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new SpacerWidget(0, 1))
.child(new MovePaneButton(true))
.child(new MovePaneButton(false))
.child(new ClosePaneButton())
.child(new CreatePaneButton())
)
.child(
new RibbonContainer()
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget())
.ribbon(new BookPropertiesWidget())
.ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget())
.ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget())
.ribbon(new NotePathsWidget())
.ribbon(new NoteMapRibbonWidget())
.ribbon(new SimilarNotesWidget())
.ribbon(new NoteInfoWidget())
.button(new RevisionsButton())
.button(new NoteActionsWidget())
)
.child(new SharedInfoWidget())
.child(new WatchedFileUpdateStatusWidget())
.child(
new FloatingButtons()
.child(new EditButton())
.child(new ShowTocWidgetButton())
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(
new ScrollingContainer()
.filling()
.child(new PromotedAttributesWidget())
.child(new SqlTableSchemasWidget())
.child(new NoteDetailWidget())
.child(new NoteListWidget())
.child(new SearchResultWidget())
.child(new SqlResultWidget())
.child(new ScrollPaddingWidget())
)
.child(new ApiLogWidget())
.child(new FindWidget())
.child(
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
...this.customWidgets.get("note-detail-pane")
)
)
)
.child(...this.customWidgets.get("center-pane"))
)
.child(
new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightsListWidget())
.child(...this.customWidgets.get("right-pane"))
)
)
.child(...this.customWidgets.get('center-pane'))
)
.child(new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightsListWidget())
.child(...this.customWidgets.get('right-pane'))
)
)
)
)
.child(new BulkActionsDialog())
.child(new AboutDialog())
@@ -254,14 +262,10 @@ export default class DesktopLayout {
}
#buildLauncherPane(isHorizontal) {
let launcherPane;
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.css("height", "53px")
.class("horizontal")
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true))
launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true));
} else {
launcherPane = new FlexContainer("column")
.css("width", "53px")

View File

@@ -122,81 +122,71 @@ export default class MobileLayout {
.setParent(appContext)
.class("horizontal-layout")
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column")
.id("mobile-sidebar-container")
)
.child(new FlexContainer("row")
.filling()
.id("mobile-rest-container")
.child(new SidebarContainer("tree", 'column')
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
.id("mobile-sidebar-wrapper")
.css("max-height", "100%")
.css('padding-left', "0")
.css('padding-right', "0")
.css('contain', 'content')
.child(new FlexContainer("column")
.filling()
.id("mobile-sidebar-wrapper")
.child(new QuickSearchWidget())
.child(new NoteTreeWidget()
.cssBlock(FANCYTREE_CSS))))
.child(new ScreenContainer("detail", "column")
.id("detail-container")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
.css("padding-left", "0")
.css("padding-right", "0")
.css('max-height', '100%')
.css("position", "relative")
.child(new FlexContainer('row').contentSized()
.css('font-size', 'larger')
.css('align-items', 'center')
.child(new ToggleSidebarButtonWidget().contentSized())
.child(new NoteTitleWidget()
.contentSized()
.css("position", "relative")
.css("top", "5px")
.css("padding-left", "0.5em"))
.child(new MobileDetailMenuWidget(true).contentSized())
)
.child(new SharedInfoWidget())
.child(new FloatingButtons()
.child(new EditButton())
.child(new RelationMapButtons())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(new PromotedAttributesWidget())
.child(new FlexContainer("column").id("mobile-sidebar-container"))
.child(
new FlexContainer("row")
.filling()
.id("mobile-rest-container")
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(
new NoteDetailWidget()
.css('padding', '5px 0 10px 0')
)
.child(new NoteListWidget())
.child(new FilePropertiesWidget().css('font-size','smaller'))
new SidebarContainer("tree", "column")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
.id("mobile-sidebar-wrapper")
.css("max-height", "100%")
.css("padding-left", "0")
.css("padding-right", "0")
.css("contain", "content")
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
)
)
.child(new ProtectedSessionPasswordDialog())
.child(new ConfirmDialog())
.child(
new ScreenContainer("detail", "column")
.id("detail-container")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
.css("padding-left", "0")
.css("padding-right", "0")
.css("max-height", "100%")
.css("position", "relative")
.child(
new FlexContainer("row")
.contentSized()
.css("font-size", "larger")
.css("align-items", "center")
.child(new ToggleSidebarButtonWidget().contentSized())
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("top", "5px").css("padding-left", "0.5em"))
.child(new MobileDetailMenuWidget(true).contentSized())
)
.child(new SharedInfoWidget())
.child(
new FloatingButtons()
.child(new EditButton())
.child(new RelationMapButtons())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(new PromotedAttributesWidget())
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(new NoteDetailWidget().css("padding", "5px 0 10px 0"))
.child(new NoteListWidget())
.child(new FilePropertiesWidget().css("font-size", "smaller"))
)
)
.child(new ProtectedSessionPasswordDialog())
.child(new ConfirmDialog())
)
.child(
new FlexContainer("column")
.contentSized()
.id("mobile-bottom-bar")
.child(new TabRowWidget().css("height", "40px"))
.child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane"))
)
.child(new FlexContainer("column")
.contentSized()
.id("mobile-bottom-bar")
.child(new TabRowWidget().css('height', '40px'))
.child(new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true))
.id("launcher-pane")))
.child(new ClassicEditorToolbar())
.child(new AboutDialog())
.child(new HelpDialog())
.child(new JumpToNoteDialog())
.child(new JumpToNoteDialog());
}
}

View File

@@ -1,6 +1,6 @@
import { CommandNames } from '../components/app_context.js';
import keyboardActionService from '../services/keyboard_actions.js';
import utils from '../services/utils.js';
import type { CommandNames } from "../components/app_context.js";
import keyboardActionService from "../services/keyboard_actions.js";
import utils from "../services/utils.js";
interface ContextMenuOptions<T extends CommandNames> {
x: number;
@@ -11,7 +11,7 @@ interface ContextMenuOptions<T extends CommandNames> {
}
interface MenuSeparatorItem {
title: "----"
title: "----";
}
export interface MenuCommandItem<T extends CommandNames> {
@@ -31,7 +31,6 @@ export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparato
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
class ContextMenu {
private $widget: JQuery<HTMLElement>;
private $cover: JQuery<HTMLElement>;
private dateContextMenuOpenedMs: number;
@@ -48,7 +47,7 @@ class ContextMenu {
if (this.isMobile) {
this.$cover.on("click", () => this.hide());
} else {
$(document).on('click', (e) => this.hide());
$(document).on("click", (e) => this.hide());
}
}
@@ -102,7 +101,7 @@ class ContextMenu {
top = this.options.y - CONTEXT_MENU_OFFSET;
}
if (this.options.orientation === 'left' && contextMenuWidth) {
if (this.options.orientation === "left" && contextMenuWidth) {
if (this.options.x + CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
// Overflow: right
left = clientWidth - contextMenuWidth - CONTEXT_MENU_OFFSET;
@@ -124,11 +123,13 @@ class ContextMenu {
}
}
this.$widget.css({
display: "block",
top: top,
left: left
}).addClass("show");
this.$widget
.css({
display: "block",
top: top,
left: left
})
.addClass("show");
}
addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[]) {
@@ -137,7 +138,7 @@ class ContextMenu {
continue;
}
if (item.title === '----') {
if (item.title === "----") {
$parent.append($("<div>").addClass("dropdown-divider"));
} else {
const $icon = $("<span>");
@@ -160,23 +161,22 @@ class ContextMenu {
const $item = $("<li>")
.addClass("dropdown-item")
.append($link)
.on('contextmenu', e => false)
.on("contextmenu", (e) => false)
// important to use mousedown instead of click since the former does not change focus
// (especially important for focused text for spell check)
.on('mousedown', e => {
.on("mousedown", (e) => {
e.stopPropagation();
if (e.which !== 1) { // only left click triggers menu items
if (e.which !== 1) {
// only left click triggers menu items
return false;
}
if (this.isMobile && "items" in item && item.items) {
const $item = $(e.target)
.closest(".dropdown-item");
const $item = $(e.target).closest(".dropdown-item");
$item.toggleClass("submenu-open");
$item.find("ul.dropdown-menu")
.toggleClass("show");
$item.find("ul.dropdown-menu").toggleClass("show");
return false;
}

View File

@@ -1,23 +1,23 @@
import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu, { MenuItem } from "./context_menu.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
import { t } from "../services/i18n.js";
import type { BrowserWindow } from "electron";
import { CommandNames } from "../components/app_context.js";
import type { CommandNames } from "../components/app_context.js";
function setupContextMenu() {
const electron = utils.dynamicRequire('electron');
const electron = utils.dynamicRequire("electron");
const remote = utils.dynamicRequire('@electron/remote');
const remote = utils.dynamicRequire("@electron/remote");
// FIXME: Remove typecast once Electron is properly integrated.
const {webContents} = remote.getCurrentWindow() as BrowserWindow;
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
webContents.on('context-menu', (event, params) => {
const {editFlags} = params;
webContents.on("context-menu", (event, params) => {
const { editFlags } = params;
const hasText = params.selectionText.trim().length > 0;
const isMac = process.platform === "darwin";
const platformModifier = isMac ? 'Meta' : 'Ctrl';
const platformModifier = isMac ? "Meta" : "Ctrl";
const items: MenuItem<CommandNames>[] = [];
@@ -60,7 +60,7 @@ function setupContextMenu() {
});
}
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") {
items.push({
title: t("electron_context_menu.copy-link"),
uiIcon: "bx bx-copy",
@@ -94,9 +94,7 @@ function setupContextMenu() {
}
if (hasText) {
const shortenedSelection = params.selectionText.length > 15
? (`${params.selectionText.substr(0, 13)}`)
: params.selectionText;
const shortenedSelection = params.selectionText.length > 15 ? `${params.selectionText.substr(0, 13)}` : params.selectionText;
// Read the search engine from the options and fallback to DuckDuckGo if the option is not set.
const customSearchEngineName = options.get("customSearchEngineName");
@@ -114,7 +112,7 @@ function setupContextMenu() {
// Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({title: "----"});
items.push({ title: "----" });
items.push({
enabled: editFlags.canPaste,
@@ -134,8 +132,8 @@ function setupContextMenu() {
x: params.x / zoomLevel,
y: params.y / zoomLevel,
items,
selectMenuItemHandler: ({command, spellingSuggestion}) => {
if (command === 'replaceMisspelling' && spellingSuggestion) {
selectMenuItemHandler: ({ command, spellingSuggestion }) => {
if (command === "replaceMisspelling" && spellingSuggestion) {
webContents.insertText(spellingSuggestion);
}
}

View File

@@ -1,4 +1,4 @@
import { t } from '../services/i18n.js';
import { t } from "../services/i18n.js";
import utils from "../services/utils.js";
import contextMenu from "./context_menu.js";
import imageService from "../services/image.js";
@@ -11,7 +11,7 @@ function setupContextMenu($image: JQuery<HTMLElement>) {
}
$image.prop(PROP_NAME, true);
$image.on('contextmenu', e => {
$image.on("contextmenu", (e) => {
e.preventDefault();
contextMenu.show({
@@ -27,17 +27,17 @@ function setupContextMenu($image: JQuery<HTMLElement>) {
title: t("image_context_menu.copy_image_to_clipboard"),
command: "copyImageToClipboard",
uiIcon: "bx bx-copy"
},
}
],
selectMenuItemHandler: async ({ command }) => {
if (command === 'copyImageReferenceToClipboard') {
if (command === "copyImageReferenceToClipboard") {
imageService.copyImageReferenceToClipboard($image);
} else if (command === 'copyImageToClipboard') {
} else if (command === "copyImageToClipboard") {
try {
const nativeImage = utils.dynamicRequire('electron').nativeImage;
const clipboard = utils.dynamicRequire('electron').clipboard;
const nativeImage = utils.dynamicRequire("electron").nativeImage;
const clipboard = utils.dynamicRequire("electron").clipboard;
const src = $image.attr('src');
const src = $image.attr("src");
if (!src) {
console.error("Missing src");
return;
@@ -46,15 +46,9 @@ function setupContextMenu($image: JQuery<HTMLElement>) {
const response = await fetch(src);
const blob = await response.blob();
clipboard.writeImage(
nativeImage.createFromBuffer(
Buffer.from(
await blob.arrayBuffer()
)
)
);
clipboard.writeImage(nativeImage.createFromBuffer(Buffer.from(await blob.arrayBuffer())));
} catch (error) {
console.error('Failed to copy image to clipboard:', error);
console.error("Failed to copy image to clipboard:", error);
}
} else {
throw new Error(`Unrecognized command '${command}'`);

View File

@@ -1,17 +1,16 @@
import treeService, { Node } from '../services/tree.js';
import treeService, { type Node } from "../services/tree.js";
import froca from "../services/froca.js";
import contextMenu, { MenuCommandItem, MenuItem } from "./context_menu.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import dialogService from "../services/dialog.js";
import server from "../services/server.js";
import { t } from '../services/i18n.js';
import type { SelectMenuItemEventListener } from '../components/events.js';
import NoteTreeWidget from '../widgets/note_tree.js';
import { FilteredCommandNames, ContextMenuCommandData } from '../components/app_context.js';
import { t } from "../services/i18n.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
export default class LauncherContextMenu implements SelectMenuItemEventListener<LauncherCommandNames> {
private treeWidget: NoteTreeWidget;
private node: Node;
@@ -26,48 +25,48 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
y: e.pageY,
items: await this.getMenuItems(),
selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item)
})
});
}
async getMenuItems(): Promise<MenuItem<LauncherCommandNames>[]> {
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const parentNoteId = this.node.getParent().data.noteId;
const isVisibleRoot = note?.noteId === '_lbVisibleLaunchers';
const isAvailableRoot = note?.noteId === '_lbAvailableLaunchers';
const isVisibleItem = parentNoteId === '_lbVisibleLaunchers';
const isAvailableItem = parentNoteId === '_lbAvailableLaunchers';
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers";
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
const items: (MenuItem<LauncherCommandNames> | null)[] = [
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-code-curly" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-customize" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-dots-horizontal" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: "----" } : null,
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-note-launcher"), command: "addNoteLauncher", uiIcon: "bx bx-note" } : null,
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null,
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null,
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null,
isVisibleRoot || isAvailableRoot ? { title: "----" } : null,
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
(isVisibleItem || isAvailableItem) ? { title: "----" } : null,
isVisibleItem || isAvailableItem ? { title: "----" } : null,
{ title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem },
{ title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted },
{ title: "----" },
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset}
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset }
];
return items.filter(row => row !== null);
return items.filter((row) => row !== null);
}
async selectMenuItemHandler({command}: MenuCommandItem<LauncherCommandNames>) {
async selectMenuItemHandler({ command }: MenuCommandItem<LauncherCommandNames>) {
if (!command) {
return;
}
if (command === 'resetLauncher') {
if (command === "resetLauncher") {
const confirmed = await dialogService.confirm(t("launcher_context_menu.reset_launcher_confirm", { title: this.node.title }));
if (confirmed) {

View File

@@ -1,33 +1,31 @@
import { t } from "../services/i18n.js";
import contextMenu from "./context_menu.js";
import appContext from "../components/app_context.js";
import { ViewScope } from "../services/link.js";
import type { ViewScope } from "../services/link.js";
function openContextMenu(notePath: string, e: PointerEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
function openContextMenu(notePath: string, e: PointerEvent | JQuery.ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
{title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external"},
{title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
{title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
],
selectMenuItemHandler: ({command}) => {
selectMenuItemHandler: ({ command }) => {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
}
if (command === 'openNoteInNewTab') {
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
}
else if (command === 'openNoteInNewSplit') {
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1];
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath, hoistedNoteId, viewScope});
}
else if (command === 'openNoteInNewWindow') {
appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId, viewScope});
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
}
}
});
@@ -35,4 +33,4 @@ function openContextMenu(notePath: string, e: PointerEvent, viewScope: ViewScope
export default {
openContextMenu
}
};

View File

@@ -1,17 +1,17 @@
import treeService, { Node } from '../services/tree.js';
import treeService, { type Node } from "../services/tree.js";
import froca from "../services/froca.js";
import clipboard from '../services/clipboard.js';
import clipboard from "../services/clipboard.js";
import noteCreateService from "../services/note_create.js";
import contextMenu, { MenuCommandItem, MenuItem } from "./context_menu.js";
import appContext, { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
import noteTypesService from "../services/note_types.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
import NoteTreeWidget from '../widgets/note_tree.js';
import FAttachment from '../entities/fattachment.js';
import { SelectMenuItemEventListener } from '../components/events.js';
import NoteTreeWidget from "../widgets/note_tree.js";
import FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
// TODO: Deduplicate once client/server is well split.
interface ConvertToAttachmentResponse {
@@ -21,7 +21,6 @@ interface ConvertToAttachmentResponse {
type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData>;
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
private treeWidget: NoteTreeWidget;
private node: Node;
@@ -36,13 +35,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
y: e.pageY,
items: await this.getMenuItems(),
selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item)
})
});
}
async getMenuItems(): Promise<MenuItem<TreeCommandNames>[]> {
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const branch = froca.getBranch(this.node.data.branchId);
const isNotRoot = note?.noteId !== 'root';
const isNotRoot = note?.noteId !== "root";
const isHoisted = note?.noteId === appContext.tabManager.getActiveContext().hoistedNoteId;
const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null;
@@ -50,12 +49,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
// the only exception is when the only selected note is the one that was right-clicked, then
// it's clear what the user meant to do.
const selNodes = this.treeWidget.getSelectedNodes();
const noSelectedNotes = selNodes.length === 0
|| (selNodes.length === 1 && selNodes[0] === this.node);
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== 'search';
const notSearch = note?.type !== "search";
const notOptions = !note?.noteId.startsWith("_options");
const parentNotSearch = !parentNote || parentNote.type !== 'search';
const parentNotSearch = !parentNote || parentNote.type !== "search";
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
const items: (MenuItem<TreeCommandNames> | null)[] = [
@@ -63,117 +61,161 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bxs-chevrons-up", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
isHoisted
? null
: {
title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`,
command: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
!isHoisted || !isNotRoot
? null
: { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: "----" },
{ title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
{
title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`,
command: "insertNoteAfter",
uiIcon: "bx bx-plus",
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions },
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions
},
{ title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
{
title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`,
command: "insertChildNote",
uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptions },
enabled: notSearch && noSelectedNotes && notOptions
},
{ title: "----" },
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
{ title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ title: "----" },
{
title: t("tree-context-menu.advanced"),
uiIcon: "bx bxs-wrench",
enabled: true,
items: [
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
{ title: "----" },
{
title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`,
command: "editBranchPrefix",
uiIcon: "bx bx-rename",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions
},
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions },
{
title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
command: "duplicateSubtree",
uiIcon: "bx bx-outline",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions
},
{ title: "----" },
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{
title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`,
command: "sortChildNotes",
uiIcon: "bx bx-sort-down",
enabled: noSelectedNotes && notSearch
},
{ title: "----" },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions }
]
},
{ title: "----" },
{
title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`,
command: "cutNotesToClipboard",
uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch
},
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bxs-wrench", enabled: true, items: [
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted },
{ title: "----" },
{
title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`,
command: "pasteNotesFromClipboard",
uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes
},
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-rename", enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions },
{ title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
{
title: t("tree-context-menu.paste-after"),
command: "pasteNotesAfterFromClipboard",
uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes
},
{ title: "----" },
{
title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`,
command: "moveNotesTo",
uiIcon: "bx bx-transfer",
enabled: isNotRoot && !isHoisted && parentNotSearch
},
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-sort-down", enabled: noSelectedNotes && notSearch },
{ title: "----" },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions },
] },
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
{
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
command: "deleteNotes",
uiIcon: "bx bx-trash destructive-action-icon",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions
},
{ title: "----" },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-transfer",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptions },
{ title: "----" },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: "----" },
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes },
{
title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`,
command: "searchInSubtree",
uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes
}
];
return items.filter(row => row !== null);
return items.filter((row) => row !== null);
}
async selectMenuItemHandler({command, type, templateNoteId}: MenuCommandItem<TreeCommandNames>) {
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
const notePath = treeService.getNotePath(this.node);
if (command === 'openInTab') {
if (command === "openInTab") {
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
else if (command === "insertNoteAfter") {
} else if (command === "insertNoteAfter") {
const parentNotePath = treeService.getNotePath(this.node.getParent());
const isProtected = treeService.getParentProtectedStatus(this.node);
noteCreateService.createNote(parentNotePath, {
target: 'after',
target: "after",
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId
});
}
else if (command === "insertChildNote") {
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
@@ -181,15 +223,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId
});
}
else if (command === 'openNoteInSplit') {
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1];
const { ntxId } = subContexts[subContexts.length - 1];
this.treeWidget.triggerCommand("openNewNoteSplit", {ntxId, notePath});
}
else if (command === 'convertNoteToAttachment') {
if (!await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm"))) {
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return;
}
@@ -199,7 +239,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const note = await froca.getNote(noteId);
if (note?.isEligibleForConversionToAttachment()) {
const {attachment} = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
const { attachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
if (attachment) {
converted++;
@@ -208,11 +248,9 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
}
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
}
else if (command === 'copyNotePathToClipboard') {
navigator.clipboard.writeText('#' + notePath);
}
else if (command) {
} else if (command === "copyNotePathToClipboard") {
navigator.clipboard.writeText("#" + notePath);
} else if (command) {
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
node: this.node,
notePath: notePath,

View File

@@ -1,8 +1,8 @@
import appContext from "./components/app_context.js";
import noteAutocompleteService from './services/note_autocomplete.js';
import noteAutocompleteService from "./services/note_autocomplete.js";
import glob from "./services/glob.js";
glob.setupGlobs()
glob.setupGlobs();
await appContext.earlyInit();

View File

@@ -1,4 +1,4 @@
import { EntityRowNames } from "./services/load_results.js";
import type { EntityRowNames } from "./services/load_results.js";
// TODO: Deduplicate with src/services/entity_changes_interface.ts
export interface EntityChange {

View File

@@ -1,4 +1,4 @@
import { AttributeType } from "../entities/fattribute.js";
import type { AttributeType } from "../entities/fattribute.js";
import server from "./server.js";
interface InitOptions {
@@ -15,29 +15,34 @@ interface InitOptions {
*/
function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) {
if (!$el.hasClass("aa-input")) {
$el.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
}, [{
displayKey: 'name',
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
cache: false,
source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "name",
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
cache: false,
source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
const result = names.map(name => ({name}));
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
const result = names.map((name) => ({ name }));
cb(result);
}
}]);
cb(result);
}
}
]
);
$el.on('autocomplete:opened', () => {
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
$el.autocomplete("close");
}
});
}
@@ -51,7 +56,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio
if ($el.hasClass("aa-input")) {
// we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was reset
$el.autocomplete('destroy');
$el.autocomplete("destroy");
}
let attributeName = "";
@@ -63,34 +68,38 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio
return;
}
const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`))
.map(attribute => ({ value: attribute }));
const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`)).map((attribute) => ({ value: attribute }));
if (attributeValues.length === 0) {
return;
}
$el.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
openOnFocus: false, // handled manually
minLength: 0,
tabAutocomplete: false
}, [{
displayKey: 'value',
cache: false,
source: async function (term, cb) {
term = term.toLowerCase();
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: false, // handled manually
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "value",
cache: false,
source: async function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));
cb(filtered);
}
}]);
cb(filtered);
}
}
]
);
$el.on('autocomplete:opened', () => {
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
$el.autocomplete("close");
}
});
@@ -102,4 +111,4 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio
export default {
initAttributeNameAutocomplete,
initLabelValueAutocomplete
}
};

View File

@@ -1,4 +1,4 @@
import FAttribute, { AttributeType, FAttributeRow } from "../entities/fattribute.js";
import type { AttributeType, FAttributeRow } from "../entities/fattribute.js";
import utils from "./utils.js";
interface Token {
@@ -23,17 +23,16 @@ function lex(str: string) {
const tokens: Token[] = [];
let quotes: boolean | string = false;
let currentWord = '';
let currentWord = "";
function isOperatorSymbol(chr: string) {
return ['=', '*', '>', '<', '!'].includes(chr);
return ["=", "*", ">", "<", "!"].includes(chr);
}
function previousOperatorSymbol() {
if (currentWord.length === 0) {
return false;
}
else {
} else {
return isOperatorSymbol(currentWord[currentWord.length - 1]);
}
}
@@ -42,7 +41,7 @@ function lex(str: string) {
* @param endIndex - index of the last character of the token
*/
function finishWord(endIndex: number) {
if (currentWord === '') {
if (currentWord === "") {
return;
}
@@ -52,54 +51,47 @@ function lex(str: string) {
endIndex: endIndex
});
currentWord = '';
currentWord = "";
}
for (let i = 0; i < str.length; i++) {
const chr = str[i];
if (chr === '\\') {
if ((i + 1) < str.length) {
if (chr === "\\") {
if (i + 1 < str.length) {
i++;
currentWord += str[i];
}
else {
} else {
currentWord += chr;
}
continue;
}
else if (['"', "'", '`'].includes(chr)) {
} else if (['"', "'", "`"].includes(chr)) {
if (!quotes) {
if (previousOperatorSymbol()) {
finishWord(i - 1);
}
quotes = chr;
}
else if (quotes === chr) {
} else if (quotes === chr) {
quotes = false;
finishWord(i - 1);
}
else {
} else {
// it's a quote, but within other kind of quotes, so it's valid as a literal character
currentWord += chr;
}
continue;
}
else if (!quotes) {
if (currentWord.length === 0 && (chr === '#' || chr === '~')) {
} else if (!quotes) {
if (currentWord.length === 0 && (chr === "#" || chr === "~")) {
currentWord = chr;
continue;
}
else if (chr === ' ') {
} else if (chr === " ") {
finishWord(i - 1);
continue;
}
else if (['(', ')'].includes(chr)) {
} else if (["(", ")"].includes(chr)) {
finishWord(i - 1);
currentWord = chr;
@@ -107,8 +99,7 @@ function lex(str: string) {
finishWord(i);
continue;
}
else if (previousOperatorSymbol() !== isOperatorSymbol(chr)) {
} else if (previousOperatorSymbol() !== isOperatorSymbol(chr)) {
finishWord(i - 1);
currentWord += chr;
@@ -149,27 +140,22 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
const { text, startIndex } = tokens[i];
function isInheritable() {
if (tokens.length > i + 3
&& tokens[i + 1].text === '('
&& tokens[i + 2].text === 'inheritable'
&& tokens[i + 3].text === ')') {
if (tokens.length > i + 3 && tokens[i + 1].text === "(" && tokens[i + 2].text === "inheritable" && tokens[i + 3].text === ")") {
i += 3;
return true;
}
else {
} else {
return false;
}
}
if (text.startsWith('#')) {
if (text.startsWith("#")) {
const labelName = text.substr(1);
checkAttributeName(labelName);
const attr: Attribute = {
type: 'label',
type: "label",
name: labelName,
isInheritable: isInheritable(),
startIndex: startIndex,
@@ -188,14 +174,13 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
}
attrs.push(attr);
}
else if (text.startsWith('~')) {
} else if (text.startsWith("~")) {
const relationName = text.substr(1);
checkAttributeName(relationName);
const attr: Attribute = {
type: 'relation',
type: "relation",
name: relationName,
isInheritable: isInheritable(),
startIndex: startIndex,
@@ -204,11 +189,10 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
attrs.push(attr);
if (i + 2 >= tokens.length || tokens[i + 1].text !== '=') {
if (i + 2 >= tokens.length || tokens[i + 1].text !== "=") {
if (allowEmptyRelations) {
break;
}
else {
} else {
throw new Error(`Relation "${text}" in ${context(i)} should point to a note.`);
}
}
@@ -220,12 +204,11 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
notePath = notePath.substr(1);
}
const noteId = notePath.split('/').pop();
const noteId = notePath.split("/").pop();
attr.value = noteId;
attr.endIndex = tokens[i].endIndex;
}
else {
} else {
throw new Error(`Invalid attribute "${text}" in ${context(i)}`);
}
}
@@ -243,4 +226,4 @@ export default {
lex,
parse,
lexAndParse
}
};

View File

@@ -4,17 +4,17 @@ import FAttribute from "../entities/fattribute.js";
import FNote from "../entities/fnote.js";
async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) {
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : '';
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : "";
const $attr = $("<span>");
if (attribute.type === 'label') {
if (attribute.type === "label") {
$attr.append(document.createTextNode(`#${attribute.name}${isInheritable}`));
if (attribute.value) {
$attr.append('=');
$attr.append("=");
$attr.append(document.createTextNode(formatValue(attribute.value)));
}
} else if (attribute.type === 'relation') {
} else if (attribute.type === "relation") {
if (attribute.isAutoLink) {
return $attr;
}
@@ -38,17 +38,13 @@ async function renderAttribute(attribute: FAttribute, renderIsInheritable: boole
function formatValue(val: string) {
if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) {
return val;
}
else if (!val.includes('"')) {
} else if (!val.includes('"')) {
return `"${val}"`;
}
else if (!val.includes("'")) {
} else if (!val.includes("'")) {
return `'${val}'`;
}
else if (!val.includes("`")) {
} else if (!val.includes("`")) {
return `\`${val}\``;
}
else {
} else {
return `"${val.replace(/"/g, '\\"')}"`;
}
}
@@ -62,9 +58,8 @@ async function createLink(noteId: string) {
return $("<a>", {
href: `#root/${noteId}`,
class: 'reference-link'
})
.text(note.title);
class: "reference-link"
}).text(note.title);
}
async function renderAttributes(attributes: FAttribute[], renderIsInheritable: boolean) {
@@ -84,31 +79,16 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
return $container;
}
const HIDDEN_ATTRIBUTES = [
'originalFileName',
'fileSize',
'template',
'inherit',
'cssClass',
'iconClass',
'pageSize',
'viewType'
];
const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType"];
async function renderNormalAttributes(note: FNote) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
let attrs = note.getAttributes();
if (promotedDefinitionAttributes.length > 0) {
attrs = attrs.filter(attr => !!promotedDefinitionAttributes.find(promAttr => promAttr.isDefinitionFor(attr)));
}
else {
attrs = attrs.filter(
attr => !attr.isDefinition()
&& !attr.isAutoLink
&& !HIDDEN_ATTRIBUTES.includes(attr.name)
&& attr.noteId === note.noteId
);
attrs = attrs.filter((attr) => !!promotedDefinitionAttributes.find((promAttr) => promAttr.isDefinitionFor(attr)));
} else {
attrs = attrs.filter((attr) => !attr.isDefinition() && !attr.isAutoLink && !HIDDEN_ATTRIBUTES.includes(attr.name) && attr.noteId === note.noteId);
}
const $renderedAttributes = await renderAttributes(attrs, false);
@@ -116,11 +96,11 @@ async function renderNormalAttributes(note: FNote) {
return {
count: attrs.length,
$renderedAttributes
}
};
}
export default {
renderAttribute,
renderAttributes,
renderNormalAttributes
}
};

View File

@@ -1,11 +1,11 @@
import server from './server.js';
import froca from './froca.js';
import FNote from '../entities/fnote.js';
import { AttributeRow } from './load_results.js';
import server from "./server.js";
import froca from "./froca.js";
import FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js";
async function addLabel(noteId: string, name: string, value: string = "") {
await server.put(`notes/${noteId}/attribute`, {
type: 'label',
type: "label",
name: name,
value: value
});
@@ -13,7 +13,7 @@ async function addLabel(noteId: string, name: string, value: string = "") {
async function setLabel(noteId: string, name: string, value: string = "") {
await server.put(`notes/${noteId}/set-attribute`, {
type: 'label',
type: "label",
name: name,
value: value
});
@@ -68,4 +68,4 @@ export default {
setLabel,
removeAttributeById,
isAffecting
}
};

View File

@@ -1,13 +1,13 @@
import utils from './utils.js';
import server from './server.js';
import toastService, { ToastOptions } from "./toast.js";
import utils from "./utils.js";
import server from "./server.js";
import toastService, { type ToastOptions } from "./toast.js";
import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { t } from './i18n.js';
import { Node } from './tree.js';
import { ResolveOptions } from '../widgets/dialogs/delete_notes.js';
import { t } from "./i18n.js";
import type { Node } from "./tree.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
// TODO: Deduplicate type with server
interface Response {
@@ -48,13 +48,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
return;
}
const forbiddenNoteIds = [
'root',
hoistedNoteService.getHoistedNoteId(),
'_lbRoot',
'_lbAvailableLaunchers',
'_lbVisibleLaunchers'
];
const forbiddenNoteIds = ["root", hoistedNoteService.getHoistedNoteId(), "_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"];
if (forbiddenNoteIds.includes(afterNote.noteId)) {
toastService.showError(t("branches.cannot-move-notes-here"));
@@ -79,7 +73,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
return;
}
if (newParentBranch.noteId === '_lbRoot') {
if (newParentBranch.noteId === "_lbRoot") {
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -89,9 +83,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
for (const branchIdToMove of branchIdsToMove) {
const branchToMove = froca.getBranch(branchIdToMove);
if (!branchToMove
|| branchToMove.noteId === hoistedNoteService.getHoistedNoteId()
|| (await branchToMove.getParentNote())?.type === 'search') {
if (!branchToMove || branchToMove.noteId === hoistedNoteService.getHoistedNoteId() || (await branchToMove.getParentNote())?.type === "search") {
continue;
}
@@ -116,10 +108,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
if (utils.isMobile()) {
proceed = true;
deleteAllClones = false;
}
else {
({proceed, deleteAllClones, eraseNotes} = await new Promise<ResolveOptions>(res =>
appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res, forceDeleteAllClones})));
} else {
({ proceed, deleteAllClones, eraseNotes } = await new Promise<ResolveOptions>((res) =>
appContext.triggerCommand("showDeleteNotesDialog", { branchIdsToDelete, callback: res, forceDeleteAllClones })
));
}
if (!proceed) {
@@ -128,8 +120,7 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
try {
await activateParentNotePath();
}
catch (e) {
} catch (e) {
console.error(e);
}
@@ -141,7 +132,7 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
counter++;
const last = counter === branchIdsToDelete.length;
const query = `?taskId=${taskId}&eraseNotes=${eraseNotes ? 'true' : 'false'}&last=${last ? 'true' : 'false'}`;
const query = `?taskId=${taskId}&eraseNotes=${eraseNotes ? "true" : "false"}&last=${last ? "true" : "false"}`;
const branch = froca.getBranch(branchIdToDelete);
@@ -170,9 +161,7 @@ async function activateParentNotePath() {
}
async function moveNodeUpInHierarchy(node: Node) {
if (hoistedNoteService.isHoistedNode(node)
|| hoistedNoteService.isTopLevelNode(node)
|| node.getParent().data.noteType === 'search') {
if (hoistedNoteService.isHoistedNode(node) || hoistedNoteService.isTopLevelNode(node) || node.getParent().data.noteType === "search") {
return;
}
@@ -193,20 +182,19 @@ async function moveNodeUpInHierarchy(node: Node) {
}
function filterSearchBranches(branchIds: string[]) {
return branchIds.filter(branchId => !branchId.startsWith('virt-'));
return branchIds.filter((branchId) => !branchId.startsWith("virt-"));
}
function filterRootNote(branchIds: string[]) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
return branchIds.filter(branchId => {
return branchIds.filter((branchId) => {
const branch = froca.getBranch(branchId);
if (!branch) {
return false;
}
return branch.noteId !== 'root'
&& branch.noteId !== hoistedNoteId;
return branch.noteId !== "root" && branch.noteId !== hoistedNoteId;
});
}
@@ -219,17 +207,17 @@ function makeToast(id: string, message: string): ToastOptions {
};
}
ws.subscribeToMessages(async message => {
if (message.taskType !== 'deleteNotes') {
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "deleteNotes") {
return;
}
if (message.type === 'taskError') {
if (message.type === "taskError") {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
} else if (message.type === "taskProgressCount") {
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
toast.closeAfter = 5000;
@@ -237,17 +225,17 @@ ws.subscribeToMessages(async message => {
}
});
ws.subscribeToMessages(async message => {
if (message.taskType !== 'undeleteNotes') {
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "undeleteNotes") {
return;
}
if (message.type === 'taskError') {
if (message.type === "taskError") {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
} else if (message.type === "taskProgressCount") {
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
toast.closeAfter = 5000;
@@ -292,5 +280,5 @@ export default {
moveNodeUpInHierarchy,
cloneNoteAfter,
cloneNoteToBranch,
cloneNoteToParentNote,
cloneNoteToParentNote
};

View File

@@ -27,7 +27,7 @@ const ACTION_GROUPS = [
},
{
title: t("bulk_actions.notes"),
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction],
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction]
},
{
title: t("bulk_actions.other"),
@@ -53,8 +53,8 @@ const ACTION_CLASSES = [
async function addAction(noteId: string, actionName: string) {
await server.post(`notes/${noteId}/attributes`, {
type: 'label',
name: 'action',
type: "label",
name: "action",
value: JSON.stringify({
name: actionName
})
@@ -64,28 +64,29 @@ async function addAction(noteId: string, actionName: string) {
}
function parseActions(note: FNote) {
const actionLabels = note.getLabels('action');
const actionLabels = note.getLabels("action");
return actionLabels.map(actionAttr => {
let actionDef;
return actionLabels
.map((actionAttr) => {
let actionDef;
try {
actionDef = JSON.parse(actionAttr.value);
} catch (e: any) {
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
return null;
}
try {
actionDef = JSON.parse(actionAttr.value);
} catch (e: any) {
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
return null;
}
const ActionClass = ACTION_CLASSES.find(actionClass => actionClass.actionName === actionDef.name);
const ActionClass = ACTION_CLASSES.find((actionClass) => actionClass.actionName === actionDef.name);
if (!ActionClass) {
logError(`No action class for '${actionDef.name}' found.`);
return null;
}
if (!ActionClass) {
logError(`No action class for '${actionDef.name}' found.`);
return null;
}
return new ActionClass(actionAttr, actionDef);
})
.filter(action => !!action);
return new ActionClass(actionAttr, actionDef);
})
.filter((action) => !!action);
}
export default {

View File

@@ -4,7 +4,7 @@ import toastService from "./toast.js";
import froca from "./froca.js";
import utils from "./utils.js";
import { t } from "./i18n.js";
import { Entity } from "./frontend_script_api.js";
import type { Entity } from "./frontend_script_api.js";
// TODO: Deduplicate with server.
export interface Bundle {
@@ -31,9 +31,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
try {
return await (function () {
return await function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext));
}.call(apiContext);
} catch (e: any) {
const note = await froca.getNote(bundle.noteId);
@@ -51,7 +51,6 @@ async function executeStartupBundles() {
}
class WidgetsByParent {
private byParent: Record<string, Widget[]>;
constructor() {
@@ -73,11 +72,13 @@ class WidgetsByParent {
return [];
}
return this.byParent[parentName]
// previously, custom widgets were provided as a single instance, but that has the disadvantage
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
// https://github.com/zadam/trilium/issues/4274
.map((w: any) => w.prototype ? new w() : w);
return (
this.byParent[parentName]
// previously, custom widgets were provided as a single instance, but that has the disadvantage
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
// https://github.com/zadam/trilium/issues/4274
.map((w: any) => (w.prototype ? new w() : w))
);
}
}
@@ -121,4 +122,4 @@ export default {
getAndExecuteBundle,
executeStartupBundles,
getWidgetBundlesByParent
}
};

View File

@@ -13,14 +13,13 @@ async function pasteAfter(afterBranchId: string) {
return;
}
if (clipboardMode === 'cut') {
if (clipboardMode === "cut") {
await branchService.moveAfterBranch(clipboardBranchIds, afterBranchId);
clipboardBranchIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId));
} else if (clipboardMode === "copy") {
const clipboardBranches = clipboardBranchIds.map((branchId) => froca.getBranch(branchId));
for (const clipboardBranch of clipboardBranches) {
if (!clipboardBranch) {
@@ -36,8 +35,7 @@ async function pasteAfter(afterBranchId: string) {
}
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
}
else {
} else {
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
}
}
@@ -47,14 +45,13 @@ async function pasteInto(parentBranchId: string) {
return;
}
if (clipboardMode === 'cut') {
if (clipboardMode === "cut") {
await branchService.moveToParentNote(clipboardBranchIds, parentBranchId);
clipboardBranchIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId));
} else if (clipboardMode === "copy") {
const clipboardBranches = clipboardBranchIds.map((branchId) => froca.getBranch(branchId));
for (const clipboardBranch of clipboardBranches) {
if (!clipboardBranch) {
@@ -70,19 +67,18 @@ async function pasteInto(parentBranchId: string) {
}
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
}
else {
} else {
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
}
}
async function copy(branchIds: string[]) {
clipboardBranchIds = branchIds;
clipboardMode = 'copy';
clipboardMode = "copy";
if (utils.isElectron()) {
// https://github.com/zadam/trilium/issues/2401
const {clipboard} = require('electron');
const { clipboard } = require("electron");
const links = [];
for (const branch of froca.getBranches(clipboardBranchIds)) {
@@ -90,7 +86,7 @@ async function copy(branchIds: string[]) {
links.push($link[0].outerHTML);
}
clipboard.writeHTML(links.join(', '));
clipboard.writeHTML(links.join(", "));
}
toastService.showMessage(t("clipboard.copied"));
@@ -100,14 +96,14 @@ function cut(branchIds: string[]) {
clipboardBranchIds = branchIds;
if (clipboardBranchIds.length > 0) {
clipboardMode = 'cut';
clipboardMode = "cut";
toastService.showMessage(t("clipboard.cut"));
}
}
function isClipboardEmpty() {
clipboardBranchIds = clipboardBranchIds.filter(branchId => !!froca.getBranch(branchId));
clipboardBranchIds = clipboardBranchIds.filter((branchId) => !!froca.getBranch(branchId));
return clipboardBranchIds.length === 0;
}
@@ -118,4 +114,4 @@ export default {
cut,
copy,
isClipboardEmpty
}
};

View File

@@ -11,8 +11,8 @@ import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
import mime_types from "./mime_types.js";
import { loadElkIfNeeded } from "./mermaid.js";
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js";
let idCounter = 1;
@@ -22,54 +22,42 @@ interface Options {
imageHasZoom?: boolean;
}
const CODE_MIME_TYPES = new Set([
"application/json"
]);
const CODE_MIME_TYPES = new Set(["application/json"]);
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, options: Options = {}) {
options = Object.assign({
tooltip: false
}, options);
options = Object.assign(
{
tooltip: false
},
options
);
const type = getRenderingType(entity);
// attachment supports only image and file/pdf/audio/video
const $renderedContent = $('<div class="rendered-content">');
if (type === 'text') {
if (type === "text") {
await renderText(entity, $renderedContent);
}
else if (type === 'code') {
} else if (type === "code") {
await renderCode(entity, $renderedContent);
}
else if (['image', 'canvas', 'mindMap'].includes(type)) {
} else if (["image", "canvas", "mindMap"].includes(type)) {
renderImage(entity, $renderedContent, options);
}
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
} else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) {
renderFile(entity, type, $renderedContent);
}
else if (type === 'mermaid') {
} else if (type === "mermaid") {
await renderMermaid(entity, $renderedContent);
}
else if (type === 'render') {
const $content = $('<div>');
} else if (type === "render") {
const $content = $("<div>");
await renderService.render(entity, $content);
$renderedContent.append($content);
}
else if (!options.tooltip && type === 'protectedSession') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
} else if (!options.tooltip && type === "protectedSession") {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`).on("click", protectedSessionService.enterProtectedSession);
$renderedContent.append(
$("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button)
);
}
else if (entity instanceof FNote) {
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
} else if (entity instanceof FNote) {
$renderedContent.append(
$("<div>")
.css("display", "flex")
@@ -98,13 +86,13 @@ async function renderText(note: FNote, $renderedContent: JQuery<HTMLElement>) {
if (blob && !utils.isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
if ($renderedContent.find('span.math-tex').length > 0) {
if ($renderedContent.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement($renderedContent[0], {trust: true});
renderMathInElement($renderedContent[0], { trust: true });
}
const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr('href') || "");
const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr("href") || "");
const referenceLinks = $renderedContent.find("a.reference-link");
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
await froca.getNotes(noteIdsToPrefetch);
@@ -128,7 +116,7 @@ async function renderCode(note: FNote, $renderedContent: JQuery<HTMLElement>) {
const $codeBlock = $("<code>");
$codeBlock.text(blob?.content || "");
$renderedContent.append($("<pre>").append($codeBlock));
await applySingleBlockSyntaxHighlight($codeBlock, mime_types.normalizeMimeTypeForCKEditor(note.mime));
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
}
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
@@ -143,9 +131,9 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
}
$renderedContent // styles needed for the zoom to work well
.css('display', 'flex')
.css('align-items', 'center')
.css('justify-content', 'center');
.css("display", "flex")
.css("align-items", "center")
.css("justify-content", "center");
const $img = $("<img>")
.attr("src", url || "")
@@ -171,10 +159,10 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
let entityType, entityId;
if (entity instanceof FNote) {
entityType = 'notes';
entityType = "notes";
entityId = entity.noteId;
} else if (entity instanceof FAttachment) {
entityType = 'attachments';
entityType = "attachments";
entityId = entity.attachmentId;
} else {
throw new Error(`Can't recognize entity type of '${entity}'`);
@@ -182,20 +170,20 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
const $content = $('<div style="display: flex; flex-direction: column; height: 100%;">');
if (type === 'pdf') {
if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
$content.append($pdfPreview);
} else if (type === 'audio') {
const $audioPreview = $('<audio controls></audio>')
} else if (type === "audio") {
const $audioPreview = $("<audio controls></audio>")
.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`))
.attr("type", entity.mime)
.css("width", "100%");
$content.append($audioPreview);
} else if (type === 'video') {
const $videoPreview = $('<video controls></video>')
} else if (type === "video") {
const $videoPreview = $("<video controls></video>")
.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`))
.attr("type", entity.mime)
.css("width", "100%");
@@ -203,22 +191,18 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
$content.append($videoPreview);
}
if (entityType === 'notes' && "noteId" in entity) {
if (entityType === "notes" && "noteId" in entity) {
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
// in attachment list
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => openService.downloadFileNote(entity.noteId));
$openButton.on('click', () => openService.openNoteExternally(entity.noteId, entity.mime));
$downloadButton.on("click", () => openService.downloadFileNote(entity.noteId));
$openButton.on("click", () => openService.openNoteExternally(entity.noteId, entity.mime));
// open doesn't work for protected notes since it works through a browser which isn't in protected session
$openButton.toggle(!entity.isProtected);
$content.append(
$('<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">')
.append($downloadButton)
.append($openButton)
);
$content.append($('<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">').append($downloadButton).append($openButton));
}
$renderedContent.append($content);
@@ -230,18 +214,16 @@ async function renderMermaid(note: FNote, $renderedContent: JQuery<HTMLElement>)
const blob = await note.getBlob();
const content = blob?.content || "";
$renderedContent
.css("display", "flex")
.css("justify-content", "space-around");
$renderedContent.css("display", "flex").css("justify-content", "space-around");
const documentStyle = window.getComputedStyle(document.documentElement);
const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme');
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme");
mermaid.mermaidAPI.initialize({startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: 'antiscript'});
mermaid.mermaidAPI.initialize({ startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: "antiscript" });
try {
await loadElkIfNeeded(content);
const {svg} = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
$renderedContent.append($(svg));
} catch (e) {
@@ -270,10 +252,12 @@ async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: F
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
$renderedContent.append(await linkService.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,
showNoteIcon: true
}));
$renderedContent.append(
await linkService.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,
showNoteIcon: true
})
);
$renderedContent.append("<br>");
}
@@ -287,24 +271,23 @@ function getRenderingType(entity: FNote | FAttachment) {
type = entity.role;
}
const mime = ("mime" in entity && entity.mime);
const mime = "mime" in entity && entity.mime;
if (type === 'file' && mime === 'application/pdf') {
type = 'pdf';
} else if (type === "file" && mime && CODE_MIME_TYPES.has(mime) ) {
if (type === "file" && mime === "application/pdf") {
type = "pdf";
} else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) {
type = "code";
} else if (type === 'file' && mime && mime.startsWith('audio/')) {
type = 'audio';
} else if (type === 'file' && mime && mime.startsWith('video/')) {
type = 'video';
} else if (type === "file" && mime && mime.startsWith("audio/")) {
type = "audio";
} else if (type === "file" && mime && mime.startsWith("video/")) {
type = "video";
}
if (entity.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protectedSession';
} else {
type = "protectedSession";
}
}

View File

@@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { FNoteRow } from "../entities/fnote.js";
import type { FNoteRow } from "../entities/fnote.js";
import froca from "./froca.js";
import server from "./server.js";
import ws from "./ws.js";
@@ -47,7 +47,7 @@ async function getYearNote(year: string) {
}
async function createSqlConsole() {
const note = await server.post<FNoteRow>('special-notes/sql-console');
const note = await server.post<FNoteRow>("special-notes/sql-console");
await ws.waitForMaxKnownEntityChangeId();
@@ -55,7 +55,7 @@ async function createSqlConsole() {
}
async function createSearchNote(opts = {}) {
const note = await server.post<FNoteRow>('special-notes/search-note', opts);
const note = await server.post<FNoteRow>("special-notes/search-note", opts);
await ws.waitForMaxKnownEntityChangeId();
@@ -71,4 +71,4 @@ export default {
getYearNote,
createSqlConsole,
createSearchNote
}
};

View File

@@ -48,14 +48,14 @@ function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate:
return result;
};
debounced.clear = function() {
debounced.clear = function () {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
debounced.flush = function() {
debounced.flush = function () {
if (timeout) {
result = func.apply(context, args || []);
context = args = null;

View File

@@ -1,28 +1,26 @@
import appContext from "../components/app_context.js";
import { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
import { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import type { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
async function info(message: string) {
return new Promise(res =>
appContext.triggerCommand("showInfoDialog", {message, callback: res}));
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
}
async function confirm(message: string) {
return new Promise(res =>
return new Promise((res) =>
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
message,
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
}));
})
);
}
async function confirmDeleteNoteBoxWithNote(title: string) {
return new Promise(res =>
appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", {title, callback: res}));
return new Promise((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
}
async function prompt(props: PromptDialogOptions) {
return new Promise(res =>
appContext.triggerCommand("showPromptDialog", {...props, callback: res}));
return new Promise<string | null>((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res }));
}
export default {

View File

@@ -16,7 +16,7 @@ const fileModificationStatus: Record<string, Record<string, Message>> = {
};
function checkType(type: string) {
if (type !== 'notes' && type !== 'attachments') {
if (type !== "notes" && type !== "attachments") {
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
}
}
@@ -40,7 +40,7 @@ function ignoreModification(entityType: string, entityId: string) {
}
ws.subscribeToMessages(async (message: Message) => {
if (message.type !== 'openedFileUpdated') {
if (message.type !== "openedFileUpdated") {
return;
}
@@ -48,7 +48,7 @@ ws.subscribeToMessages(async (message: Message) => {
fileModificationStatus[message.entityType][message.entityId] = message;
appContext.triggerEvent('openedFileUpdated', {
appContext.triggerEvent("openedFileUpdated", {
entityType: message.entityType,
entityId: message.entityId,
lastModifiedMs: message.lastModifiedMs,
@@ -60,4 +60,4 @@ export default {
getFileModificationStatus,
fileModificationUploaded,
ignoreModification
}
};

View File

@@ -21,4 +21,4 @@ export interface Froca {
getBranches(branchIds: string[], silentNotFoundError?: boolean): FBranch[];
getAttachmentsForNote(noteId: string): Promise<FAttachment[]>;
}
}

View File

@@ -1,12 +1,11 @@
import FBranch, { FBranchRow } from "../entities/fbranch.js";
import FNote, { FNoteRow } from "../entities/fnote.js";
import FAttribute, { FAttributeRow } from "../entities/fattribute.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FNote, { type FNoteRow } from "../entities/fnote.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import server from "./server.js";
import appContext from "../components/app_context.js";
import FBlob, { FBlobRow } from "../entities/fblob.js";
import FAttachment, { FAttachmentRow } from "../entities/fattachment.js";
import { Froca } from "./froca-interface.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import type { Froca } from "./froca-interface.js";
interface SubtreeResponse {
notes: FNoteRow[];
@@ -44,7 +43,7 @@ class FrocaImpl implements Froca {
}
async loadInitialTree() {
const resp = await server.get<SubtreeResponse>('tree');
const resp = await server.get<SubtreeResponse>("tree");
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
@@ -73,7 +72,7 @@ class FrocaImpl implements Froca {
const noteIdsToSort = new Set<string>();
for (const noteRow of noteRows) {
const {noteId} = noteRow;
const { noteId } = noteRow;
let note = this.notes[noteId];
@@ -81,12 +80,12 @@ class FrocaImpl implements Froca {
note.update(noteRow);
// search note doesn't have child branches in the database and all the children are virtual branches
if (note.type !== 'search') {
if (note.type !== "search") {
for (const childNoteId of note.children) {
const childNote = this.notes[childNoteId];
if (childNote) {
childNote.parents = childNote.parents.filter(p => p !== noteId);
childNote.parents = childNote.parents.filter((p) => p !== noteId);
delete this.branches[childNote.parentToBranch[noteId]];
delete childNote.parentToBranch[noteId];
@@ -99,7 +98,7 @@ class FrocaImpl implements Froca {
// we want to remove all "real" branches (represented in the database) since those will be created
// from branches argument but want to preserve all virtual ones from saved search
note.parents = note.parents.filter(parentNoteId => {
note.parents = note.parents.filter((parentNoteId) => {
const parentNote = this.notes[parentNoteId];
const branch = this.branches[parentNote.childToBranch[noteId]];
@@ -111,15 +110,14 @@ class FrocaImpl implements Froca {
return true;
}
parentNote.children = parentNote.children.filter(p => p !== noteId);
parentNote.children = parentNote.children.filter((p) => p !== noteId);
delete this.branches[parentNote.childToBranch[noteId]];
delete parentNote.childToBranch[noteId];
return false;
});
}
else {
} else {
this.notes[noteId] = new FNote(this, noteRow);
}
}
@@ -145,7 +143,7 @@ class FrocaImpl implements Froca {
}
for (const attributeRow of attributeRows) {
const {attributeId} = attributeRow;
const { attributeId } = attributeRow;
this.attributes[attributeId] = new FAttribute(this, attributeRow);
@@ -155,7 +153,7 @@ class FrocaImpl implements Froca {
note.attributes.push(attributeId);
}
if (attributeRow.type === 'relation') {
if (attributeRow.type === "relation") {
const targetNote = this.notes[attributeRow.value];
if (targetNote) {
@@ -180,21 +178,21 @@ class FrocaImpl implements Froca {
noteIds = Array.from(new Set(noteIds)); // make noteIds unique
const resp = await server.post<SubtreeResponse>('tree/load', { noteIds });
const resp = await server.post<SubtreeResponse>("tree/load", { noteIds });
this.addResp(resp);
appContext.triggerEvent('notesReloaded', {noteIds});
appContext.triggerEvent("notesReloaded", { noteIds });
}
async loadSearchNote(noteId: string) {
const note = await this.getNote(noteId);
if (!note || note.type !== 'search') {
if (!note || note.type !== "search") {
return;
}
const {searchResultNoteIds, highlightedTokens, error} = await server.get<SearchNoteResponse>(`search-note/${note.noteId}`);
const { searchResultNoteIds, highlightedTokens, error } = await server.get<SearchNoteResponse>(`search-note/${note.noteId}`);
if (!Array.isArray(searchResultNoteIds)) {
throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`);
@@ -208,14 +206,16 @@ class FrocaImpl implements Froca {
const branches: FBranchRow[] = [...note.getParentBranches(), ...note.getChildBranches()];
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
branchId: `virt-${note.noteId}-${resultNoteId}`,
noteId: resultNoteId,
parentNoteId: note.noteId,
notePosition: (index + 1) * 10,
fromSearchNote: true
}));
searchResultNoteIds.forEach((resultNoteId, index) =>
branches.push({
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
branchId: `virt-${note.noteId}-${resultNoteId}`,
noteId: resultNoteId,
parentNoteId: note.noteId,
notePosition: (index + 1) * 10,
fromSearchNote: true
})
);
// update this note with standard (parent) branches + virtual (children) branches
this.addResp({
@@ -227,37 +227,40 @@ class FrocaImpl implements Froca {
froca.notes[note.noteId].searchResultsLoaded = true;
froca.notes[note.noteId].highlightedTokens = highlightedTokens;
return {error};
return { error };
}
getNotesFromCache(noteIds: string[], silentNotFoundError = false): FNote[] {
return noteIds.map(noteId => {
if (!this.notes[noteId] && !silentNotFoundError) {
console.trace(`Can't find note '${noteId}'`);
return noteIds
.map((noteId) => {
if (!this.notes[noteId] && !silentNotFoundError) {
console.trace(`Can't find note '${noteId}'`);
return null;
}
else {
return this.notes[noteId];
}
}).filter(note => !!note) as FNote[];
return null;
} else {
return this.notes[noteId];
}
})
.filter((note) => !!note) as FNote[];
}
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
noteIds = Array.from(new Set(noteIds)); // make unique
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);
await this.reloadNotes(missingNoteIds);
return noteIds.map(noteId => {
if (!this.notes[noteId] && !silentNotFoundError) {
console.trace(`Can't find note '${noteId}'`);
return noteIds
.map((noteId) => {
if (!this.notes[noteId] && !silentNotFoundError) {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
}).filter(note => !!note) as FNote[];
return null;
} else {
return this.notes[noteId];
}
})
.filter((note) => !!note) as FNote[];
}
async noteExists(noteId: string): Promise<boolean> {
@@ -267,11 +270,10 @@ class FrocaImpl implements Froca {
}
async getNote(noteId: string, silentNotFoundError = false): Promise<FNote | null> {
if (noteId === 'none') {
if (noteId === "none") {
console.trace(`No 'none' note.`);
return null;
}
else if (!noteId) {
} else if (!noteId) {
console.trace(`Falsy noteId '${noteId}', returning null.`);
return null;
}
@@ -288,9 +290,7 @@ class FrocaImpl implements Froca {
}
getBranches(branchIds: string[], silentNotFoundError = false): FBranch[] {
return branchIds
.map(branchId => this.getBranch(branchId, silentNotFoundError))
.filter(b => !!b) as FBranch[];
return branchIds.map((branchId) => this.getBranch(branchId, silentNotFoundError)).filter((b) => !!b) as FBranch[];
}
getBranch(branchId: string, silentNotFoundError = false) {
@@ -298,15 +298,14 @@ class FrocaImpl implements Froca {
if (!silentNotFoundError) {
logError(`Not existing branch '${branchId}'`);
}
}
else {
} else {
return this.branches[branchId];
}
}
async getBranchId(parentNoteId: string, childNoteId: string) {
if (childNoteId === 'root') {
return 'none_root';
if (childNoteId === "root") {
return "none_root";
}
const child = await this.getNote(childNoteId);
@@ -354,7 +353,7 @@ class FrocaImpl implements Froca {
}
processAttachmentRows(attachmentRows: FAttachmentRow[]): FAttachment[] {
return attachmentRows.map(attachmentRow => {
return attachmentRows.map((attachmentRow) => {
let attachment;
if (attachmentRow.attachmentId in this.attachments) {
@@ -376,16 +375,15 @@ class FrocaImpl implements Froca {
const key = `${entityType}-${entityId}`;
if (!this.blobPromises[key]) {
this.blobPromises[key] = server.get<FBlobRow>(`${entityType}/${entityId}/blob`)
.then(row => new FBlob(row))
.catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e));
this.blobPromises[key] = server
.get<FBlobRow>(`${entityType}/${entityId}/blob`)
.then((row) => new FBlob(row))
.catch((e) => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e));
// we don't want to keep large payloads forever in memory, so we clean that up quite quickly
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
// if the blob is updated within the cache lifetime, it should be invalidated by froca_updater
this.blobPromises[key]?.then(
() => setTimeout(() => this.blobPromises[key] = null, 1000)
);
this.blobPromises[key]?.then(() => setTimeout(() => (this.blobPromises[key] = null), 1000));
}
return await this.blobPromises[key];

View File

@@ -3,46 +3,44 @@ import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import noteAttributeCache from "./note_attribute_cache.js";
import FBranch, { FBranchRow } from "../entities/fbranch.js";
import FAttribute, { FAttributeRow } from "../entities/fattribute.js";
import FAttachment, { FAttachmentRow } from "../entities/fattachment.js";
import FNote, { FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js"
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FNote, { type FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js";
async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges);
for (const ec of entityChanges) {
try {
if (ec.entityName === 'notes') {
if (ec.entityName === "notes") {
processNoteChange(loadResults, ec);
} else if (ec.entityName === 'branches') {
} else if (ec.entityName === "branches") {
await processBranchChange(loadResults, ec);
} else if (ec.entityName === 'attributes') {
} else if (ec.entityName === "attributes") {
processAttributeChange(loadResults, ec);
} else if (ec.entityName === 'note_reordering') {
} else if (ec.entityName === "note_reordering") {
processNoteReordering(loadResults, ec);
} else if (ec.entityName === 'revisions') {
} else if (ec.entityName === "revisions") {
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
} else if (ec.entityName === 'options') {
} else if (ec.entityName === "options") {
const attributeEntity = ec.entity as FAttributeRow;
if (attributeEntity.name === 'openNoteContexts') {
if (attributeEntity.name === "openNoteContexts") {
continue; // only noise
}
options.set(attributeEntity.name, attributeEntity.value);
loadResults.addOption(attributeEntity.name);
} else if (ec.entityName === 'attachments') {
} else if (ec.entityName === "attachments") {
processAttachment(loadResults, ec);
} else if (ec.entityName === 'blobs' || ec.entityName === 'etapi_tokens') {
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
// NOOP
}
else {
} else {
throw new Error(`Unknown entityName '${ec.entityName}'`);
}
}
catch (e: any) {
} catch (e: any) {
throw new Error(`Can't process entity ${JSON.stringify(ec)} with error ${e.message} ${e.stack}`);
}
}
@@ -54,19 +52,17 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
// mean we need to load the target of the relation (and then perhaps transitively the whole note path of this target).
const missingNoteIds = [];
for (const {entityName, entity} of entityChanges) {
if (!entity) { // if erased
for (const { entityName, entity } of entityChanges) {
if (!entity) {
// if erased
continue;
}
if (entityName === 'branches' && !((entity as FBranchRow).parentNoteId in froca.notes)) {
if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) {
missingNoteIds.push((entity as FBranchRow).parentNoteId);
}
else if (entityName === 'attributes') {
} else if (entityName === "attributes") {
let attributeEntity = entity as FAttributeRow;
if (attributeEntity.type === 'relation'
&& (attributeEntity.name === 'template' || attributeEntity.name === 'inherit')
&& !(attributeEntity.value in froca.notes)) {
if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) {
missingNoteIds.push(attributeEntity.value);
}
}
@@ -84,7 +80,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
// TODO: Remove after porting the file
// @ts-ignore
const appContext = (await import("../components/app_context.js")).default as any;
await appContext.triggerEvent('entitiesReloaded', {loadResults});
await appContext.triggerEvent("entitiesReloaded", { loadResults });
}
}
@@ -106,8 +102,7 @@ function processNoteChange(loadResults: LoadResults, ec: EntityChange) {
if (ec.isErased || ec.entity?.isDeleted) {
delete froca.notes[ec.entityId];
}
else {
} else {
if (note.blobId !== (ec.entity as FNoteRow).blobId) {
for (const key of Object.keys(froca.blobPromises)) {
if (key.includes(note.noteId)) {
@@ -138,12 +133,12 @@ async function processBranchChange(loadResults: LoadResults, ec: EntityChange) {
const parentNote = froca.notes[branch.parentNoteId];
if (childNote) {
childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== branch.parentNoteId);
childNote.parents = childNote.parents.filter((parentNoteId) => parentNoteId !== branch.parentNoteId);
delete childNote.parentToBranch[branch.parentNoteId];
}
if (parentNote) {
parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== branch.noteId);
parentNote.children = parentNote.children.filter((childNoteId) => childNoteId !== branch.noteId);
delete parentNote.childToBranch[branch.noteId];
}
@@ -175,8 +170,7 @@ async function processBranchChange(loadResults: LoadResults, ec: EntityChange) {
if (branch) {
branch.update(ec.entity as FBranch);
}
else if (childNote || parentNote) {
} else if (childNote || parentNote) {
froca.branches[ec.entityId] = branch = new FBranch(froca, branchEntity);
}
@@ -226,14 +220,14 @@ function processAttributeChange(loadResults: LoadResults, ec: EntityChange) {
if (ec.isErased || ec.entity?.isDeleted) {
if (attribute) {
const sourceNote = froca.notes[attribute.noteId];
const targetNote = attribute.type === 'relation' && froca.notes[attribute.value];
const targetNote = attribute.type === "relation" && froca.notes[attribute.value];
if (sourceNote) {
sourceNote.attributes = sourceNote.attributes.filter(attributeId => attributeId !== attribute.attributeId);
sourceNote.attributes = sourceNote.attributes.filter((attributeId) => attributeId !== attribute.attributeId);
}
if (targetNote) {
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId);
targetNote.targetRelations = targetNote.targetRelations.filter((attributeId) => attributeId !== attribute.attributeId);
}
if (ec.componentId) {
@@ -252,7 +246,7 @@ function processAttributeChange(loadResults: LoadResults, ec: EntityChange) {
const attributeEntity = ec.entity as FAttributeRow;
const sourceNote = froca.notes[attributeEntity.noteId];
const targetNote = attributeEntity.type === 'relation' && froca.notes[attributeEntity.value];
const targetNote = attributeEntity.type === "relation" && froca.notes[attributeEntity.value];
if (attribute) {
attribute.update(ec.entity as FAttributeRow);
@@ -285,7 +279,7 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
const note = attachment.getNote();
if (note && note.attachments) {
note.attachments = note.attachments.filter(att => att.attachmentId !== attachment.attachmentId);
note.attachments = note.attachments.filter((att) => att.attachmentId !== attachment.attachmentId);
}
loadResults.addAttachmentRow(attachmentEntity);
@@ -312,4 +306,4 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
export default {
processEntityChanges
}
};

View File

@@ -1,13 +1,13 @@
import server from './server.js';
import utils from './utils.js';
import toastService from './toast.js';
import linkService from './link.js';
import froca from './froca.js';
import noteTooltipService from './note_tooltip.js';
import protectedSessionService from './protected_session.js';
import dateNotesService from './date_notes.js';
import searchService from './search.js';
import RightPanelWidget from '../widgets/right_panel_widget.js';
import server from "./server.js";
import utils from "./utils.js";
import toastService from "./toast.js";
import linkService from "./link.js";
import froca from "./froca.js";
import noteTooltipService from "./note_tooltip.js";
import protectedSessionService from "./protected_session.js";
import dateNotesService from "./date_notes.js";
import searchService from "./search.js";
import RightPanelWidget from "../widgets/right_panel_widget.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
@@ -15,12 +15,11 @@ import BasicWidget from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js";
import shortcutService from "./shortcuts.js";
import dialogService from "./dialog.js";
import FNote from '../entities/fnote.js';
import { t } from './i18n.js';
import NoteContext from '../components/note_context.js';
import NoteDetailWidget from '../widgets/note_detail.js';
import Component from '../components/component.js';
import FNote from "../entities/fnote.js";
import { t } from "./i18n.js";
import NoteContext from "../components/note_context.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import Component from "../components/component.js";
/**
* A whole number
@@ -39,7 +38,7 @@ interface AddToToolbarOpts {
action: () => void;
/** id of the button, used to identify the old instances of this button to be replaced
* ID is optional because of BC, but not specifying it is deprecated. ID can be alphanumeric only. */
id: string;
id: string;
/** name of the boxicon to be used (e.g. "time" for "bx-time" icon) */
icon: string;
/** keyboard shortcut for the button, e.g. "alt+t" */
@@ -105,7 +104,7 @@ interface Api {
*/
activateNewNote(notePath: string): Promise<void>;
/**
/**
* Open a note in a new tab.
*
* @method
@@ -131,7 +130,7 @@ interface Api {
*/
addButtonToToolbar(opts: AddToToolbarOpts): void;
/**
/**
* @private
*/
__runOnBackendInner(func: unknown, params: unknown[], transactional: boolean): unknown;
@@ -248,7 +247,7 @@ interface Api {
*/
triggerEvent: typeof appContext.triggerEvent;
/**
/**
* Create a note link (jQuery object) for given note.
*
* @param {string} notePath (or noteId)
@@ -388,7 +387,7 @@ interface Api {
*/
setHoistedNoteId(noteId: string): void;
/**
/**
* @param keyboardShortcut - e.g. "ctrl+shift+a"
* @param [namespace] specify namespace of the handler for the cases where call for bind may be repeated.
* If a handler with this ID exists, it's replaced by the new handler.
@@ -447,57 +446,55 @@ interface Api {
* available in the JS frontend notes. You can use e.g. <code>api.showMessage(api.startNote.title);</code></p>
*/
function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, originEntity: Entity | null = null, $container: JQuery<HTMLElement> | null = null) {
this.$container = $container;
this.startNote = startNote;
this.currentNote = currentNote;
this.currentNote = currentNote;
this.originEntity = originEntity;
this.dayjs = dayjs;
this.RightPanelWidget = RightPanelWidget;
this.NoteContextAwareWidget = NoteContextAwareWidget;
this.BasicWidget = BasicWidget;
this.activateNote = async notePath => {
this.activateNote = async (notePath) => {
await appContext.tabManager.getActiveContext().setNote(notePath);
};
this.activateNewNote = async notePath => {
this.activateNewNote = async (notePath) => {
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath);
await appContext.triggerEvent('focusAndSelectTitle');
await appContext.triggerEvent("focusAndSelectTitle");
};
this.openTabWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate });
if (activate) {
await appContext.triggerEvent('focusAndSelectTitle');
await appContext.triggerEvent("focusAndSelectTitle");
}
};
this.openSplitWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId();
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1];
const { ntxId } = subContexts[subContexts.length - 1];
await appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath });
if (activate) {
await appContext.triggerEvent('focusAndSelectTitle');
await appContext.triggerEvent("focusAndSelectTitle");
}
};
this.addButtonToToolbar = async opts => {
this.addButtonToToolbar = async (opts) => {
console.warn("api.addButtonToToolbar() has been deprecated since v0.58 and may be removed in the future. Use Menu -> Configure Launchbar to create/update launchers instead.");
const {action, ...reqBody} = opts;
await server.put('special-notes/api-script-launcher', {
const { action, ...reqBody } = opts;
await server.put("special-notes/api-script-launcher", {
action: action.toString(),
...reqBody
});
@@ -508,31 +505,33 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
return params;
}
return params.map(p => {
return params.map((p) => {
if (typeof p === "function") {
return `!@#Function: ${p.toString()}`;
}
else {
} else {
return p;
}
});
}
this.__runOnBackendInner = async (func, params, transactional) => {
if (typeof func === "function") {
func = func.toString();
}
const ret = await server.post<ExecResult>('script/exec', {
script: func,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId,
originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event
originEntityId: originEntity ? originEntity.noteId : null,
transactional
}, "script");
const ret = await server.post<ExecResult>(
"script/exec",
{
script: func,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId,
originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event
originEntityId: originEntity ? originEntity.noteId : null,
transactional
},
"script"
);
if (ret.success) {
await ws.waitForMaxKnownEntityChangeId();
@@ -541,7 +540,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
} else {
throw new Error(`server error: ${ret.error}`);
}
}
};
this.runOnBackend = async (func, params = []) => {
if (func?.constructor.name === "AsyncFunction" || (typeof func === "string" && func?.startsWith?.("async "))) {
@@ -551,7 +550,6 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
return await this.__runOnBackendInner(func, params, true);
};
this.runAsyncOnBackendWithManualTransactionHandling = async (func, params = []) => {
if (func?.constructor.name === "Function" || (typeof func === "string" && func?.startsWith?.("function"))) {
toastService.showError(t("frontend_script_api.sync_warning"));
@@ -559,72 +557,67 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
return await this.__runOnBackendInner(func, params, false);
};
this.searchForNotes = async searchString => {
this.searchForNotes = async (searchString) => {
return await searchService.searchForNotes(searchString);
};
this.searchForNote = async searchString => {
this.searchForNote = async (searchString) => {
const notes = await this.searchForNotes(searchString);
return notes.length > 0 ? notes[0] : null;
};
this.getNote = async noteId => await froca.getNote(noteId);
this.getNotes = async (noteIds, silentNotFoundError = false) => await froca.getNotes(noteIds, silentNotFoundError);
this.reloadNotes = async noteIds => await froca.reloadNotes(noteIds);
this.getInstanceName = () => window.glob.instanceName;
this.getNote = async (noteId) => await froca.getNote(noteId);
this.getNotes = async (noteIds, silentNotFoundError = false) => await froca.getNotes(noteIds, silentNotFoundError);
this.reloadNotes = async (noteIds) => await froca.reloadNotes(noteIds);
this.getInstanceName = () => window.glob.instanceName;
this.formatDateISO = utils.formatDateISO;
this.parseDate = utils.parseDate;
this.showMessage = toastService.showMessage;
this.showError = toastService.showError;
this.showInfoDialog = dialogService.info;
this.showInfoDialog = dialogService.info;
this.showConfirmDialog = dialogService.confirm;
this.showPromptDialog = dialogService.prompt;
this.triggerCommand = (name, data) => appContext.triggerCommand(name, data);
this.triggerCommand = (name, data) => appContext.triggerCommand(name, data);
this.triggerEvent = (name, data) => appContext.triggerEvent(name, data);
this.createLink = linkService.createLink;
this.createNoteLink = linkService.createLink;
this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text});
this.addTextToActiveContextEditor = (text) => appContext.triggerCommand("addTextToActiveEditor", { text });
this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
this.getActiveContext = () => appContext.tabManager.getActiveContext();
this.getActiveMainContext = () => appContext.tabManager.getActiveMainContext();
this.getNoteContexts = () => appContext.tabManager.getNoteContexts();
this.getNoteContexts = () => appContext.tabManager.getNoteContexts();
this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts();
this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor();
this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor();
this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor();
this.getActiveNoteDetailWidget = () => new Promise(resolve => appContext.triggerCommand('executeInActiveNoteDetailWidget', {callback: resolve}));
this.getActiveNoteDetailWidget = () => new Promise((resolve) => appContext.triggerCommand("executeInActiveNoteDetailWidget", { callback: resolve }));
this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath();
this.getComponentByEl = el => appContext.getComponentByEl(el);
this.getComponentByEl = (el) => appContext.getComponentByEl(el);
this.setupElementTooltip = noteTooltipService.setupElementTooltip;
this.protectNote = async (noteId, protect) => {
await protectedSessionService.protectNote(noteId, protect, false);
};
this.protectSubTree = async (noteId, protect) => {
await protectedSessionService.protectNote(noteId, protect, true);
};
this.getTodayNote = dateNotesService.getTodayNote;
this.getTodayNote = dateNotesService.getTodayNote;
this.getDayNote = dateNotesService.getDayNote;
this.getWeekNote = dateNotesService.getWeekNote;
this.getWeekNote = dateNotesService.getWeekNote;
this.getMonthNote = dateNotesService.getMonthNote;
this.getYearNote = dateNotesService.getYearNote;
@@ -637,32 +630,33 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
};
this.bindGlobalShortcut = shortcutService.bindGlobalShortcut;
this.waitUntilSynced = ws.waitForMaxKnownEntityChangeId;
this.refreshIncludedNote = includedNoteId => appContext.triggerEvent('refreshIncludedNote', {noteId: includedNoteId});
this.waitUntilSynced = ws.waitForMaxKnownEntityChangeId;
this.refreshIncludedNote = (includedNoteId) => appContext.triggerEvent("refreshIncludedNote", { noteId: includedNoteId });
this.randomString = utils.randomString;
this.formatSize = utils.formatSize;
this.formatNoteSize = utils.formatSize;
this.logMessages = {};
this.logSpacedUpdates = {};
this.log = message => {
const {noteId} = this.startNote;
this.logSpacedUpdates = {};
this.log = (message) => {
const { noteId } = this.startNote;
message = `${utils.now()}: ${message}`;
console.log(`Script ${noteId}: ${message}`);
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
this.logSpacedUpdates[noteId] =
this.logSpacedUpdates[noteId] ||
new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
appContext.triggerEvent("apiLogMessages", {noteId, messages});
}, 100);
appContext.triggerEvent("apiLogMessages", { noteId, messages });
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
@@ -670,5 +664,5 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
}
export default FrontendScriptApi as any as {
new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery<HTMLElement> | null): Api
new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery<HTMLElement> | null): Api;
};

View File

@@ -10,10 +10,10 @@ function setupGlobs() {
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
window.glob.getComponentByEl = el => appContext.getComponentByEl(el);
window.glob.getComponentByEl = (el) => appContext.getComponentByEl(el);
window.glob.getHeaders = server.getHeaders;
window.glob.getReferenceLinkTitle = href => linkService.getReferenceLinkTitle(href);
window.glob.getReferenceLinkTitleSync = href => linkService.getReferenceLinkTitleSync(href);
window.glob.getReferenceLinkTitle = (href) => linkService.getReferenceLinkTitle(href);
window.glob.getReferenceLinkTitleSync = (href) => linkService.getReferenceLinkTitleSync(href);
// required for ESLint plugin and CKEditor
window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
@@ -32,16 +32,9 @@ function setupGlobs() {
let message = "Uncaught error: ";
if (string.includes("script error")) {
message += 'No details available';
message += "No details available";
} else {
message += [
`Message: ${msg}`,
`URL: ${url}`,
`Line: ${lineNo}`,
`Column: ${columnNo}`,
`Error object: ${JSON.stringify(error)}`,
`Stack: ${error && error.stack}`
].join(', ');
message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${JSON.stringify(error)}`, `Stack: ${error && error.stack}`].join(", ");
}
ws.logError(message);
@@ -55,7 +48,7 @@ function setupGlobs() {
let message = "Uncaught error: ";
if (string?.includes("script error")) {
message += 'No details available';
message += "No details available";
} else {
message += [
`Message: ${e.reason.message}`,
@@ -63,7 +56,7 @@ function setupGlobs() {
`Column: ${e.reason.columnNumber}`,
`Error object: ${JSON.stringify(e.reason)}`,
`Stack: ${e.reason && e.reason.stack}`
].join(', ');
].join(", ");
}
ws.logError(message);
@@ -78,7 +71,7 @@ function setupGlobs() {
utils.initHelpButtons($(window));
$("body").on("click", "a.external", function () {
window.open($(this).attr("href"), '_blank');
window.open($(this).attr("href"), "_blank");
return false;
});
@@ -86,4 +79,4 @@ function setupGlobs() {
export default {
setupGlobs
}
};

View File

@@ -1,5 +1,5 @@
import appContext from "../components/app_context.js";
import treeService, { Node } from "./tree.js";
import treeService, { type Node } from "./tree.js";
import dialogService from "./dialog.js";
import froca from "./froca.js";
import NoteContext from "../components/note_context.js";
@@ -8,7 +8,7 @@ import { t } from "./i18n.js";
function getHoistedNoteId() {
const activeNoteContext = appContext.tabManager.getActiveContext();
return activeNoteContext ? activeNoteContext.hoistedNoteId : 'root';
return activeNoteContext ? activeNoteContext.hoistedNoteId : "root";
}
async function unhoist() {
@@ -25,14 +25,13 @@ function isTopLevelNode(node: Node) {
function isHoistedNode(node: Node) {
// even though check for 'root' should not be necessary, we keep it just in case
return node.data.noteId === "root"
|| node.data.noteId === getHoistedNoteId();
return node.data.noteId === "root" || node.data.noteId === getHoistedNoteId();
}
async function isHoistedInHiddenSubtree() {
const hoistedNoteId = getHoistedNoteId();
if (hoistedNoteId === 'root') {
if (hoistedNoteId === "root") {
return false;
}
@@ -50,7 +49,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
const hoistedNoteId = noteContext.hoistedNoteId;
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes('_hidden') || resolvedNotePath.includes('_lbBookmarks'))) {
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
const noteId = treeService.getNoteIdFromUrl(resolvedNotePath);
if (!noteId) {
return false;
@@ -58,8 +57,10 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
const requestedNote = await froca.getNote(noteId);
const hoistedNote = await froca.getNote(hoistedNoteId);
if ((!hoistedNote?.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
&& !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote?.title, hoistedNote: hoistedNote?.title }))) {
if (
(!hoistedNote?.hasAncestor("_hidden") || resolvedNotePath.includes("_lbBookmarks")) &&
!(await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote?.title, hoistedNote: hoistedNote?.title })))
) {
return false;
}
@@ -77,4 +78,4 @@ export default {
isHoistedNode,
checkNoteAccess,
isHoistedInHiddenSubtree
}
};

View File

@@ -6,16 +6,14 @@ await library_loader.requireLibrary(library_loader.I18NEXT);
export async function initLocale() {
const locale = (options.get("locale") as string) || "en";
await i18next
.use(i18nextHttpBackend)
.init({
lng: locale,
fallbackLng: "en",
backend: {
loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
},
returnEmptyString: false
});
await i18next.use(i18nextHttpBackend).init({
lng: locale,
fallbackLng: "en",
backend: {
loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
},
returnEmptyString: false
});
}
export const t = i18next.t;

View File

@@ -3,20 +3,19 @@ import toastService from "./toast.js";
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
try {
$imageWrapper.attr('contenteditable', 'true');
$imageWrapper.attr("contenteditable", "true");
selectImage($imageWrapper.get(0));
const success = document.execCommand('copy');
const success = document.execCommand("copy");
if (success) {
toastService.showMessage(t("image.copied-to-clipboard"));
} else {
toastService.showAndLogError(t("image.cannot-copy"));
}
}
finally {
} finally {
window.getSelection()?.removeAllRanges();
$imageWrapper.removeAttr('contenteditable');
$imageWrapper.removeAttr("contenteditable");
}
}
@@ -24,7 +23,7 @@ function selectImage(element: HTMLElement | undefined) {
if (!element) {
return;
}
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(element);

View File

@@ -1,4 +1,4 @@
import toastService, { ToastOptions } from "./toast.js";
import toastService, { type ToastOptions } from "./toast.js";
import server from "./server.js";
import ws from "./ws.js";
import utils from "./utils.js";
@@ -6,7 +6,7 @@ import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[], options: Record<string, string | Blob>) {
if (!['notes', 'attachments'].includes(entityType)) {
if (!["notes", "attachments"].includes(entityType)) {
throw new Error(`Unrecognized import entity type '${entityType}'.`);
}
@@ -21,9 +21,9 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
counter++;
const formData = new FormData();
formData.append('upload', file);
formData.append('taskId', taskId);
formData.append('last', counter === files.length ? "true" : "false");
formData.append("upload", file);
formData.append("taskId", taskId);
formData.append("last", counter === files.length ? "true" : "false");
for (const key in options) {
formData.append(key, options[key]);
@@ -33,14 +33,14 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
url: `${window.glob.baseApiUrl}notes/${parentNoteId}/${entityType}-import`,
headers: await server.getHeaders(),
data: formData,
dataType: 'json',
type: 'POST',
dataType: "json",
type: "POST",
timeout: 60 * 60 * 1000,
error: function (xhr) {
toastService.showError(t("import.failed", { message: xhr.responseText }));
},
contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false, // NEEDED, DON'T REMOVE THIS
processData: false // NEEDED, DON'T REMOVE THIS
});
}
}
@@ -54,17 +54,17 @@ function makeToast(id: string, message: string): ToastOptions {
};
}
ws.subscribeToMessages(async message => {
if (message.taskType !== 'importNotes') {
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "importNotes") {
return;
}
if (message.type === 'taskError') {
if (message.type === "taskError") {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
} else if (message.type === "taskProgressCount") {
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("import.successful"));
toast.closeAfter = 5000;
@@ -76,17 +76,17 @@ ws.subscribeToMessages(async message => {
}
});
ws.subscribeToMessages(async message => {
if (message.taskType !== 'importAttachments') {
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "importAttachments") {
return;
}
if (message.type === 'taskError') {
if (message.type === "taskError") {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
} else if (message.type === "taskProgressCount") {
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
} else if (message.type === "taskSucceeded") {
const toast = makeToast(message.taskId, t("import.successful"));
toast.closeAfter = 5000;
@@ -95,7 +95,7 @@ ws.subscribeToMessages(async message => {
if (message.result.parentNoteId) {
await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId, {
viewScope: {
viewMode: 'attachments'
viewMode: "attachments"
}
});
}

View File

@@ -1,5 +1,5 @@
import server from "./server.js";
import appContext, { CommandNames } from "../components/app_context.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import shortcutService from "./shortcuts.js";
import Component from "../components/component.js";
@@ -7,115 +7,114 @@ const keyboardActionRepo: Record<string, Action> = {};
// TODO: Deduplicate with server.
export interface Action {
actionName: CommandNames;
effectiveShortcuts: string[];
scope: string;
actionName: CommandNames;
effectiveShortcuts: string[];
scope: string;
}
const keyboardActionsLoaded = server.get<Action[]>('keyboard-actions').then(actions => {
actions = actions.filter(a => !!a.actionName); // filter out separators
const keyboardActionsLoaded = server.get<Action[]>("keyboard-actions").then((actions) => {
actions = actions.filter((a) => !!a.actionName); // filter out separators
for (const action of actions) {
action.effectiveShortcuts = action.effectiveShortcuts.filter(shortcut => !shortcut.startsWith("global:"));
for (const action of actions) {
action.effectiveShortcuts = action.effectiveShortcuts.filter((shortcut) => !shortcut.startsWith("global:"));
keyboardActionRepo[action.actionName] = action;
}
keyboardActionRepo[action.actionName] = action;
}
return actions;
return actions;
});
async function getActions() {
return await keyboardActionsLoaded;
return await keyboardActionsLoaded;
}
async function getActionsForScope(scope: string) {
const actions = await keyboardActionsLoaded;
const actions = await keyboardActionsLoaded;
return actions.filter(action => action.scope === scope);
return actions.filter((action) => action.scope === scope);
}
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
const actions = await getActionsForScope(scope);
const actions = await getActionsForScope(scope);
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, {ntxId: appContext.tabManager.activeNtxId}));
}
}
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
}
}
}
getActionsForScope("window").then(actions => {
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, {ntxId: appContext.tabManager.activeNtxId}));
}
}
getActionsForScope("window").then((actions) => {
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
}
}
});
async function getAction(actionName: string, silent = false) {
await keyboardActionsLoaded;
await keyboardActionsLoaded;
const action = keyboardActionRepo[actionName];
const action = keyboardActionRepo[actionName];
if (!action) {
if (silent) {
console.debug(`Cannot find action '${actionName}'`);
}
else {
throw new Error(`Cannot find action '${actionName}'`);
}
}
if (!action) {
if (silent) {
console.debug(`Cannot find action '${actionName}'`);
} else {
throw new Error(`Cannot find action '${actionName}'`);
}
}
return action;
return action;
}
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
//@ts-ignore
//TODO: each() does not support async callbacks.
$container.find('kbd[data-command]').each(async (i, el) => {
const actionName = $(el).attr('data-command');
if (!actionName) {
return;
}
//@ts-ignore
//TODO: each() does not support async callbacks.
$container.find("kbd[data-command]").each(async (i, el) => {
const actionName = $(el).attr("data-command");
if (!actionName) {
return;
}
const action = await getAction(actionName, true);
const action = await getAction(actionName, true);
if (action) {
const keyboardActions = action.effectiveShortcuts.join(', ');
if (action) {
const keyboardActions = action.effectiveShortcuts.join(", ");
if (keyboardActions || $(el).text() !== "not set") {
$(el).text(keyboardActions);
}
}
});
if (keyboardActions || $(el).text() !== "not set") {
$(el).text(keyboardActions);
}
}
});
//@ts-ignore
//TODO: each() does not support async callbacks.
$container.find('[data-trigger-command]').each(async (i, el) => {
const actionName = $(el).attr('data-trigger-command');
if (!actionName) {
return;
}
const action = await getAction(actionName, true);
//@ts-ignore
//TODO: each() does not support async callbacks.
$container.find("[data-trigger-command]").each(async (i, el) => {
const actionName = $(el).attr("data-trigger-command");
if (!actionName) {
return;
}
const action = await getAction(actionName, true);
if (action) {
const title = $(el).attr('title');
const shortcuts = action.effectiveShortcuts.join(', ');
if (action) {
const title = $(el).attr("title");
const shortcuts = action.effectiveShortcuts.join(", ");
if (title?.includes(shortcuts)) {
return;
}
if (title?.includes(shortcuts)) {
return;
}
const newTitle = !title?.trim() ? shortcuts : `${title} (${shortcuts})`;
const newTitle = !title?.trim() ? shortcuts : `${title} (${shortcuts})`;
$(el).attr('title', newTitle);
}
});
$(el).attr("title", newTitle);
}
});
}
export default {
updateDisplayedShortcuts,
setupActionsForElement,
getActions,
getActionsForScope
updateDisplayedShortcuts,
setupActionsForElement,
getActions,
getActionsForScope
};

View File

@@ -26,7 +26,8 @@ const CODE_MIRROR: Library = {
"node_modules/codemirror/addon/mode/simple.js",
"node_modules/codemirror/addon/search/match-highlighter.js",
"node_modules/codemirror/mode/meta.js",
"node_modules/codemirror/keymap/vim.js"
"node_modules/codemirror/keymap/vim.js",
"libraries/codemirror/eslint.js"
];
const mimeTypes = mimeTypesService.getMimeTypes();
@@ -38,26 +39,16 @@ const CODE_MIRROR: Library = {
return scriptsToLoad;
},
css: [
"node_modules/codemirror/lib/codemirror.css",
"node_modules/codemirror/addon/lint/lint.css"
]
css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"]
};
const ESLINT: Library = {
js: [
"node_modules/eslint/bin/eslint.js"
]
js: ["libraries/eslint/eslint.js"]
};
const RELATION_MAP: Library = {
js: [
"node_modules/jsplumb/dist/js/jsplumb.min.js",
"node_modules/panzoom/dist/panzoom.min.js"
],
css: [
"stylesheets/relation_map.css"
]
js: ["node_modules/jsplumb/dist/js/jsplumb.min.js", "node_modules/panzoom/dist/panzoom.min.js"],
css: ["stylesheets/relation_map.css"]
};
const PRINT_THIS: Library = {
@@ -69,62 +60,44 @@ const CALENDAR_WIDGET: Library = {
};
const KATEX: Library = {
js: [ "node_modules/katex/dist/katex.min.js",
"node_modules/katex/dist/contrib/mhchem.min.js",
"node_modules/katex/dist/contrib/auto-render.min.js" ],
css: [ "node_modules/katex/dist/katex.min.css" ]
js: ["node_modules/katex/dist/katex.min.js", "node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/auto-render.min.js"],
css: ["node_modules/katex/dist/katex.min.css"]
};
const WHEEL_ZOOM: Library = {
js: [ "node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
};
const FORCE_GRAPH: Library = {
js: [ "node_modules/force-graph/dist/force-graph.min.js"]
js: ["node_modules/force-graph/dist/force-graph.min.js"]
};
const MERMAID: Library = {
js: [
"node_modules/mermaid/dist/mermaid.min.js"
]
}
js: ["node_modules/mermaid/dist/mermaid.min.js"]
};
/**
* The ELK extension of Mermaid.js, which supports more advanced layouts.
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
*/
const MERMAID_ELK: Library = {
js: [
"libraries/mermaid-elk/elk.min.js"
]
}
js: ["libraries/mermaid-elk/elk.min.js"]
};
const EXCALIDRAW: Library = {
js: [
"node_modules/react/umd/react.production.min.js",
"node_modules/react-dom/umd/react-dom.production.min.js",
"node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js",
]
js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"]
};
const MARKJS: Library = {
js: [
"node_modules/mark.js/dist/jquery.mark.es6.min.js"
]
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
};
const I18NEXT: Library = {
js: [
"node_modules/i18next/i18next.min.js",
"node_modules/i18next-http-backend/i18nextHttpBackend.min.js"
]
js: ["node_modules/i18next/i18next.min.js", "node_modules/i18next-http-backend/i18nextHttpBackend.min.js"]
};
const MIND_ELIXIR: Library = {
js: [
"node_modules/mind-elixir/dist/MindElixir.iife.js",
"node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"
]
js: ["node_modules/mind-elixir/dist/MindElixir.iife.js", "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"]
};
const HIGHLIGHT_JS: Library = {
@@ -155,7 +128,7 @@ const HIGHLIGHT_JS: Library = {
async function requireLibrary(library: Library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
library.css.map((cssUrl) => requireCss(cssUrl));
}
if (library.js) {
@@ -191,16 +164,14 @@ async function requireScript(url: string) {
}
async function requireCss(url: string, prependAssetPath = true) {
const cssLinks = Array
.from(document.querySelectorAll('link'))
.map(el => el.href);
const cssLinks = Array.from(document.querySelectorAll("link")).map((el) => el.href);
if (!cssLinks.some(l => l.endsWith(url))) {
if (!cssLinks.some((l) => l.endsWith(url))) {
if (prependAssetPath) {
url = `${window.glob.assetPath}/${url}`;
}
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
$("head").append($('<link rel="stylesheet" type="text/css" />').attr("href", url));
}
}
@@ -250,4 +221,4 @@ export default {
I18NEXT,
MIND_ELIXIR,
HIGHLIGHT_JS
}
};

View File

@@ -1,6 +1,6 @@
import treeService from './tree.js';
import treeService from "./tree.js";
import linkContextMenuService from "../menus/link_context_menu.js";
import appContext, { NoteCommandData } from "../components/app_context.js";
import appContext, { type NoteCommandData } from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
@@ -13,14 +13,14 @@ function getNotePathFromUrl(url: string) {
async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
let icon;
if (!viewMode || viewMode === 'default') {
if (!viewMode || viewMode === "default") {
const note = await froca.getNote(noteId);
icon = note?.getIcon();
} else if (viewMode === 'source') {
icon = 'bx bx-code-curly';
} else if (viewMode === 'attachments') {
icon = 'bx bx-file';
} else if (viewMode === "source") {
icon = "bx bx-code-curly";
} else if (viewMode === "attachments") {
icon = "bx bx-file";
}
return icon;
}
@@ -72,14 +72,14 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) {
}
const viewScope = options.viewScope || {};
const viewMode = viewScope.viewMode || 'default';
const viewMode = viewScope.viewMode || "default";
let linkTitle = options.title;
if (!linkTitle) {
if (viewMode === 'attachments' && viewScope.attachmentId) {
if (viewMode === "attachments" && viewScope.attachmentId) {
const attachment = await froca.getAttachment(viewScope.attachmentId);
linkTitle = attachment ? attachment.title : '[missing attachment]';
linkTitle = attachment ? attachment.title : "[missing attachment]";
} else if (noteId) {
linkTitle = await treeService.getNoteTitle(noteId, parentNoteId);
}
@@ -87,7 +87,7 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) {
const note = await froca.getNote(noteId);
if (autoConvertToImage && (note?.type && ['image', 'canvas', 'mermaid'].includes(note.type)) && viewMode === 'default') {
if (autoConvertToImage && note?.type && ["image", "canvas", "mermaid"].includes(note.type) && viewMode === "default") {
const encodedTitle = encodeURIComponent(linkTitle || "");
return $("<img>")
@@ -101,9 +101,7 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) {
let icon = await getLinkIcon(noteId, viewMode);
if (icon) {
$container
.append($("<span>").addClass(`bx ${icon}`))
.append(" ");
$container.append($("<span>").addClass(`bx ${icon}`)).append(" ");
}
}
@@ -128,7 +126,7 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) {
$container.append($noteLink);
if (showNotePath) {
const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath) || [];
const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || [];
resolvedPathSegments.pop(); // Remove last element
const resolvedPath = resolvedPathSegments.join("/");
@@ -144,21 +142,23 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) {
return $container;
}
function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}: NoteCommandData) {
function calculateHash({ notePath, ntxId, hoistedNoteId, viewScope = {} }: NoteCommandData) {
notePath = notePath || "";
const params = [
ntxId ? { ntxId: ntxId } : null,
(hoistedNoteId && hoistedNoteId !== 'root') ? { hoistedNoteId: hoistedNoteId } : null,
viewScope.viewMode && viewScope.viewMode !== 'default' ? { viewMode: viewScope.viewMode } : null,
hoistedNoteId && hoistedNoteId !== "root" ? { hoistedNoteId: hoistedNoteId } : null,
viewScope.viewMode && viewScope.viewMode !== "default" ? { viewMode: viewScope.viewMode } : null,
viewScope.attachmentId ? { attachmentId: viewScope.attachmentId } : null
].filter(p => !!p);
].filter((p) => !!p);
const paramStr = params.map(pair => {
const name = Object.keys(pair)[0];
const value = (pair as Record<string, string | undefined>)[name];
const paramStr = params
.map((pair) => {
const name = Object.keys(pair)[0];
const value = (pair as Record<string, string | undefined>)[name];
return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`;
}).join("&");
return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`;
})
.join("&");
if (!notePath && !paramStr) {
return "";
@@ -178,7 +178,7 @@ function parseNavigationStateFromUrl(url: string | undefined) {
return {};
}
const hashIdx = url.indexOf('#');
const hashIdx = url.indexOf("#");
if (hashIdx === -1) {
return {};
}
@@ -191,7 +191,7 @@ function parseNavigationStateFromUrl(url: string | undefined) {
}
const viewScope: ViewScope = {
viewMode: 'default'
viewMode: "default"
};
let ntxId = null;
let hoistedNoteId = null;
@@ -203,13 +203,13 @@ function parseNavigationStateFromUrl(url: string | undefined) {
name = decodeURIComponent(name);
value = decodeURIComponent(value);
if (name === 'ntxId') {
if (name === "ntxId") {
ntxId = value;
} else if (name === 'hoistedNoteId') {
} else if (name === "hoistedNoteId") {
hoistedNoteId = value;
} else if (name === 'searchString') {
} else if (name === "searchString") {
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
} else if (['viewMode', 'attachmentId'].includes(name)) {
} else if (["viewMode", "attachmentId"].includes(name)) {
(viewScope as any)[name] = value;
} else {
console.warn(`Unrecognized hash parameter '${name}'.`);
@@ -229,7 +229,7 @@ function parseNavigationStateFromUrl(url: string | undefined) {
function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
const $link = $(evt.target as any).closest("a,.block-link");
const hrefLink = $link.attr('href') || $link.attr('data-href');
const hrefLink = $link.attr("href") || $link.attr("data-href");
return goToLinkExt(evt, hrefLink, $link);
}
@@ -246,7 +246,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
return handleFootnote(hrefLink, $link);
}
const {notePath, viewScope} = parseNavigationStateFromUrl(hrefLink);
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = evt.which === 1;
@@ -258,15 +258,15 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
if (notePath) {
if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope});
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
} else if (isLeftClick) {
const ntxId = $(evt.target as any).closest("[data-ntx-id]").attr("data-ntx-id");
const ntxId = $(evt.target as any)
.closest("[data-ntx-id]")
.attr("data-ntx-id");
const noteContext = ntxId
? appContext.tabManager.getNoteContextById(ntxId)
: appContext.tabManager.getActiveContext();
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
noteContext.setNote(notePath, {viewScope}).then(() => {
noteContext.setNote(notePath, { viewScope }).then(() => {
if (noteContext !== appContext.tabManager.getActiveContext()) {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
}
@@ -276,27 +276,67 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
if (openInNewTab
|| (withinEditLink && (leftClick || middleClick))
|| (outsideOfCKEditor && (leftClick || middleClick))
) {
if (hrefLink.toLowerCase().startsWith('http') || hrefLink.startsWith("api/")) {
window.open(hrefLink, '_blank');
} else if ((hrefLink.toLowerCase().startsWith('file:') || hrefLink.toLowerCase().startsWith('geo:')) && utils.isElectron()) {
const electron = utils.dynamicRequire('electron');
if (openInNewTab || (withinEditLink && (leftClick || middleClick)) || (outsideOfCKEditor && (leftClick || middleClick))) {
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
window.open(hrefLink, "_blank");
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
const electron = utils.dynamicRequire("electron");
electron.shell.openPath(hrefLink);
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
// Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
// And be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
const allowedSchemes = [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo'
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
];
if (allowedSchemes.some(protocol => hrefLink.toLowerCase().startsWith(protocol+':'))){
window.open(hrefLink, '_blank');
if (allowedSchemes.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
window.open(hrefLink, "_blank");
}
}
}
@@ -313,10 +353,9 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
* @returns whether the event should be consumed or not.
*/
function handleFootnote(hrefLink: string, $link: JQuery<HTMLElement>) {
const el = $link.closest(".ck-content")
.find(hrefLink)[0];
const el = $link.closest(".ck-content").find(hrefLink)[0];
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" })
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
return true;
}
@@ -337,7 +376,7 @@ function linkContextMenu(e: PointerEvent) {
}
async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
const $link = $el[0].tagName === 'A' ? $el : $el.find("a");
const $link = $el[0].tagName === "A" ? $el : $el.find("a");
href = href || $link.attr("href");
if (!href) {
@@ -345,7 +384,7 @@ async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | n
return;
}
const {noteId, viewScope} = parseNavigationStateFromUrl(href);
const { noteId, viewScope } = parseNavigationStateFromUrl(href);
if (!noteId) {
console.warn("Missing note ID.");
return;
@@ -370,7 +409,7 @@ async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | n
}
async function getReferenceLinkTitle(href: string) {
const {noteId, viewScope} = parseNavigationStateFromUrl(href);
const { noteId, viewScope } = parseNavigationStateFromUrl(href);
if (!noteId) {
return "[missing note]";
}
@@ -380,7 +419,7 @@ async function getReferenceLinkTitle(href: string) {
return "[missing note]";
}
if (viewScope?.viewMode === 'attachments' && viewScope?.attachmentId) {
if (viewScope?.viewMode === "attachments" && viewScope?.attachmentId) {
const attachment = await note.getAttachmentById(viewScope.attachmentId);
return attachment ? attachment.title : "[missing attachment]";
@@ -390,7 +429,7 @@ async function getReferenceLinkTitle(href: string) {
}
function getReferenceLinkTitleSync(href: string) {
const {noteId, viewScope} = parseNavigationStateFromUrl(href);
const { noteId, viewScope } = parseNavigationStateFromUrl(href);
if (!noteId) {
return "[missing note]";
}
@@ -400,12 +439,12 @@ function getReferenceLinkTitleSync(href: string) {
return "[missing note]";
}
if (viewScope?.viewMode === 'attachments' && viewScope?.attachmentId) {
if (viewScope?.viewMode === "attachments" && viewScope?.attachmentId) {
if (!note.attachments) {
return "[loading title...]";
}
const attachment = note.attachments.find(att => att.attachmentId === viewScope.attachmentId);
const attachment = note.attachments.find((att) => att.attachmentId === viewScope.attachmentId);
return attachment ? attachment.title : "[missing attachment]";
} else {
@@ -415,27 +454,27 @@ function getReferenceLinkTitleSync(href: string) {
// TODO: Check why the event is not supported.
//@ts-ignore
$(document).on('click', "a", goToLink);
$(document).on("click", "a", goToLink);
// TODO: Check why the event is not supported.
//@ts-ignore
$(document).on('auxclick', "a", goToLink); // to handle the middle button
$(document).on("auxclick", "a", goToLink); // to handle the middle button
// TODO: Check why the event is not supported.
//@ts-ignore
$(document).on('contextmenu', 'a', linkContextMenu);
$(document).on('dblclick', "a", e => {
$(document).on("contextmenu", "a", linkContextMenu);
$(document).on("dblclick", "a", (e) => {
e.preventDefault();
e.stopPropagation();
const $link = $(e.target).closest("a");
const address = $link.attr('href');
const address = $link.attr("href");
if (address && address.startsWith('http')) {
window.open(address, '_blank');
if (address && address.startsWith("http")) {
window.open(address, "_blank");
}
});
$(document).on('mousedown', 'a', e => {
$(document).on("mousedown", "a", (e) => {
if (e.which === 2) {
// prevent paste on middle click
// https://github.com/zadam/trilium/issues/2995

View File

@@ -1,5 +1,5 @@
import { AttributeType } from "../entities/fattribute.js";
import { EntityChange } from "../server_types.js";
import type { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js";
// TODO: Deduplicate with server.
@@ -47,13 +47,13 @@ interface ContentNoteIdToComponentIdRow {
}
type EntityRowMappings = {
"notes": NoteRow,
"branches": BranchRow,
"attributes": AttributeRow,
"options": OptionRow,
"revisions": RevisionRow,
"note_reordering": NoteReorderingRow
}
notes: NoteRow;
branches: BranchRow;
attributes: AttributeRow;
options: OptionRow;
revisions: RevisionRow;
note_reordering: NoteReorderingRow;
};
export type EntityRowNames = keyof EntityRowMappings;
@@ -72,7 +72,7 @@ export default class LoadResults {
constructor(entityChanges: EntityChange[]) {
const entities: Record<string, Record<string, any>> = {};
for (const {entityId, entityName, entity} of entityChanges) {
for (const { entityId, entityName, entity } of entityChanges) {
if (entity) {
entities[entityName] = entities[entityName] || [];
entities[entityName][entityId] = entity;
@@ -99,7 +99,7 @@ export default class LoadResults {
}
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) {
@@ -119,13 +119,11 @@ export default class LoadResults {
}
addBranch(branchId: string, componentId: string) {
this.branchRows.push({branchId, componentId});
this.branchRows.push({ branchId, componentId });
}
getBranchRows() {
return this.branchRows
.map(row => this.getEntityRow("branches", row.branchId))
.filter(branch => !!branch);
return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch);
}
addNoteReordering(parentNoteId: string, componentId: string) {
@@ -137,22 +135,22 @@ export default class LoadResults {
}
addAttribute(attributeId: string, componentId: string) {
this.attributeRows.push({attributeId, componentId});
this.attributeRows.push({ attributeId, componentId });
}
getAttributeRows(componentId = 'none'): AttributeRow[] {
getAttributeRows(componentId = "none"): AttributeRow[] {
return this.attributeRows
.filter(row => row.componentId !== componentId)
.map(row => this.getEntityRow("attributes", row.attributeId))
.filter(attr => !!attr) as AttributeRow[];
.filter((row) => row.componentId !== componentId)
.map((row) => this.getEntityRow("attributes", row.attributeId))
.filter((attr) => !!attr) as AttributeRow[];
}
addRevision(revisionId: string, noteId?: string, componentId?: string | null) {
this.revisionRows.push({revisionId, noteId, componentId});
this.revisionRows.push({ revisionId, noteId, componentId });
}
hasRevisionForNote(noteId: string) {
return !!this.revisionRows.find(row => row.noteId === noteId);
return !!this.revisionRows.find((row) => row.noteId === noteId);
}
getNoteIds() {
@@ -165,11 +163,11 @@ export default class LoadResults {
}
const componentIds = this.noteIdToComponentId[noteId];
return componentIds && componentIds.find(sId => sId !== componentId) !== undefined;
return componentIds && componentIds.find((sId) => sId !== componentId) !== undefined;
}
addNoteContent(noteId: string, componentId: string) {
this.contentNoteIdToComponentId.push({noteId, componentId});
this.contentNoteIdToComponentId.push({ noteId, componentId });
}
isNoteContentReloaded(noteId: string, componentId?: string) {
@@ -177,7 +175,7 @@ export default class LoadResults {
return false;
}
return this.contentNoteIdToComponentId.find(l => l.noteId === noteId && l.componentId !== componentId);
return this.contentNoteIdToComponentId.find((l) => l.noteId === noteId && l.componentId !== componentId);
}
addOption(name: string) {
@@ -205,25 +203,23 @@ export default class LoadResults {
* notably changes in note itself should not have any effect on attributes
*/
hasAttributeRelatedChanges() {
return this.branchRows.length > 0
|| this.attributeRows.length > 0;
return this.branchRows.length > 0 || this.attributeRows.length > 0;
}
isEmpty() {
return Object.keys(this.noteIdToComponentId).length === 0
&& this.branchRows.length === 0
&& this.attributeRows.length === 0
&& this.noteReorderings.length === 0
&& this.revisionRows.length === 0
&& this.contentNoteIdToComponentId.length === 0
&& this.optionNames.length === 0
&& this.attachmentRows.length === 0;
return (
Object.keys(this.noteIdToComponentId).length === 0 &&
this.branchRows.length === 0 &&
this.attributeRows.length === 0 &&
this.noteReorderings.length === 0 &&
this.revisionRows.length === 0 &&
this.contentNoteIdToComponentId.length === 0 &&
this.optionNames.length === 0 &&
this.attachmentRows.length === 0
);
}
isEmptyForTree() {
return Object.keys(this.noteIdToComponentId).length === 0
&& this.branchRows.length === 0
&& this.attributeRows.length === 0
&& this.noteReorderings.length === 0;
return Object.keys(this.noteIdToComponentId).length === 0 && this.branchRows.length === 0 && this.attributeRows.length === 0 && this.noteReorderings.length === 0;
}
}

View File

@@ -6,12 +6,12 @@ import shortcutService from "./shortcuts.js";
function init() {
if (utils.isElectron() && utils.isMac()) {
shortcutService.bindGlobalShortcut('meta+c', () => exec("copy"));
shortcutService.bindGlobalShortcut('meta+v', () => exec('paste'));
shortcutService.bindGlobalShortcut('meta+x', () => exec('cut'));
shortcutService.bindGlobalShortcut('meta+a', () => exec('selectAll'));
shortcutService.bindGlobalShortcut('meta+z', () => exec('undo'));
shortcutService.bindGlobalShortcut('meta+y', () => exec('redo'));
shortcutService.bindGlobalShortcut("meta+c", () => exec("copy"));
shortcutService.bindGlobalShortcut("meta+v", () => exec("paste"));
shortcutService.bindGlobalShortcut("meta+x", () => exec("cut"));
shortcutService.bindGlobalShortcut("meta+a", () => exec("selectAll"));
shortcutService.bindGlobalShortcut("meta+z", () => exec("undo"));
shortcutService.bindGlobalShortcut("meta+y", () => exec("redo"));
}
}
@@ -23,4 +23,4 @@ function exec(cmd: string) {
export default {
init
}
};

View File

@@ -5,10 +5,10 @@ let elkLoaded = false;
/**
* Determines whether the ELK extension of Mermaid.js needs to be loaded (which is a relatively large library), based on the
* front-matter of the diagram and loads the library if needed.
*
*
* <p>
* If the library has already been loaded or the diagram does not require it, the method will exit immediately.
*
*
* @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter.
*/
export async function loadElkIfNeeded(mermaidContent: string) {
@@ -18,11 +18,11 @@ export async function loadElkIfNeeded(mermaidContent: string) {
}
const parsedContent = await mermaid.parse(mermaidContent, {
suppressErrors: true
suppressErrors: true
});
if (parsedContent?.config?.layout === "elk") {
elkLoaded = true;
await library_loader.requireLibrary(library_loader.MERMAID_ELK);
mermaid.registerLayoutLoaders(MERMAID_ELK);
}
}
}
}

View File

@@ -0,0 +1,216 @@
/**
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
*/
export const MIME_TYPE_AUTO = "text-x-trilium-auto";
export interface MimeTypeDefinition {
default?: boolean;
title: string;
mime: string;
/** The name of the language/mime type as defined by highlight.js (or one of the aliases), in order to be used for syntax highlighting such as inside code blocks. */
highlightJs?: string;
/** If specified, will load the corresponding highlight.js file from the `libraries/highlightjs/${id}.js` instead of `node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`. */
highlightJsSource?: "libraries";
/** If specified, will load the corresponding highlight file from the given path instead of `node_modules`. */
codeMirrorSource?: string;
}
/**
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
*/
export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
{ default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
{ title: "APL", mime: "text/apl" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
{ title: "ASP.NET", mime: "application/x-aspx" },
{ title: "Asterisk", mime: "text/x-asterisk" },
{ title: "Batch file (DOS)", mime: "application/x-bat", highlightJs: "dos", codeMirrorSource: "libraries/codemirror/batch.js" },
{ title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" },
{ default: true, title: "C", mime: "text/x-csrc", highlightJs: "c" },
{ default: true, title: "C#", mime: "text/x-csharp", highlightJs: "csharp" },
{ default: true, title: "C++", mime: "text/x-c++src", highlightJs: "cpp" },
{ title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" },
{ title: "ClojureScript", mime: "text/x-clojurescript" },
{ title: "Closure Stylesheets (GSS)", mime: "text/x-gss" },
{ title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" },
{ title: "Cobol", mime: "text/x-cobol" },
{ title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" },
{ title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" },
{ title: "CQL", mime: "text/x-cassandra" },
{ title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" },
{ default: true, title: "CSS", mime: "text/css", highlightJs: "css" },
{ title: "Cypher", mime: "application/x-cypher-query" },
{ title: "Cython", mime: "text/x-cython" },
{ title: "D", mime: "text/x-d", highlightJs: "d" },
{ title: "Dart", mime: "application/dart", highlightJs: "dart" },
{ title: "diff", mime: "text/x-diff", highlightJs: "diff" },
{ title: "Django", mime: "text/x-django", highlightJs: "django" },
{ title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" },
{ title: "DTD", mime: "application/xml-dtd" },
{ title: "Dylan", mime: "text/x-dylan" },
{ title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" },
{ title: "ECL", mime: "text/x-ecl" },
{ title: "edn", mime: "application/edn" },
{ title: "Eiffel", mime: "text/x-eiffel" },
{ title: "Elm", mime: "text/x-elm", highlightJs: "elm" },
{ title: "Embedded Javascript", mime: "application/x-ejs" },
{ title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" },
{ title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" },
{ title: "Esper", mime: "text/x-esper" },
{ title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" },
{ title: "Factor", mime: "text/x-factor" },
{ title: "FCL", mime: "text/x-fcl" },
{ title: "Forth", mime: "text/x-forth" },
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
{ title: "Gas", mime: "text/x-gas" },
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
{ default: true, title: "Go", mime: "text/x-go", highlightJs: "go" },
{ default: true, title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy" },
{ title: "HAML", mime: "text/x-haml", highlightJs: "haml" },
{ default: true, title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell" },
{ title: "Haskell (Literate)", mime: "text/x-literate-haskell" },
{ title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" },
{ default: true, title: "HTML", mime: "text/html", highlightJs: "xml" },
{ default: true, title: "HTTP", mime: "message/http", highlightJs: "http" },
{ title: "HXML", mime: "text/x-hxml" },
{ title: "IDL", mime: "text/x-idl" },
{ default: true, title: "Java", mime: "text/x-java", highlightJs: "java" },
{ title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" },
{ title: "Jinja2", mime: "text/jinja2" },
{ default: true, title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript" },
{ default: true, title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript" },
{ default: true, title: "JSON", mime: "application/json", highlightJs: "json" },
{ title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" },
{ title: "JSX", mime: "text/jsx", highlightJs: "javascript" },
{ title: "Julia", mime: "text/x-julia", highlightJs: "julia" },
{ default: true, title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin" },
{ title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" },
{ title: "LESS", mime: "text/x-less", highlightJs: "less" },
{ title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" },
{ title: "Lua", mime: "text/x-lua", highlightJs: "lua" },
{ title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" },
{ default: true, title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown" },
{ title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" },
{ title: "mbox", mime: "application/mbox" },
{ title: "mIRC", mime: "text/mirc" },
{ title: "Modelica", mime: "text/x-modelica" },
{ title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" },
{ title: "mscgen", mime: "text/x-mscgen" },
{ title: "msgenny", mime: "text/x-msgenny" },
{ title: "MUMPS", mime: "text/x-mumps" },
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
{ title: "NTriples", mime: "application/n-triples" },
{ title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" },
{ title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" },
{ title: "Octave", mime: "text/x-octave" },
{ title: "Oz", mime: "text/x-oz" },
{ title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" },
{ title: "PEG.js", mime: "null" },
{ default: true, title: "Perl", mime: "text/x-perl" },
{ title: "PGP", mime: "application/pgp" },
{ default: true, title: "PHP", mime: "text/x-php" },
{ title: "Pig", mime: "text/x-pig" },
{ title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" },
{ title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" },
{ title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" },
{ title: "Properties files", mime: "text/x-properties", highlightJs: "properties" },
{ title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" },
{ title: "Pug", mime: "text/x-pug" },
{ title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" },
{ default: true, title: "Python", mime: "text/x-python", highlightJs: "python" },
{ title: "Q", mime: "text/x-q", highlightJs: "q" },
{ title: "R", mime: "text/x-rsrc", highlightJs: "r" },
{ title: "reStructuredText", mime: "text/x-rst" },
{ title: "RPM Changes", mime: "text/x-rpm-changes" },
{ title: "RPM Spec", mime: "text/x-rpm-spec" },
{ default: true, title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby" },
{ title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" },
{ title: "SAS", mime: "text/x-sas", highlightJs: "sas" },
{ title: "Sass", mime: "text/x-sass" },
{ title: "Scala", mime: "text/x-scala" },
{ title: "Scheme", mime: "text/x-scheme" },
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
{ default: true, title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash" },
{ title: "Sieve", mime: "application/sieve" },
{ title: "Slim", mime: "text/x-slim" },
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
{ title: "Smarty", mime: "text/x-smarty" },
{ title: "SML", mime: "text/x-sml", highlightJs: "sml" },
{ title: "Solr", mime: "text/x-solr" },
{ title: "Soy", mime: "text/x-soy" },
{ title: "SPARQL", mime: "application/sparql-query" },
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
{ default: true, title: "SQL", mime: "text/x-sql", highlightJs: "sql" },
{ title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" },
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql" },
{ title: "Squirrel", mime: "text/x-squirrel" },
{ title: "sTeX", mime: "text/x-stex" },
{ title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" },
{ default: true, title: "Swift", mime: "text/x-swift" },
{ title: "SystemVerilog", mime: "text/x-systemverilog" },
{ title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" },
{ title: "Terraform (HCL)", mime: "text/x-hcl", highlightJs: "terraform", highlightJsSource: "libraries", codeMirrorSource: "libraries/codemirror/hcl.js" },
{ title: "Textile", mime: "text/x-textile" },
{ title: "TiddlyWiki ", mime: "text/x-tiddlywiki" },
{ title: "Tiki wiki", mime: "text/tiki" },
{ title: "TOML", mime: "text/x-toml", highlightJs: "ini" },
{ title: "Tornado", mime: "text/x-tornado" },
{ title: "troff", mime: "text/troff" },
{ title: "TTCN", mime: "text/x-ttcn" },
{ title: "TTCN_CFG", mime: "text/x-ttcn-cfg" },
{ title: "Turtle", mime: "text/turtle" },
{ title: "Twig", mime: "text/x-twig", highlightJs: "twig" },
{ title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" },
{ title: "TypeScript-JSX", mime: "text/typescript-jsx" },
{ title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" },
{ title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" },
{ title: "Velocity", mime: "text/velocity" },
{ title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" },
{ title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" },
{ title: "Vue.js Component", mime: "text/x-vue" },
{ title: "Web IDL", mime: "text/x-webidl" },
{ default: true, title: "XML", mime: "text/xml", highlightJs: "xml" },
{ title: "XQuery", mime: "application/xquery", highlightJs: "xquery" },
{ title: "xu", mime: "text/x-xu" },
{ title: "Yacas", mime: "text/x-yacas" },
{ default: true, title: "YAML", mime: "text/x-yaml", highlightJs: "yaml" },
{ title: "Z80", mime: "text/x-z80" }
]);
/**
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
* code plugin.
*
* @param mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
* @returns the normalized MIME type (e.g. `text-c-src`).
*/
export function normalizeMimeTypeForCKEditor(mimeType: string) {
return mimeType.toLowerCase().replace(/[\W_]+/g, "-");
}
let byHighlightJsNameMappings: Record<string, MimeTypeDefinition> | null = null;
/**
* Given a Highlight.js language tag (e.g. `css`), it returns a corresponding {@link MimeTypeDefinition} if found.
*
* If there are multiple {@link MimeTypeDefinition}s for the language tag, then only the first one is retrieved. For example for `javascript`, the "JS frontend" mime type is returned.
*
* @param highlightJsName a language tag.
* @returns the corresponding {@link MimeTypeDefinition} if found, or `undefined` otherwise.
*/
export function getMimeTypeFromHighlightJs(highlightJsName: string) {
if (!byHighlightJsNameMappings) {
byHighlightJsNameMappings = {};
for (const mimeType of MIME_TYPES_DICT) {
if (mimeType.highlightJs && !byHighlightJsNameMappings[mimeType.highlightJs]) {
byHighlightJsNameMappings[mimeType.highlightJs] = mimeType;
}
}
}
return byHighlightJsNameMappings[highlightJsName];
}

View File

@@ -1,202 +1,19 @@
import { MIME_TYPE_AUTO, MIME_TYPES_DICT, normalizeMimeTypeForCKEditor, type MimeTypeDefinition } from "./mime_type_definitions.js";
import options from "./options.js";
/**
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
*/
const MIME_TYPE_AUTO = "text-x-trilium-auto";
/**
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
*/
interface MimeTypeDefinition {
default?: boolean;
title: string;
mime: string;
/** The name of the language/mime type as defined by highlight.js (or one of the aliases), in order to be used for syntax highlighting such as inside code blocks. */
highlightJs?: string;
/** If specified, will load the corresponding highlight.js file from the `libraries/highlightjs/${id}.js` instead of `node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`. */
highlightJsSource?: "libraries";
/** If specified, will load the corresponding highlight file from the given path instead of `node_modules`. */
codeMirrorSource?: string;
}
interface MimeType extends MimeTypeDefinition {
enabled: boolean
enabled: boolean;
}
const MIME_TYPES_DICT: MimeTypeDefinition[] = [
{ default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
{ title: "APL", mime: "text/apl" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
{ title: "ASP.NET", mime: "application/x-aspx" },
{ title: "Asterisk", mime: "text/x-asterisk" },
{ title: "Batch file (DOS)", mime: "application/x-bat", highlightJs: "dos", codeMirrorSource: "libraries/codemirror/batch.js" },
{ title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" },
{ default: true, title: "C", mime: "text/x-csrc", highlightJs: "c" },
{ default: true, title: "C#", mime: "text/x-csharp", highlightJs: "csharp" },
{ default: true, title: "C++", mime: "text/x-c++src", highlightJs: "cpp" },
{ title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" },
{ title: "ClojureScript", mime: "text/x-clojurescript" },
{ title: "Closure Stylesheets (GSS)", mime: "text/x-gss" },
{ title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" },
{ title: "Cobol", mime: "text/x-cobol" },
{ title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" },
{ title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" },
{ title: "CQL", mime: "text/x-cassandra" },
{ title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" },
{ default: true, title: "CSS", mime: "text/css", highlightJs: "css" },
{ title: "Cypher", mime: "application/x-cypher-query" },
{ title: "Cython", mime: "text/x-cython" },
{ title: "D", mime: "text/x-d", highlightJs: "d" },
{ title: "Dart", mime: "application/dart", highlightJs: "dart" },
{ title: "diff", mime: "text/x-diff", highlightJs: "diff" },
{ title: "Django", mime: "text/x-django", highlightJs: "django" },
{ title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" },
{ title: "DTD", mime: "application/xml-dtd" },
{ title: "Dylan", mime: "text/x-dylan" },
{ title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" },
{ title: "ECL", mime: "text/x-ecl" },
{ title: "edn", mime: "application/edn" },
{ title: "Eiffel", mime: "text/x-eiffel" },
{ title: "Elm", mime: "text/x-elm", highlightJs: "elm" },
{ title: "Embedded Javascript", mime: "application/x-ejs" },
{ title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" },
{ title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" },
{ title: "Esper", mime: "text/x-esper" },
{ title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" },
{ title: "Factor", mime: "text/x-factor" },
{ title: "FCL", mime: "text/x-fcl" },
{ title: "Forth", mime: "text/x-forth" },
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
{ title: "Gas", mime: "text/x-gas" },
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
{ default: true, title: "Go", mime: "text/x-go", highlightJs: "go" },
{ default: true, title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy" },
{ title: "HAML", mime: "text/x-haml", highlightJs: "haml" },
{ default: true, title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell" },
{ title: "Haskell (Literate)", mime: "text/x-literate-haskell" },
{ title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" },
{ default: true, title: "HTML", mime: "text/html", highlightJs: "xml" },
{ default: true, title: "HTTP", mime: "message/http", highlightJs: "http" },
{ title: "HXML", mime: "text/x-hxml" },
{ title: "IDL", mime: "text/x-idl" },
{ default: true, title: "Java", mime: "text/x-java", highlightJs: "java" },
{ title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" },
{ title: "Jinja2", mime: "text/jinja2" },
{ default: true, title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript" },
{ default: true, title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript" },
{ default: true, title: "JSON", mime: "application/json", highlightJs: "json" },
{ title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" },
{ title: "JSX", mime: "text/jsx", highlightJs: "javascript" },
{ title: "Julia", mime: "text/x-julia", highlightJs: "julia" },
{ default: true, title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin" },
{ title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" },
{ title: "LESS", mime: "text/x-less", highlightJs: "less" },
{ title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" },
{ title: "Lua", mime: "text/x-lua", highlightJs: "lua" },
{ title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" },
{ default: true, title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown" },
{ title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" },
{ title: "mbox", mime: "application/mbox" },
{ title: "mIRC", mime: "text/mirc" },
{ title: "Modelica", mime: "text/x-modelica" },
{ title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" },
{ title: "mscgen", mime: "text/x-mscgen" },
{ title: "msgenny", mime: "text/x-msgenny" },
{ title: "MUMPS", mime: "text/x-mumps" },
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
{ title: "NTriples", mime: "application/n-triples" },
{ title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" },
{ title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" },
{ title: "Octave", mime: "text/x-octave" },
{ title: "Oz", mime: "text/x-oz" },
{ title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" },
{ title: "PEG.js", mime: "null" },
{ default: true, title: "Perl", mime: "text/x-perl" },
{ title: "PGP", mime: "application/pgp" },
{ default: true, title: "PHP", mime: "text/x-php" },
{ title: "Pig", mime: "text/x-pig" },
{ title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" },
{ title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" },
{ title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" },
{ title: "Properties files", mime: "text/x-properties", highlightJs: "properties" },
{ title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" },
{ title: "Pug", mime: "text/x-pug" },
{ title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" },
{ default: true, title: "Python", mime: "text/x-python", highlightJs: "python" },
{ title: "Q", mime: "text/x-q", highlightJs: "q" },
{ title: "R", mime: "text/x-rsrc", highlightJs: "r" },
{ title: "reStructuredText", mime: "text/x-rst" },
{ title: "RPM Changes", mime: "text/x-rpm-changes" },
{ title: "RPM Spec", mime: "text/x-rpm-spec" },
{ default: true, title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby" },
{ title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" },
{ title: "SAS", mime: "text/x-sas", highlightJs: "sas" },
{ title: "Sass", mime: "text/x-sass" },
{ title: "Scala", mime: "text/x-scala" },
{ title: "Scheme", mime: "text/x-scheme" },
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
{ default: true, title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash" },
{ title: "Sieve", mime: "application/sieve" },
{ title: "Slim", mime: "text/x-slim" },
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
{ title: "Smarty", mime: "text/x-smarty" },
{ title: "SML", mime: "text/x-sml", highlightJs: "sml" },
{ title: "Solr", mime: "text/x-solr" },
{ title: "Soy", mime: "text/x-soy" },
{ title: "SPARQL", mime: "application/sparql-query" },
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
{ default: true, title: "SQL", mime: "text/x-sql", highlightJs: "sql" },
{ title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" },
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql" },
{ title: "Squirrel", mime: "text/x-squirrel" },
{ title: "sTeX", mime: "text/x-stex" },
{ title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" },
{ default: true, title: "Swift", mime: "text/x-swift" },
{ title: "SystemVerilog", mime: "text/x-systemverilog" },
{ title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" },
{ title: "Terraform (HCL)", mime: "text/x-hcl", highlightJs: "terraform", highlightJsSource: "libraries", codeMirrorSource: "libraries/codemirror/hcl.js" },
{ title: "Textile", mime: "text/x-textile" },
{ title: "TiddlyWiki ", mime: "text/x-tiddlywiki" },
{ title: "Tiki wiki", mime: "text/tiki" },
{ title: "TOML", mime: "text/x-toml", highlightJs: "ini" },
{ title: "Tornado", mime: "text/x-tornado" },
{ title: "troff", mime: "text/troff" },
{ title: "TTCN", mime: "text/x-ttcn" },
{ title: "TTCN_CFG", mime: "text/x-ttcn-cfg" },
{ title: "Turtle", mime: "text/turtle" },
{ title: "Twig", mime: "text/x-twig", highlightJs: "twig" },
{ title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" },
{ title: "TypeScript-JSX", mime: "text/typescript-jsx" },
{ title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" },
{ title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" },
{ title: "Velocity", mime: "text/velocity" },
{ title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" },
{ title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" },
{ title: "Vue.js Component", mime: "text/x-vue" },
{ title: "Web IDL", mime: "text/x-webidl" },
{ default: true, title: "XML", mime: "text/xml", highlightJs: "xml" },
{ title: "XQuery", mime: "application/xquery", highlightJs: "xquery" },
{ title: "xu", mime: "text/x-xu" },
{ title: "Yacas", mime: "text/x-yacas" },
{ default: true, title: "YAML", mime: "text/x-yaml", highlightJs: "yaml" },
{ title: "Z80", mime: "text/x-z80" }
];
let mimeTypes: MimeType[] | null = null;
function loadMimeTypes() {
mimeTypes = JSON.parse(JSON.stringify(MIME_TYPES_DICT)) as MimeType[]; // clone
const enabledMimeTypes = options.getJson('codeNotesMimeTypes')
|| MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime);
const enabledMimeTypes = options.getJson("codeNotesMimeTypes") || MIME_TYPES_DICT.filter((mt) => mt.default).map((mt) => mt.mime);
for (const mt of mimeTypes) {
mt.enabled = enabledMimeTypes.includes(mt.mime) || mt.mime === 'text/plain'; // text/plain is always enabled
mt.enabled = enabledMimeTypes.includes(mt.mime) || mt.mime === "text/plain"; // text/plain is always enabled
}
}
@@ -234,22 +51,9 @@ function getHighlightJsNameForMime(mimeType: string) {
return mimeToHighlightJsMapping[mimeType];
}
/**
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
* code plugin.
*
* @param mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
* @returns the normalized MIME type (e.g. `text-c-src`).
*/
function normalizeMimeTypeForCKEditor(mimeType: string) {
return mimeType.toLowerCase()
.replace(/[\W_]+/g,"-");
}
export default {
MIME_TYPE_AUTO,
getMimeTypes,
loadMimeTypes,
getHighlightJsNameForMime,
normalizeMimeTypeForCKEditor
}
getHighlightJsNameForMime
};

View File

@@ -9,7 +9,7 @@ import FAttribute from "../entities/fattribute.js";
*/
class NoteAttributeCache {
attributes: Record<string, FAttribute[]>;
constructor() {
this.attributes = {};
}

View File

@@ -1,7 +1,7 @@
import server from "./server.js";
import appContext from "../components/app_context.js";
import utils from './utils.js';
import noteCreateService from './note_create.js';
import utils from "./utils.js";
import noteCreateService from "./note_create.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
@@ -31,36 +31,42 @@ interface Options {
async function autocompleteSourceForCKEditor(queryText: string) {
return await new Promise<MentionItem[]>((res, rej) => {
autocompleteSource(queryText, rows => {
res(rows.map(row => {
return {
action: row.action,
noteTitle: row.noteTitle,
id: `@${row.notePathTitle}`,
name: row.notePathTitle || "",
link: `#${row.notePath}`,
notePath: row.notePath,
highlightedNotePathTitle: row.highlightedNotePathTitle
}
}));
}, {
allowCreatingNotes: true
});
autocompleteSource(
queryText,
(rows) => {
res(
rows.map((row) => {
return {
action: row.action,
noteTitle: row.noteTitle,
id: `@${row.notePathTitle}`,
name: row.notePathTitle || "",
link: `#${row.notePath}`,
notePath: row.notePath,
highlightedNotePathTitle: row.highlightedNotePathTitle
};
})
);
},
{
allowCreatingNotes: true
}
);
});
}
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
const fastSearch = options.fastSearch === false ? false : true;
if (fastSearch === false) {
if (term.trim().length === 0){
if (term.trim().length === 0) {
return;
}
cb(
[{
cb([
{
noteTitle: term,
highlightedNotePathTitle: t("quick-search.searching")
}]
);
}
]);
}
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
@@ -69,9 +75,9 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
if (term.trim().length >= 1 && options.allowCreatingNotes) {
results = [
{
action: 'create-note',
action: "create-note",
noteTitle: term,
parentNoteId: activeNoteId || 'root',
parentNoteId: activeNoteId || "root",
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
} as Suggestion
].concat(results);
@@ -80,7 +86,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
if (term.trim().length >= 1 && options.allowJumpToSearchNotes) {
results = results.concat([
{
action: 'search-notes',
action: "search-notes",
noteTitle: term,
highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
}
@@ -90,7 +96,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [
{
action: 'external-link',
action: "external-link",
externalLink: term,
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
} as Suggestion
@@ -102,42 +108,42 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
function clearText($el: JQuery<HTMLElement>) {
$el.setSelectedNotePath("");
$el.autocomplete("val", "").trigger('change');
$el.autocomplete("val", "").trigger("change");
}
function setText($el: JQuery<HTMLElement>, text: string) {
$el.setSelectedNotePath("");
$el
.autocomplete("val", text.trim())
.autocomplete("open");
$el.autocomplete("val", text.trim()).autocomplete("open");
}
function showRecentNotes($el:JQuery<HTMLElement>) {
function showRecentNotes($el: JQuery<HTMLElement>) {
$el.setSelectedNotePath("");
$el.autocomplete("val", "");
$el.autocomplete('open');
$el.trigger('focus');
$el.autocomplete("open");
$el.trigger("focus");
}
function fullTextSearch($el: JQuery<HTMLElement>, options: Options){
const searchString = $el.autocomplete('val') as unknown as string;
function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
const searchString = $el.autocomplete("val") as unknown as string;
if (options.fastSearch === false || searchString?.trim().length === 0) {
return;
}
$el.trigger('focus');
$el.trigger("focus");
options.fastSearch = false;
$el.autocomplete('val', '');
$el.autocomplete()
$el.autocomplete("val", "");
$el.autocomplete();
$el.setSelectedNotePath("");
$el.autocomplete('val', searchString);
$el.autocomplete("val", searchString);
// Set a delay to avoid resetting to true before full text search (await server.get) is called.
setTimeout(() => { options.fastSearch = true; }, 100);
setTimeout(() => {
options.fastSearch = true;
}, 100);
}
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
if ($el.hasClass("note-autocomplete-input")) {
// clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected');
$el.off("autocomplete:noteselected");
return $el;
}
@@ -146,20 +152,15 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.addClass("note-autocomplete-input");
const $clearTextButton = $("<button>")
.addClass("input-group-text input-clearer-button bx bxs-tag-x")
.prop("title", t("note_autocomplete.clear-text-field"));
const $clearTextButton = $("<button>").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field"));
const $showRecentNotesButton = $("<button>")
.addClass("input-group-text show-recent-notes-button bx bx-time")
.prop("title", t("note_autocomplete.show-recent-notes"));
const $showRecentNotesButton = $("<button>").addClass("input-group-text show-recent-notes-button bx bx-time").prop("title", t("note_autocomplete.show-recent-notes"));
const $fullTextSearchButton = $("<button>")
.addClass("input-group-text full-text-search-button bx bx-search")
.prop("title", `${t("note_autocomplete.full-text-search")} (Shift+Enter)`);
const $goToSelectedNoteButton = $("<a>")
.addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
@@ -167,9 +168,9 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.after($goToSelectedNoteButton);
}
$clearTextButton.on('click', () => clearText($el));
$clearTextButton.on("click", () => clearText($el));
$showRecentNotesButton.on('click', e => {
$showRecentNotesButton.on("click", (e) => {
showRecentNotes($el);
// this will cause the click not give focus to the "show recent notes" button
@@ -177,7 +178,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
return false;
});
$fullTextSearchButton.on('click', e => {
$fullTextSearchButton.on("click", (e) => {
fullTextSearch($el, options);
return false;
});
@@ -185,53 +186,56 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
let autocompleteOptions: AutoCompleteConfig = {};
if (options.container) {
autocompleteOptions.dropdownMenuContainer = options.container;
autocompleteOptions.debug = true; // don't close on blur
autocompleteOptions.debug = true; // don't close on blur
}
if (options.allowJumpToSearchNotes) {
$el.on('keydown', (event) => {
if (event.ctrlKey && event.key === 'Enter') {
$el.on("keydown", (event) => {
if (event.ctrlKey && event.key === "Enter") {
// Prevent Ctrl + Enter from triggering autoComplete.
event.stopImmediatePropagation();
event.preventDefault();
$el.trigger('autocomplete:selected', { action: 'search-notes', noteTitle: $el.autocomplete("val")});
$el.trigger("autocomplete:selected", { action: "search-notes", noteTitle: $el.autocomplete("val") });
}
});
}
$el.on('keydown', async (event) => {
if (event.shiftKey && event.key === 'Enter') {
$el.on("keydown", async (event) => {
if (event.shiftKey && event.key === "Enter") {
// Prevent Enter from triggering autoComplete.
event.stopImmediatePropagation();
event.preventDefault();
fullTextSearch($el,options)
fullTextSearch($el, options);
}
});
$el.autocomplete({
...autocompleteOptions,
appendTo: document.querySelector('body'),
hint: false,
autoselect: true,
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
// re-querying of the autocomplete source which then changes the currently selected suggestion
openOnFocus: false,
minLength: 0,
tabAutocomplete: false
}, [
$el.autocomplete(
{
source: (term, cb) => autocompleteSource(term, cb, options),
displayKey: 'notePathTitle',
templates: {
suggestion: suggestion => suggestion.highlightedNotePathTitle
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache: false
}
]);
...autocompleteOptions,
appendTo: document.querySelector("body"),
hint: false,
autoselect: true,
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
// re-querying of the autocomplete source which then changes the currently selected suggestion
openOnFocus: false,
minLength: 0,
tabAutocomplete: false
},
[
{
source: (term, cb) => autocompleteSource(term, cb, options),
displayKey: "notePathTitle",
templates: {
suggestion: (suggestion) => suggestion.highlightedNotePathTitle
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache: false
}
]
);
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
($el as any).on('autocomplete:selected', async (event: Event, suggestion: Suggestion) => {
if (suggestion.action === 'external-link') {
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
if (suggestion.action === "external-link") {
$el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink);
@@ -239,12 +243,12 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.autocomplete("close");
$el.trigger('autocomplete:externallinkselected', [suggestion]);
$el.trigger("autocomplete:externallinkselected", [suggestion]);
return;
}
if (suggestion.action === 'create-note') {
if (suggestion.action === "create-note") {
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
if (!success) {
@@ -262,9 +266,9 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
}
if (suggestion.action === 'search-notes') {
if (suggestion.action === "search-notes") {
const searchString = suggestion.noteTitle;
appContext.triggerCommand('searchNotes', { searchString });
appContext.triggerCommand("searchNotes", { searchString });
return;
}
@@ -275,23 +279,23 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.autocomplete("close");
$el.trigger('autocomplete:noteselected', [suggestion]);
$el.trigger("autocomplete:noteselected", [suggestion]);
});
$el.on('autocomplete:closed', () => {
$el.on("autocomplete:closed", () => {
if (!String($el.val())?.trim()) {
clearText($el);
}
});
$el.on('autocomplete:opened', () => {
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
$el.autocomplete("close");
}
});
// clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected');
$el.off("autocomplete:noteselected");
return $el;
}
@@ -312,21 +316,17 @@ function init() {
return null;
}
const chunks = notePath.split('/');
const chunks = notePath.split("/");
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
}
};
$.fn.setSelectedNotePath = function (notePath) {
notePath = notePath || "";
$(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", !notePath.trim())
.attr("href", `#${notePath}`); // we also set href here so tooltip can be displayed
$(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", !notePath.trim()).attr("href", `#${notePath}`); // we also set href here so tooltip can be displayed
};
$.fn.getSelectedExternalLink = function () {
@@ -340,12 +340,9 @@ function init() {
$.fn.setSelectedExternalLink = function (externalLink) {
if (externalLink) {
// TODO: This doesn't seem to do anything with the external link, is it normal?
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", true);
$(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", true);
}
}
};
$.fn.setNote = async function (noteId) {
const note = noteId ? await froca.getNote(noteId, true) : null;
@@ -353,7 +350,7 @@ function init() {
$(this)
.val(note ? note.title : "")
.setSelectedNotePath(noteId);
}
};
}
export default {
@@ -362,4 +359,4 @@ export default {
showRecentNotes,
setText,
init
}
};

View File

@@ -8,7 +8,7 @@ import toastService from "./toast.js";
import { t } from "./i18n.js";
import FNote from "../entities/fnote.js";
import FBranch from "../entities/fbranch.js";
import { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
interface CreateNoteOpts {
isProtected?: boolean;
@@ -26,7 +26,7 @@ interface CreateNoteOpts {
// TODO: Replace with interface once note_context.js is converted.
getSelectedHtml(): string;
removeSelection(): void;
}
};
}
interface Response {
@@ -41,11 +41,14 @@ interface DuplicateResponse {
}
async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) {
options = Object.assign({
activate: true,
focus: 'title',
target: 'into'
}, options);
options = Object.assign(
{
activate: true,
focus: "title",
target: "into"
},
options
);
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted,
// but this is quite weird since the user doesn't see WHERE the note is being created, so it shouldn't occur often
@@ -53,25 +56,25 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
options.isProtected = false;
}
if (appContext.tabManager.getActiveContextNoteType() !== 'text') {
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
options.saveSelection = false;
}
if (options.saveSelection && options.textEditor) {
if (options.saveSelection && options.textEditor) {
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
}
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
if (options.type === 'mermaid' && !options.content) {
if (options.type === "mermaid" && !options.content) {
options.content = `graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`
C-->D;`;
}
const {note, branch} = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
title: options.title,
content: options.content || "",
isProtected: options.isProtected,
@@ -91,11 +94,10 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
const activeNoteContext = appContext.tabManager.getActiveContext();
await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
if (options.focus === 'title') {
appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true});
}
else if (options.focus === 'content') {
appContext.triggerEvent('focusOnDetail', {ntxId: activeNoteContext.ntxId});
if (options.focus === "title") {
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
} else if (options.focus === "content") {
appContext.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId });
}
}
@@ -109,15 +111,15 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
}
async function chooseNoteType() {
return new Promise<ChooseNoteTypeResponse>(res => {
return new Promise<ChooseNoteTypeResponse>((res) => {
// TODO: Remove ignore after callback for chooseNoteType is defined in app_context.ts
//@ts-ignore
appContext.triggerCommand("chooseNoteType", {callback: res});
appContext.triggerCommand("chooseNoteType", { callback: res });
});
}
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
const {success, noteType, templateNoteId} = await chooseNoteType();
const { success, noteType, templateNoteId } = await chooseNoteType();
if (!success) {
return;
@@ -143,15 +145,14 @@ function parseSelectedHtml(selectedHtml: string) {
const content = selectedHtml.replace(dom[0].outerHTML, "");
return [title, content];
}
else {
} else {
return [null, selectedHtml];
}
}
async function duplicateSubtree(noteId: string, parentNotePath: string) {
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
const {note} = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
const { note } = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
await ws.waitForMaxKnownEntityChangeId();

View File

@@ -158,7 +158,6 @@ const TPL = `
</div>`;
class NoteListRenderer {
private $noteList: JQuery<HTMLElement>;
private parentNote: FNote;
@@ -181,7 +180,7 @@ class NoteListRenderer {
this.parentNote = parentNote;
const includedNoteIds = this.getIncludedNoteIds();
this.noteIds = noteIds.filter(noteId => !includedNoteIds.has(noteId) && noteId !== '_hidden');
this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
if (this.noteIds.length === 0) {
return;
@@ -190,17 +189,17 @@ class NoteListRenderer {
$parent.append(this.$noteList);
this.page = 1;
this.pageSize = parseInt(parentNote.getLabelValue('pageSize') || "");
this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || "");
if (!this.pageSize || this.pageSize < 1) {
this.pageSize = 20;
}
this.viewType = parentNote.getLabelValue('viewType');
this.viewType = parentNote.getLabelValue("viewType");
if (!['list', 'grid'].includes(this.viewType || "")) {
if (!["list", "grid"].includes(this.viewType || "")) {
// when not explicitly set, decide based on the note type
this.viewType = parentNote.type === 'search' ? 'list' : 'grid';
this.viewType = parentNote.type === "search" ? "list" : "grid";
}
this.$noteList.addClass(`${this.viewType}-view`);
@@ -211,11 +210,9 @@ class NoteListRenderer {
/** @returns {Set<string>} list of noteIds included (images, included notes) in the parent note and which
* don't have to be shown in the note list. */
getIncludedNoteIds() {
const includedLinks = this.parentNote
? this.parentNote.getRelations().filter(rel => rel.name === 'imageLink' || rel.name === 'includeNoteLink')
: [];
const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
return new Set(includedLinks.map(rel => rel.value));
return new Set(includedLinks.map((rel) => rel.value));
}
async renderList() {
@@ -228,18 +225,16 @@ class NoteListRenderer {
if (highlightedTokens.length > 0) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
const regex = highlightedTokens
.map(token => utils.escapeRegExp(token))
.join("|");
const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|");
this.highlightRegex = new RegExp(regex, 'gi');
this.highlightRegex = new RegExp(regex, "gi");
} else {
this.highlightRegex = null;
}
this.$noteList.show();
const $container = this.$noteList.find('.note-list-container').empty();
const $container = this.$noteList.find(".note-list-container").empty();
const startIdx = (this.page - 1) * this.pageSize;
const endIdx = startIdx + this.pageSize;
@@ -248,7 +243,7 @@ class NoteListRenderer {
const pageNotes = await froca.getNotes(pageNoteIds);
for (const note of pageNotes) {
const $card = await this.renderNote(note, this.parentNote.isLabelTruthy('expanded'));
const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded"));
$container.append($card);
}
@@ -259,7 +254,7 @@ class NoteListRenderer {
}
renderPager() {
const $pager = this.$noteList.find('.note-list-pager').empty();
const $pager = this.$noteList.find(".note-list-pager").empty();
if (!this.page || !this.pageSize) {
return;
}
@@ -279,18 +274,17 @@ class NoteListRenderer {
$pager.append(
i === this.page
? $('<span>').text(i).css('text-decoration', 'underline').css('font-weight', "bold")
? $("<span>").text(i).css("text-decoration", "underline").css("font-weight", "bold")
: $('<a href="javascript:">')
.text(i)
.attr("title", `Page of ${startIndex} - ${endIndex}`)
.on('click', () => {
this.page = i;
this.renderList();
}),
.text(i)
.attr("title", `Page of ${startIndex} - ${endIndex}`)
.on("click", () => {
this.page = i;
this.renderList();
}),
" &nbsp; "
);
}
else if (lastPrinted) {
} else if (lastPrinted) {
$pager.append("... &nbsp; ");
lastPrinted = false;
@@ -304,33 +298,34 @@ class NoteListRenderer {
async renderNote(note: FNote, expand: boolean = false) {
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
const notePath = this.parentNote.type === 'search'
? note.noteId // for search note parent, we want to display a non-search path
: `${this.parentNote.noteId}/${note.noteId}`;
const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note);
const notePath =
this.parentNote.type === "search"
? note.noteId // for search note parent, we want to display a non-search path
: `${this.parentNote.noteId}/${note.noteId}`;
const $card = $('<div class="note-book-card">')
.attr('data-note-id', note.noteId)
.attr("data-note-id", note.noteId)
.append(
$('<h5 class="note-book-header">')
.append($expander)
.append($('<span class="note-icon">').addClass(note.getIcon()))
.append(this.viewType === 'grid'
? $('<span class="note-book-title">').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId))
: (await linkService.createLink(notePath, {showTooltip: false, showNotePath: this.showNotePath}))
.addClass("note-book-title")
.append(
this.viewType === "grid"
? $('<span class="note-book-title">').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId))
: (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title")
)
.append($renderedAttributes)
);
if (this.viewType === 'grid') {
if (this.viewType === "grid") {
$card
.addClass("block-link")
.attr("data-href", `#${notePath}`)
.on('click', e => linkService.goToLink(e));
.on("click", (e) => linkService.goToLink(e));
}
$expander.on('click', () => this.toggleContent($card, note, !$card.hasClass("expanded")));
$expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded")));
if (this.highlightRegex) {
$card.find(".note-book-title").markRegExp(this.highlightRegex, {
@@ -347,22 +342,21 @@ class NoteListRenderer {
}
async toggleContent($card: JQuery<HTMLElement>, note: FNote, expand: boolean) {
if (this.viewType === 'list' && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) {
if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) {
return;
}
const $expander = $card.find('> .note-book-header .note-expander');
const $expander = $card.find("> .note-book-header .note-expander");
if (expand || this.viewType === 'grid') {
if (expand || this.viewType === "grid") {
$card.addClass("expanded");
$expander.addClass("bx-chevron-down").removeClass("bx-chevron-right");
}
else {
} else {
$card.removeClass("expanded");
$expander.addClass("bx-chevron-right").removeClass("bx-chevron-down");
}
if ((expand || this.viewType === 'grid') && $card.find('.note-book-content').length === 0) {
if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) {
$card.append(await this.renderNoteContent(note));
}
}
@@ -371,8 +365,8 @@ class NoteListRenderer {
const $content = $('<div class="note-book-content">');
try {
const {$renderedContent, type} = await contentRenderer.getRenderedContent(note, {
trim: this.viewType === 'grid' // for grid only short content is needed
const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, {
trim: this.viewType === "grid" // for grid only short content is needed
});
if (this.highlightRegex) {
@@ -393,11 +387,10 @@ class NoteListRenderer {
$content.append("rendering error");
}
if (this.viewType === 'list') {
const imageLinks = note.getRelations('imageLink');
if (this.viewType === "list") {
const imageLinks = note.getRelations("imageLink");
const childNotes = (await note.getChildNotes())
.filter(childNote => !imageLinks.find(rel => rel.value === childNote.noteId));
const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
for (const childNote of childNotes) {
$content.append(await this.renderNote(childNote));

View File

@@ -12,7 +12,7 @@ function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler);
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
$(document).on("click", e => {
$(document).on("click", (e) => {
if ($(e.target).closest(".note-tooltip").length) {
// click within the tooltip shouldn't close it
return;
@@ -23,11 +23,11 @@ function setupGlobalTooltip() {
}
function cleanUpTooltips() {
$('.note-tooltip').remove();
$(".note-tooltip").remove();
}
function setupElementTooltip($el: JQuery<HTMLElement>) {
$el.on('mouseenter', mouseEnterHandler);
$el.on("mouseenter", mouseEnterHandler);
}
async function mouseEnterHandler(this: HTMLElement) {
@@ -51,7 +51,7 @@ async function mouseEnterHandler(this: HTMLElement) {
return;
}
if (!notePath || !noteId || viewScope?.viewMode !== 'default') {
if (!notePath || !noteId || viewScope?.viewMode !== "default") {
return;
}
@@ -67,13 +67,13 @@ async function mouseEnterHandler(this: HTMLElement) {
if (url?.startsWith("#fn")) {
renderPromise = renderFootnote($link, url);
} else {
renderPromise = renderTooltip(await froca.getNote(noteId))
renderPromise = renderTooltip(await froca.getNote(noteId));
}
const [content] = await Promise.all([
renderPromise,
// to reduce flicker due to accidental mouseover, cursor must stay for a bit over the link for tooltip to appear
new Promise(res => setTimeout(res, 500))
new Promise((res) => setTimeout(res, 500))
]);
if (!content || utils.isHtmlEmpty(content)) {
@@ -81,18 +81,18 @@ async function mouseEnterHandler(this: HTMLElement) {
}
const html = `<div class="note-tooltip-content">${content}</div>`;
const tooltipClass = 'tooltip-' + Math.floor(Math.random() * 999_999_999);
const tooltipClass = "tooltip-" + Math.floor(Math.random() * 999_999_999);
// we need to check if we're still hovering over the element
// since the operation to get tooltip content was async, it is possible that
// we now create tooltip which won't close because it won't receive mouseleave event
if ($(this).filter(":hover").length > 0) {
$(this).tooltip({
container: 'body',
container: "body",
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
// with bottom this flickering happens a bit less
placement: 'bottom',
trigger: 'manual',
placement: "bottom",
trigger: "manual",
//TODO: boundary No longer applicable?
//boundary: 'window',
title: html,
@@ -103,7 +103,7 @@ async function mouseEnterHandler(this: HTMLElement) {
});
cleanUpTooltips();
$(this).tooltip('show');
$(this).tooltip("show");
// Dismiss the tooltip immediately if a link was clicked inside the tooltip.
$(`.${tooltipClass} a`).on("click", (e) => {
@@ -121,7 +121,7 @@ async function mouseEnterHandler(this: HTMLElement) {
} else {
setTimeout(checkTooltip, 1000);
}
}
};
setTimeout(checkTooltip, 1000);
}
@@ -142,12 +142,12 @@ async function renderTooltip(note: FNote | null) {
const noteTitleWithPathAsSuffix = await treeService.getNoteTitleWithPathAsSuffix(bestNotePath);
let content = "";
if (noteTitleWithPathAsSuffix) {
content = `<h5 class="note-tooltip-title">${noteTitleWithPathAsSuffix.prop('outerHTML')}</h5>`;
content = `<h5 class="note-tooltip-title">${noteTitleWithPathAsSuffix.prop("outerHTML")}</h5>`;
}
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note);
const {$renderedContent} = await contentRenderer.getRenderedContent(note, {
const { $renderedContent } = await contentRenderer.getRenderedContent(note, {
tooltip: true,
trim: true
});
@@ -161,11 +161,11 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) {
// A footnote text reference
const footnoteRef = url.substring(3);
const $footnoteContent = $link
.closest(".ck-content") // find the parent CK content
.find("> .footnote-section") // find the footnote section
.find(`a[href="#fnref${footnoteRef}"]`) // find the footnote link
.closest(".footnote-item") // find the parent container of the footnote
.find(".footnote-content"); // find the actual text content of the footnote
.closest(".ck-content") // find the parent CK content
.find("> .footnote-section") // find the footnote section
.find(`a[href="#fnref${footnoteRef}"]`) // find the footnote link
.closest(".footnote-item") // find the parent container of the footnote
.find(".footnote-content"); // find the actual text content of the footnote
return $footnoteContent.html() || "";
}
@@ -173,4 +173,4 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) {
export default {
setupGlobalTooltip,
setupElementTooltip
}
};

View File

@@ -1,8 +1,8 @@
import server from "./server.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import { MenuItem } from "../menus/context_menu.js";
import { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js";
import type { MenuItem } from "../menus/context_menu.js";
import type { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js";
type NoteTypeCommandNames = FilteredCommandNames<ContextMenuCommandData>;
@@ -43,4 +43,4 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) {
export default {
getNoteTypeItems
}
};

View File

@@ -1,14 +1,14 @@
import utils from "./utils.js";
import server from "./server.js";
type ExecFunction = (command: string, cb: ((err: string, stdout: string, stderror: string) => void)) => void;
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
interface TmpResponse {
tmpFilePath: string;
}
function checkType(type: string) {
if (type !== 'notes' && type !== 'attachments') {
if (type !== "notes" && type !== "attachments") {
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
}
}
@@ -27,7 +27,7 @@ function getOpenFileUrl(type: string, noteId: string) {
function download(url: string) {
if (utils.isElectron()) {
const remote = utils.dynamicRequire('@electron/remote');
const remote = utils.dynamicRequire("@electron/remote");
remote.getCurrentWebContents().downloadURL(url);
} else {
@@ -36,13 +36,13 @@ function download(url: string) {
}
function downloadFileNote(noteId: string) {
const url = `${getFileUrl('notes', noteId)}?${Date.now()}`; // don't use cache
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
download(url);
}
function downloadAttachment(attachmentId: string) {
const url = `${getFileUrl('attachments', attachmentId)}?${Date.now()}`; // don't use cache
const url = `${getFileUrl("attachments", attachmentId)}?${Date.now()}`; // don't use cache
download(url);
}
@@ -55,12 +55,12 @@ async function openCustom(type: string, entityId: string, mime: string) {
const resp = await server.post<TmpResponse>(`${type}/${entityId}/save-to-tmp-dir`);
let filePath = resp.tmpFilePath;
const exec = utils.dynamicRequire('child_process').exec as ExecFunction;
const exec = utils.dynamicRequire("child_process").exec as ExecFunction;
const platform = process.platform;
if (platform === 'linux') {
if (platform === "linux") {
// we don't know which terminal is available, try in succession
const terminals = ['x-terminal-emulator', 'gnome-terminal', 'konsole', 'xterm', 'xfce4-terminal', 'mate-terminal', 'rxvt', 'terminator', 'terminology'];
const terminals = ["x-terminal-emulator", "gnome-terminal", "konsole", "xterm", "xfce4-terminal", "mate-terminal", "rxvt", "terminator", "terminology"];
const openFileWithTerminal = (terminal: string) => {
const command = `${terminal} -e 'mimeopen -d "${filePath}"'`;
console.log(`Open Note custom: ${command} `);
@@ -77,9 +77,9 @@ async function openCustom(type: string, entityId: string, mime: string) {
const searchTerminal = (index: number) => {
const terminal = terminals[index];
if (!terminal) {
console.error('Open Note custom: No terminal found!');
console.error("Open Note custom: No terminal found!");
// TODO: Remove {url: true} if not needed.
(open as any)(getFileUrl(type, entityId), {url: true});
(open as any)(getFileUrl(type, entityId), { url: true });
return;
}
exec(`which ${terminal}`, (error, stdout, stderr) => {
@@ -91,7 +91,7 @@ async function openCustom(type: string, entityId: string, mime: string) {
});
};
searchTerminal(0);
} else if (platform === 'win32') {
} else if (platform === "win32") {
if (filePath.indexOf("/") !== -1) {
// Note that the path separator must be \ instead of /
filePath = filePath.replace(/\//g, "\\");
@@ -102,7 +102,7 @@ async function openCustom(type: string, entityId: string, mime: string) {
console.error("Open Note custom: ", err);
// TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type.
// Also don't know why {url: true} is passed.
(open as any)(getFileUrl(entityId), {url: true});
(open as any)(getFileUrl(entityId), { url: true });
return;
}
});
@@ -110,15 +110,12 @@ async function openCustom(type: string, entityId: string, mime: string) {
console.log('Currently "Open Note custom" only supports linux and windows systems');
// TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type.
// Also don't know why {url: true} is passed.
(open as any)(getFileUrl(entityId), {url: true});
(open as any)(getFileUrl(entityId), { url: true });
}
}
const openNoteCustom =
async (noteId: string, mime: string) => await openCustom('notes', noteId, mime);
const openAttachmentCustom =
async (attachmentId: string, mime: string) => await openCustom('attachments', attachmentId, mime);
const openNoteCustom = async (noteId: string, mime: string) => await openCustom("notes", noteId, mime);
const openAttachmentCustom = async (attachmentId: string, mime: string) => await openCustom("attachments", attachmentId, mime);
function downloadRevision(noteId: string, revisionId: string) {
const url = getUrlForDownload(`api/revisions/${revisionId}/download`);
@@ -133,18 +130,14 @@ function getUrlForDownload(url: string) {
if (utils.isElectron()) {
// electron needs absolute URL, so we extract current host, port, protocol
return `${getHost()}/${url}`;
}
else {
} else {
// web server can be deployed on subdomain, so we need to use a relative path
return url;
}
}
function canOpenInBrowser(mime: string) {
return mime === "application/pdf"
|| mime.startsWith("image")
|| mime.startsWith("audio")
|| mime.startsWith("video");
return mime === "application/pdf" || mime.startsWith("image") || mime.startsWith("audio") || mime.startsWith("video");
}
async function openExternally(type: string, entityId: string, mime: string) {
@@ -153,15 +146,14 @@ async function openExternally(type: string, entityId: string, mime: string) {
if (utils.isElectron()) {
const resp = await server.post<TmpResponse>(`${type}/${entityId}/save-to-tmp-dir`);
const electron = utils.dynamicRequire('electron');
const electron = utils.dynamicRequire("electron");
const res = await electron.shell.openPath(resp.tmpFilePath);
if (res) {
// fallback in case there's no default application for this file
window.open(getFileUrl(type, entityId));
}
}
else {
} else {
// allow browser to handle opening common file
if (canOpenInBrowser(mime)) {
window.open(getOpenFileUrl(type, entityId));
@@ -171,10 +163,8 @@ async function openExternally(type: string, entityId: string, mime: string) {
}
}
const openNoteExternally =
async (noteId: string, mime: string) => await openExternally('notes', noteId, mime);
const openAttachmentExternally =
async (attachmentId: string, mime: string) => await openExternally('attachments', attachmentId, mime);
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
function getHost() {
const url = new URL(window.location.href);
@@ -184,17 +174,17 @@ function getHost() {
async function openDirectory(directory: string) {
try {
if (utils.isElectron()) {
const electron = utils.dynamicRequire('electron');
const electron = utils.dynamicRequire("electron");
const res = await electron.shell.openPath(directory);
if (res) {
console.error('Failed to open directory:', res);
console.error("Failed to open directory:", res);
}
} else {
console.error('Not running in an Electron environment.');
console.error("Not running in an Electron environment.");
}
} catch (err: any) {
// Handle file system errors (e.g. path does not exist or is inaccessible)
console.error('Error:', err.message);
console.error("Error:", err.message);
}
}
@@ -209,4 +199,4 @@ export default {
openNoteCustom,
openAttachmentCustom,
openDirectory
}
};

View File

@@ -1,4 +1,3 @@
import server from "./server.js";
type OptionValue = number | string;
@@ -8,7 +7,7 @@ class Options {
private arr!: Record<string, OptionValue>;
constructor() {
this.initializedPromise = server.get<Record<string, OptionValue>>('options').then(data => this.load(data));
this.initializedPromise = server.get<Record<string, OptionValue>>("options").then((data) => this.load(data));
}
load(arr: Record<string, OptionValue>) {
@@ -30,8 +29,7 @@ class Options {
}
try {
return JSON.parse(value);
}
catch (e) {
} catch (e) {
return null;
}
}
@@ -53,7 +51,7 @@ class Options {
}
is(key: string) {
return this.arr[key] === 'true';
return this.arr[key] === "true";
}
set(key: string, value: OptionValue) {

View File

@@ -11,35 +11,29 @@ interface DefinitionObject {
}
function parse(value: string) {
const tokens = value.split(',').map(t => t.trim());
const tokens = value.split(",").map((t) => t.trim());
const defObj: DefinitionObject = {};
for (const token of tokens) {
if (token === 'promoted') {
if (token === "promoted") {
defObj.isPromoted = true;
}
else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
defObj.labelType = token as LabelType;
}
else if (['single', 'multi'].includes(token)) {
} else if (["single", "multi"].includes(token)) {
defObj.multiplicity = token as Multiplicity;
}
else if (token.startsWith('precision')) {
const chunks = token.split('=');
} else if (token.startsWith("precision")) {
const chunks = token.split("=");
defObj.numberPrecision = parseInt(chunks[1]);
}
else if (token.startsWith('alias')) {
const chunks = token.split('=');
} else if (token.startsWith("alias")) {
const chunks = token.split("=");
defObj.promotedAlias = chunks[1];
}
else if (token.startsWith('inverse')) {
const chunks = token.split('=');
} else if (token.startsWith("inverse")) {
const chunks = token.split("=");
defObj.inverseRelation = chunks[1];
}
else {
} else {
console.log("Unrecognized attribute definition token:", token);
}
}

View File

@@ -1,5 +1,5 @@
import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js';
import server from "./server.js";
import protectedSessionHolder from "./protected_session_holder.js";
import toastService from "./toast.js";
import type { ToastOptions } from "./toast.js";
import ws from "./ws.js";
@@ -7,7 +7,7 @@ import appContext from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import { t } from './i18n.js';
import { t } from "./i18n.js";
let protectedSessionDeferred: JQuery.Deferred<any, any, any> | null = null;
@@ -19,8 +19,8 @@ interface Response {
interface Message {
taskId: string;
data: {
protect: boolean
}
protect: boolean;
};
}
async function leaveProtectedSession() {
@@ -40,8 +40,7 @@ function enterProtectedSession() {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
dfd.resolve(false);
}
else {
} else {
// using deferred instead of promise because it allows resolving from the outside
protectedSessionDeferred = dfd;
@@ -61,7 +60,7 @@ async function reloadData() {
}
async function setupProtectedSession(password: string) {
const response = await server.post<Response>('login/protected', { password: password });
const response = await server.post<Response>("login/protected", { password: password });
if (!response.success) {
toastService.showError(t("protected_session.wrong_password"), 3000);
@@ -71,13 +70,13 @@ async function setupProtectedSession(password: string) {
protectedSessionHolder.enableProtectedSession();
}
ws.subscribeToMessages(async message => {
if (message.type === 'protectedSessionLogin') {
ws.subscribeToMessages(async (message) => {
if (message.type === "protectedSessionLogin") {
await reloadData();
await appContext.triggerEvent('frocaReloaded');
await appContext.triggerEvent("frocaReloaded");
appContext.triggerEvent('protectedSessionStarted');
appContext.triggerEvent("protectedSessionStarted");
appContext.triggerCommand("closeProtectedSessionPasswordDialog");
@@ -87,8 +86,7 @@ ws.subscribeToMessages(async message => {
}
toastService.showMessage(t("protected_session.started"));
}
else if (message.type === 'protectedSessionLogout') {
} else if (message.type === "protectedSessionLogout") {
utils.reloadFrontendApp(`Protected session logout`);
}
});
@@ -108,23 +106,23 @@ function makeToast(message: Message, title: string, text: string): ToastOptions
};
}
ws.subscribeToMessages(async message => {
if (message.taskType !== 'protectNotes') {
ws.subscribeToMessages(async (message) => {
if (message.taskType !== "protectNotes") {
return;
}
const isProtecting = message.data.protect;
const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title");
if (message.type === 'taskError') {
if (message.type === "taskError") {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
} else if (message.type === "taskProgressCount") {
const count = message.progressCount;
const text = ( isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count }));
const text = isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count });
toastService.showPersistent(makeToast(message, title, text));
} else if (message.type === 'taskSucceeded') {
const text = (isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully"))
} else if (message.type === "taskSucceeded") {
const text = isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully");
const toast = makeToast(message, title, text);
toast.closeAfter = 3000;

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