mirror of
https://github.com/zadam/trilium.git
synced 2025-11-12 16:25:51 +01:00
Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
42
src/app.ts
42
src/app.ts
@@ -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;
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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'.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
6
src/express.d.ts
vendored
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}'`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,4 +21,4 @@ export interface Froca {
|
||||
getBranches(branchIds: string[], silentNotFoundError?: boolean): FBranch[];
|
||||
|
||||
getAttachmentsForNote(noteId: string): Promise<FAttachment[]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
216
src/public/app/services/mime_type_definitions.ts
Normal file
216
src/public/app/services/mime_type_definitions.ts
Normal 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];
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import FAttribute from "../entities/fattribute.js";
|
||||
*/
|
||||
class NoteAttributeCache {
|
||||
attributes: Record<string, FAttribute[]>;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.attributes = {};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}),
|
||||
" "
|
||||
);
|
||||
}
|
||||
else if (lastPrinted) {
|
||||
} else if (lastPrinted) {
|
||||
$pager.append("... ");
|
||||
|
||||
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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user