Merge remote-tracking branch 'origin/next60' into next60

This commit is contained in:
zadam 2023-04-11 17:02:53 +02:00
commit a5f0b2a81e
41 changed files with 1991 additions and 299 deletions

16
.eslintrc.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es2021": true,
"node": true
},
"extends": "eslint:recommended",
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
}
}

View File

@ -1,7 +1,7 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />

6
.idea/jsLinters/eslint.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

1944
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@
"@excalidraw/excalidraw": "0.14.2", "@excalidraw/excalidraw": "0.14.2",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"axios": "1.3.4", "axios": "1.3.5",
"better-sqlite3": "7.4.5", "better-sqlite3": "7.4.5",
"chokidar": "3.5.3", "chokidar": "3.5.3",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
@ -55,28 +55,28 @@
"express-rate-limit": "6.7.0", "express-rate-limit": "6.7.0",
"express-session": "1.17.3", "express-session": "1.17.3",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"helmet": "6.0.1", "helmet": "6.1.2",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
"http-proxy-agent": "5.0.0", "http-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
"image-type": "4.1.0", "image-type": "4.1.0",
"ini": "4.0.0", "ini": "3.0.1",
"is-animated": "2.0.2", "is-animated": "2.0.2",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"jimp": "0.22.7", "jimp": "0.22.7",
"joplin-turndown-plugin-gfm": "1.0.12", "joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "21.1.0", "jsdom": "21.1.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-abi": "3.33.0", "node-abi": "3.35.0",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"open": "8.4.1", "open": "8.4.1",
"rand-token": "1.0.1", "rand-token": "1.0.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"request": "2.88.2", "request": "2.88.2",
"rimraf": "3.0.2", "rimraf": "5.0.0",
"safe-compare": "1.1.4", "safe-compare": "1.1.4",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"sanitize-html": "2.10.0", "sanitize-html": "2.10.0",
@ -90,7 +90,7 @@
"turndown": "7.1.2", "turndown": "7.1.2",
"unescape": "1.0.1", "unescape": "1.0.1",
"ws": "8.13.0", "ws": "8.13.0",
"xml2js": "0.4.23", "xml2js": "0.5.0",
"yauzl": "2.10.0" "yauzl": "2.10.0"
}, },
"devDependencies": { "devDependencies": {
@ -99,12 +99,13 @@
"electron-builder": "23.6.0", "electron-builder": "23.6.0",
"electron-packager": "17.1.1", "electron-packager": "17.1.1",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",
"eslint": "^8.38.0",
"esm": "3.2.25", "esm": "3.2.25",
"jasmine": "4.6.0", "jasmine": "4.6.0",
"jsdoc": "4.0.2", "jsdoc": "4.0.2",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"rcedit": "3.0.1", "rcedit": "3.0.1",
"webpack": "5.76.3", "webpack": "5.78.0",
"webpack-cli": "5.0.1" "webpack-cli": "5.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -5,12 +5,13 @@ const favicon = require('serve-favicon');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const helmet = require('helmet'); const helmet = require('helmet');
const session = require('express-session'); const session = require('express-session');
const compression = require('compression') const compression = require('compression');
const FileStore = require('session-file-store')(session); const FileStore = require('session-file-store')(session);
const sessionSecret = require('./services/session_secret'); const sessionSecret = require('./services/session_secret');
const dataDir = require('./services/data_dir'); const dataDir = require('./services/data_dir');
const utils = require('./services/utils'); const utils = require('./services/utils');
const assetPath = require('./services/asset_path'); const assetPath = require('./services/asset_path');
const env = require('./services/env');
require('./services/handlers'); require('./services/handlers');
require('./becca/becca_loader'); require('./becca/becca_loader');
@ -30,27 +31,37 @@ app.use(helmet({
crossOriginEmbedderPolicy: false crossOriginEmbedderPolicy: false
})); }));
const persistentCacheStatic = (root, options) => {
if (!env.isDev()) {
options = {
maxAge: '1y',
...options
};
}
return express.static(root, options);
};
app.use(express.text({limit: '500mb'})); app.use(express.text({limit: '500mb'}));
app.use(express.json({limit: '500mb'})); app.use(express.json({limit: '500mb'}));
app.use(express.raw({limit: '500mb'})); app.use(express.raw({limit: '500mb'}));
app.use(express.urlencoded({extended: false})); app.use(express.urlencoded({extended: false}));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public/root'))); app.use(express.static(path.join(__dirname, 'public/root')));
app.use(`/${assetPath}/app`, express.static(path.join(__dirname, 'public/app'))); app.use(`/${assetPath}/app`, persistentCacheStatic(path.join(__dirname, 'public/app')));
app.use(`/${assetPath}/app-dist`, express.static(path.join(__dirname, 'public/app-dist'))); app.use(`/${assetPath}/app-dist`, persistentCacheStatic(path.join(__dirname, 'public/app-dist')));
app.use(`/${assetPath}/fonts`, express.static(path.join(__dirname, 'public/fonts'))); app.use(`/${assetPath}/fonts`, persistentCacheStatic(path.join(__dirname, 'public/fonts')));
app.use(`/assets/vX/fonts`, express.static(path.join(__dirname, 'public/fonts'))); app.use(`/assets/vX/fonts`, express.static(path.join(__dirname, 'public/fonts')));
app.use(`/${assetPath}/stylesheets`, express.static(path.join(__dirname, 'public/stylesheets'))); app.use(`/${assetPath}/stylesheets`, persistentCacheStatic(path.join(__dirname, 'public/stylesheets')));
app.use(`/assets/vX/stylesheets`, express.static(path.join(__dirname, 'public/stylesheets'))); app.use(`/assets/vX/stylesheets`, express.static(path.join(__dirname, 'public/stylesheets')));
app.use(`/${assetPath}/libraries`, express.static(path.join(__dirname, '..', 'libraries'))); app.use(`/${assetPath}/libraries`, persistentCacheStatic(path.join(__dirname, '..', 'libraries')));
app.use(`/assets/vX/libraries`, express.static(path.join(__dirname, '..', 'libraries'))); app.use(`/assets/vX/libraries`, express.static(path.join(__dirname, '..', 'libraries')));
// excalidraw-view mode in shared notes // excalidraw-view mode in shared notes
app.use(`/${assetPath}/node_modules/react/umd/react.production.min.js`, express.static(path.join(__dirname, '..', 'node_modules/react/umd/react.production.min.js'))); app.use(`/${assetPath}/node_modules/react/umd/react.production.min.js`, persistentCacheStatic(path.join(__dirname, '..', 'node_modules/react/umd/react.production.min.js')));
app.use(`/${assetPath}/node_modules/react-dom/umd/react-dom.production.min.js`, express.static(path.join(__dirname, '..', 'node_modules/react-dom/umd/react-dom.production.min.js'))); app.use(`/${assetPath}/node_modules/react-dom/umd/react-dom.production.min.js`, persistentCacheStatic(path.join(__dirname, '..', 'node_modules/react-dom/umd/react-dom.production.min.js')));
// expose whole dist folder since complete assets are needed in edit and share // expose whole dist folder since complete assets are needed in edit and share
app.use(`/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/'))); app.use(`/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/')));
app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/'))); app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/`, persistentCacheStatic(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/')));
app.use(`/${assetPath}/images`, express.static(path.join(__dirname, '..', 'images'))); app.use(`/${assetPath}/images`, persistentCacheStatic(path.join(__dirname, '..', 'images')));
app.use(`/assets/vX/images`, express.static(path.join(__dirname, '..', 'images'))); app.use(`/assets/vX/images`, express.static(path.join(__dirname, '..', 'images')));
app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest'))); app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest')));
app.use(`/robots.txt`, express.static(path.join(__dirname, 'public/robots.txt'))); app.use(`/robots.txt`, express.static(path.join(__dirname, 'public/robots.txt')));
@ -61,7 +72,7 @@ const sessionParser = session({
cookie: { cookie: {
// path: "/", // path: "/",
httpOnly: true, httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // in milliseconds maxAge: 24 * 60 * 60 * 1000 // in milliseconds
}, },
name: 'trilium.sid', name: 'trilium.sid',
store: new FileStore({ store: new FileStore({

View File

@ -96,7 +96,7 @@ class BAttribute extends AbstractBeccaEntity {
} }
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 target not existing note '${this.value}'.`); throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
} }
} }

View File

@ -100,7 +100,7 @@ class BNote extends AbstractBeccaEntity {
* @private */ * @private */
this.parents = []; this.parents = [];
/** @type {BNote[]} /** @type {BNote[]}
* @private*/ * @private */
this.children = []; this.children = [];
/** @type {BAttribute[]} /** @type {BAttribute[]}
* @private */ * @private */
@ -110,11 +110,11 @@ class BNote extends AbstractBeccaEntity {
* @private */ * @private */
this.__attributeCache = null; this.__attributeCache = null;
/** @type {BAttribute[]|null} /** @type {BAttribute[]|null}
* @private*/ * @private */
this.inheritableAttributeCache = null; this.inheritableAttributeCache = null;
/** @type {BAttribute[]} /** @type {BAttribute[]}
* @private*/ * @private */
this.targetRelations = []; this.targetRelations = [];
this.becca.addNote(this.noteId, this); this.becca.addNote(this.noteId, this);
@ -472,6 +472,20 @@ class BNote extends AbstractBeccaEntity {
*/ */
hasLabel(name, value) { return this.hasAttribute(LABEL, name, value); } hasLabel(name, value) { return this.hasAttribute(LABEL, name, value); }
/**
* @param {string} name - label name
* @returns {boolean} true if label exists (including inherited) and does not have "false" value.
*/
isLabelTruthy(name) {
const label = this.getLabel(name);
if (!label) {
return false;
}
return label && label.value !== 'false';
}
/** /**
* @param {string} name - label name * @param {string} name - label name
* @param {string} [value] - label value * @param {string} [value] - label value

View File

@ -2,7 +2,7 @@ const becca = require('./becca');
const log = require('../services/log'); const log = require('../services/log');
const beccaService = require('./becca_service'); const beccaService = require('./becca_service');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
const { JSDOM } = require("jsdom"); const {JSDOM} = require("jsdom");
const DEBUG = false; const DEBUG = false;
@ -168,7 +168,6 @@ function trimMime(mime) {
} }
mimeCache[mime] = str; mimeCache[mime] = str;
mimeCache[mime] = str;
} }
return mimeCache[mime]; return mimeCache[mime];
@ -224,8 +223,8 @@ function splitToWords(text) {
*/ */
function hasConnectingRelation(sourceNote, targetNote) { function hasConnectingRelation(sourceNote, targetNote) {
return sourceNote.getAttributes().find(attr => attr.type === 'relation' return sourceNote.getAttributes().find(attr => attr.type === 'relation'
&& ['includenotelink', 'imagelink'].includes(attr.name) && ['includenotelink', 'imagelink'].includes(attr.name)
&& attr.value === targetNote.noteId); && attr.value === targetNote.noteId);
} }
async function findSimilarNotes(noteId) { async function findSimilarNotes(noteId) {
@ -301,7 +300,7 @@ async function findSimilarNotes(noteId) {
for (const branch of parentNote.getParentBranches()) { for (const branch of parentNote.getParentBranches()) {
score += gatherRewards(branch.prefix, 0.3) score += gatherRewards(branch.prefix, 0.3)
+ gatherAncestorRewards(branch.parentNote); + gatherAncestorRewards(branch.parentNote);
} }
} }
} }
@ -314,7 +313,7 @@ async function findSimilarNotes(noteId) {
function computeScore(candidateNote) { function computeScore(candidateNote) {
let score = gatherRewards(trimMime(candidateNote.mime)) let score = gatherRewards(trimMime(candidateNote.mime))
+ gatherAncestorRewards(candidateNote); + gatherAncestorRewards(candidateNote);
if (candidateNote.isDecrypted) { if (candidateNote.isDecrypted) {
score += gatherRewards(candidateNote.title); score += gatherRewards(candidateNote.title);
@ -382,7 +381,7 @@ async function findSimilarNotes(noteId) {
score += 1; score += 1;
} }
else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10) else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10)
|| utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) { || utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) {
if (displayRewards) { if (displayRewards) {
console.log("Adding reward for same day of creation"); console.log("Adding reward for same day of creation");
} }

View File

@ -20,7 +20,7 @@ function register(router) {
const {date} = req.params; const {date} = req.params;
if (!isValidDate(date)) { if (!isValidDate(date)) {
throw getDateInvalidError(res, date); throw getDateInvalidError(date);
} }
const note = specialNotesService.getInboxNote(date); const note = specialNotesService.getInboxNote(date);
@ -31,7 +31,7 @@ function register(router) {
const {date} = req.params; const {date} = req.params;
if (!isValidDate(date)) { if (!isValidDate(date)) {
throw getDateInvalidError(res, date); throw getDateInvalidError(date);
} }
const note = dateNotesService.getDayNote(date); const note = dateNotesService.getDayNote(date);
@ -42,7 +42,7 @@ function register(router) {
const {date} = req.params; const {date} = req.params;
if (!isValidDate(date)) { if (!isValidDate(date)) {
throw getDateInvalidError(res, date); throw getDateInvalidError(date);
} }
const note = dateNotesService.getWeekNote(date); const note = dateNotesService.getWeekNote(date);
@ -53,7 +53,7 @@ function register(router) {
const {month} = req.params; const {month} = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) { if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
throw getMonthInvalidError(res, month); throw getMonthInvalidError(month);
} }
const note = dateNotesService.getMonthNote(month); const note = dateNotesService.getMonthNote(month);
@ -64,7 +64,7 @@ function register(router) {
const {year} = req.params; const {year} = req.params;
if (!/[0-9]{4}/.test(year)) { if (!/[0-9]{4}/.test(year)) {
throw getYearInvalidError(res, year); throw getYearInvalidError(year);
} }
const note = dateNotesService.getYearNote(year); const note = dateNotesService.getYearNote(year);

View File

@ -608,6 +608,20 @@ class FNote {
*/ */
hasLabel(name) { return this.hasAttribute(LABEL, name); } hasLabel(name) { return this.hasAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {boolean} true if label exists (including inherited) and does not have "false" value.
*/
isLabelTruthy(name) {
const label = this.getLabel(name);
if (!label) {
return false;
}
return label && label.value !== 'false';
}
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {boolean} true if relation exists (excluding inherited) * @returns {boolean} true if relation exists (excluding inherited)

View File

@ -128,6 +128,10 @@ export default class DesktopLayout {
) )
.child( .child(
new RibbonContainer() new RibbonContainer()
// 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 PromotedAttributesWidget())
.ribbon(new ScriptExecutorWidget()) .ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget()) .ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget()) .ribbon(new EditedNotesWidget())
@ -135,7 +139,6 @@ export default class DesktopLayout {
.ribbon(new NotePropertiesWidget()) .ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget()) .ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget()) .ribbon(new ImagePropertiesWidget())
.ribbon(new PromotedAttributesWidget())
.ribbon(new BasicPropertiesWidget()) .ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget()) .ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget()) .ribbon(new InheritedAttributesWidget())

View File

@ -147,7 +147,9 @@ class ContextMenu {
// "contextmenu" event also triggers "click" event which depending on the timing can close just opened context menu // "contextmenu" event also triggers "click" event which depending on the timing can close just opened context menu
// we might filter out right clicks, but then it's better if even right clicks close the context menu // we might filter out right clicks, but then it's better if even right clicks close the context menu
if (Date.now() - this.dateContextMenuOpenedMs > 300) { if (Date.now() - this.dateContextMenuOpenedMs > 300) {
this.$widget.hide(); // seems like if we hide the menu immediately, some clicks can get propagated to the underlying component
// see https://github.com/zadam/trilium/pull/3805 for details
setTimeout(() => this.$widget.hide(), 100);
} }
} }
} }

View File

@ -10,7 +10,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove); branchIdsToMove = filterRootNote(branchIdsToMove);
branchIdsToMove = filterSearchBranches(branchIdsToMove); branchIdsToMove = filterSearchBranches(branchIdsToMove);
const beforeBranch = await froca.getBranch(beforeBranchId); const beforeBranch = froca.getBranch(beforeBranchId);
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) { if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
toastService.showError('Cannot move notes here.'); toastService.showError('Cannot move notes here.');
@ -31,7 +31,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove); branchIdsToMove = filterRootNote(branchIdsToMove);
branchIdsToMove = filterSearchBranches(branchIdsToMove); branchIdsToMove = filterSearchBranches(branchIdsToMove);
const afterNote = await froca.getBranch(afterBranchId).getNote(); const afterNote = froca.getBranch(afterBranchId).getNote();
const forbiddenNoteIds = [ const forbiddenNoteIds = [
'root', 'root',
@ -59,7 +59,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
} }
async function moveToParentNote(branchIdsToMove, newParentBranchId) { async function moveToParentNote(branchIdsToMove, newParentBranchId) {
const newParentBranch = await froca.getBranch(newParentBranchId); const newParentBranch = froca.getBranch(newParentBranchId);
if (newParentBranch.noteId === '_lbRoot') { if (newParentBranch.noteId === '_lbRoot') {
toastService.showError('Cannot move notes here.'); toastService.showError('Cannot move notes here.');
@ -165,7 +165,7 @@ function filterRootNote(branchIds) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
return branchIds.filter(branchId => { return branchIds.filter(branchId => {
const branch = froca.getBranch(branchId); const branch = froca.getBranch(branchId);
return branch.noteId !== 'root' return branch.noteId !== 'root'
&& branch.noteId !== hoistedNoteId; && branch.noteId !== hoistedNoteId;

View File

@ -14,7 +14,7 @@ async function processEntityChanges(entityChanges) {
if (ec.entityName === 'notes') { if (ec.entityName === 'notes') {
processNoteChange(loadResults, ec); processNoteChange(loadResults, ec);
} else if (ec.entityName === 'branches') { } else if (ec.entityName === 'branches') {
processBranchChange(loadResults, ec); await processBranchChange(loadResults, ec);
} else if (ec.entityName === 'attributes') { } else if (ec.entityName === 'attributes') {
processAttributeChange(loadResults, ec); processAttributeChange(loadResults, ec);
} else if (ec.entityName === 'note_reordering') { } else if (ec.entityName === 'note_reordering') {
@ -104,7 +104,7 @@ function processNoteChange(loadResults, ec) {
} }
} }
function processBranchChange(loadResults, ec) { async function processBranchChange(loadResults, ec) {
if (ec.isErased && ec.entityId in froca.branches) { if (ec.isErased && ec.entityId in froca.branches) {
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
return; return;
@ -138,7 +138,15 @@ function processBranchChange(loadResults, ec) {
loadResults.addBranch(ec.entityId, ec.componentId); loadResults.addBranch(ec.entityId, ec.componentId);
const childNote = froca.notes[ec.entity.noteId]; const childNote = froca.notes[ec.entity.noteId];
const parentNote = froca.notes[ec.entity.parentNoteId]; let parentNote = froca.notes[ec.entity.parentNoteId];
if (childNote && !parentNote) {
// a branch cannot exist without the parent
// a note loaded into froca has to also contain all its ancestors
// this problem happened e.g. in sharing where _share was hidden and thus not loaded
// sharing meant cloning into _share, which crashed because _share was not loaded
parentNote = await froca.getNote(ec.entity.parentNoteId);
}
if (branch) { if (branch) {
branch.update(ec.entity); branch.update(ec.entity);

View File

@ -65,7 +65,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext().setNote(notePath);
appContext.triggerEvent('focusAndSelectTitle'); await appContext.triggerEvent('focusAndSelectTitle');
}; };
/** /**
@ -82,7 +82,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
await appContext.tabManager.openContextWithNote(notePath, { activate }); await appContext.tabManager.openContextWithNote(notePath, { activate });
if (activate) { if (activate) {
appContext.triggerEvent('focusAndSelectTitle'); await appContext.triggerEvent('focusAndSelectTitle');
} }
}; };
@ -100,10 +100,10 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1]; const {ntxId} = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath}); await appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
if (activate) { if (activate) {
appContext.triggerEvent('focusAndSelectTitle'); await appContext.triggerEvent('focusAndSelectTitle');
} }
}; };

View File

@ -10,7 +10,6 @@ export async function uploadFiles(parentNoteId, files, options) {
} }
const taskId = utils.randomString(10); const taskId = utils.randomString(10);
let noteId;
let counter = 0; let counter = 0;
for (const file of files) { for (const file of files) {
@ -25,19 +24,19 @@ export async function uploadFiles(parentNoteId, files, options) {
formData.append(key, options[key]); formData.append(key, options[key]);
} }
({noteId} = await $.ajax({ await $.ajax({
url: `${baseApiUrl}notes/${parentNoteId}/import`, url: `${baseApiUrl}notes/${parentNoteId}/import`,
headers: await server.getHeaders(), headers: await server.getHeaders(),
data: formData, data: formData,
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
timeout: 60 * 60 * 1000, timeout: 60 * 60 * 1000,
error: function(xhr) { error: function (xhr) {
toastService.showError(`Import failed: ${xhr.responseText}`); toastService.showError(`Import failed: ${xhr.responseText}`);
}, },
contentType: false, // NEEDED, DON'T REMOVE THIS contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false, // NEEDED, DON'T REMOVE THIS processData: false, // NEEDED, DON'T REMOVE THIS
})); });
} }
} }
@ -74,4 +73,4 @@ ws.subscribeToMessages(async message => {
export default { export default {
uploadFiles uploadFiles
} };

View File

@ -29,7 +29,7 @@ function formatTimeWithSeconds(date) {
// this is producing local time! // this is producing local time!
function formatDate(date) { function formatDate(date) {
// return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear(); // return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
// instead of european format we'll just use ISO as that's pretty unambiguous // instead of european format we'll just use ISO as that's pretty unambiguous
return formatDateISO(date); return formatDateISO(date);
@ -45,7 +45,7 @@ function formatDateTime(date) {
} }
function localNowDateTime() { function localNowDateTime() {
return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ') return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ');
} }
function now() { function now() {
@ -101,7 +101,7 @@ async function stopWatch(what, func) {
} }
function formatValueWithWhitespace(val) { function formatValueWithWhitespace(val) {
return /[^\w_-]/.test(val) ? `"${val}"` : val; return /[^\w-]/.test(val) ? `"${val}"` : val;
} }
function formatLabel(label) { function formatLabel(label) {
@ -329,7 +329,7 @@ function initHelpDropdown($el) {
initHelpButtons($dropdownMenu); initHelpButtons($dropdownMenu);
} }
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/" const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
function openHelp(e) { function openHelp(e) {
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank'); window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
@ -340,7 +340,7 @@ function initHelpButtons($el) {
// so we do it manually // so we do it manually
$el.on("click", e => { $el.on("click", e => {
if ($(e.target).attr("data-help-page")) { if ($(e.target).attr("data-help-page")) {
openHelp(e) openHelp(e);
} }
}); });
} }

View File

@ -285,7 +285,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$title = this.$widget.find('.attr-detail-title'); this.$title = this.$widget.find('.attr-detail-title');
this.$inputName = this.$widget.find('.attr-input-name'); this.$inputName = this.$widget.find('.attr-input-name');
this.$inputName.on('keyup', () => this.userEditedAttribute()); this.$inputName.on('input', ev => {
if (!ev.originalEvent?.isComposing) { // https://github.com/zadam/trilium/pull/3812
this.userEditedAttribute();
}
});
this.$inputName.on('change', () => this.userEditedAttribute()); this.$inputName.on('change', () => this.userEditedAttribute());
this.$inputName.on('autocomplete:closed', () => this.userEditedAttribute()); this.$inputName.on('autocomplete:closed', () => this.userEditedAttribute());
@ -299,7 +303,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$rowValue = this.$widget.find('.attr-row-value'); this.$rowValue = this.$widget.find('.attr-row-value');
this.$inputValue = this.$widget.find('.attr-input-value'); this.$inputValue = this.$widget.find('.attr-input-value');
this.$inputValue.on('keyup', () => this.userEditedAttribute()); this.$inputValue.on('input', ev => {
if (!ev.originalEvent?.isComposing) { // https://github.com/zadam/trilium/pull/3812
this.userEditedAttribute();
}
});
this.$inputValue.on('change', () => this.userEditedAttribute()); this.$inputValue.on('change', () => this.userEditedAttribute());
this.$inputValue.on('autocomplete:closed', () => this.userEditedAttribute()); this.$inputValue.on('autocomplete:closed', () => this.userEditedAttribute());
this.$inputValue.on('focus', () => { this.$inputValue.on('focus', () => {
@ -328,7 +336,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$rowInverseRelation = this.$widget.find('.attr-row-inverse-relation'); this.$rowInverseRelation = this.$widget.find('.attr-row-inverse-relation');
this.$inputInverseRelation = this.$widget.find('.attr-input-inverse-relation'); this.$inputInverseRelation = this.$widget.find('.attr-input-inverse-relation');
this.$inputInverseRelation.on('keyup', () => this.userEditedAttribute()); this.$inputInverseRelation.on('input', ev => {
if (!ev.originalEvent?.isComposing) { // https://github.com/zadam/trilium/pull/3812
this.userEditedAttribute();
}
});
this.$rowTargetNote = this.$widget.find('.attr-row-target-note'); this.$rowTargetNote = this.$widget.find('.attr-row-target-note');
this.$inputTargetNote = this.$widget.find('.attr-input-target-note'); this.$inputTargetNote = this.$widget.find('.attr-input-target-note');

View File

@ -65,6 +65,26 @@ const TPL = `<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" ro
sort folders at the top sort folders at the top
</label> </label>
</div> </div>
<br />
<h5>Natural Sort</h5>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="sort-natural" value="1">
sort with respect to different character sorting and collation rules in different languages or regions.
</label>
</div>
<br />
<div class="form-check">
<label>
Natural sort language
<input class="form-control" name="sort-locale">
The language code for natural sort, e.g. "zh-CN" for Chinese.
</label>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button> <button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button>
@ -83,8 +103,10 @@ export default class SortChildNotesDialog extends BasicWidget {
const sortBy = this.$form.find("input[name='sort-by']:checked").val(); const sortBy = this.$form.find("input[name='sort-by']:checked").val();
const sortDirection = this.$form.find("input[name='sort-direction']:checked").val(); const sortDirection = this.$form.find("input[name='sort-direction']:checked").val();
const foldersFirst = this.$form.find("input[name='sort-folders-first']").is(":checked"); const foldersFirst = this.$form.find("input[name='sort-folders-first']").is(":checked");
const sortNatural = this.$form.find("input[name='sort-natural']").is(":checked");
const sortLocale = this.$form.find("input[name='sort-locale']").val();
await server.put(`notes/${this.parentNoteId}/sort-children`, {sortBy, sortDirection, foldersFirst}); await server.put(`notes/${this.parentNoteId}/sort-children`, {sortBy, sortDirection, foldersFirst, sortNatural, sortLocale});
utils.closeActiveDialog(); utils.closeActiveDialog();
}); });

View File

@ -55,7 +55,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
this.$container = this.$widget.find(".note-map-container"); this.$container = this.$widget.find(".note-map-container");
this.$styleResolver = this.$widget.find('.style-resolver'); this.$styleResolver = this.$widget.find('.style-resolver');
window.addEventListener('resize', () => this.setDimensions(), false); new ResizeObserver(() => this.setDimensions()).observe(this.$container[0])
this.$widget.find(".map-type-switcher button").on("click", async e => { this.$widget.find(".map-type-switcher button").on("click", async e => {
const type = $(e.target).closest("button").attr("data-type"); const type = $(e.target).closest("button").attr("data-type");

View File

@ -87,8 +87,8 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
this.noteMapWidget.setDimensions(); this.noteMapWidget.setDimensions();
}); });
window.addEventListener('resize', () => { const handleResize = () => {
if (!this.graph) { // no graph has been even rendered if (!this.noteMapWidget.graph) { // no graph has been even rendered
return; return;
} }
@ -98,7 +98,9 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
else if (this.openState === 'small') { else if (this.openState === 'small') {
this.setSmallSize(); this.setSmallSize();
} }
}, false); }
new ResizeObserver(handleResize).observe(this.$widget[0])
} }
setSmallSize() { setSmallSize() {

View File

@ -29,6 +29,9 @@ const TPL = `
.promoted-attribute-cell div.input-group { .promoted-attribute-cell div.input-group {
margin-left: 10px; margin-left: 10px;
} }
.promoted-attribute-cell strong {
word-break:keep-all;
}
</style> </style>
<div class="promoted-attributes-container"></div> <div class="promoted-attributes-container"></div>
@ -54,13 +57,13 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
const promotedDefAttrs = note.getPromotedDefinitionAttributes(); const promotedDefAttrs = note.getPromotedDefinitionAttributes();
if (promotedDefAttrs.length === 0) { if (promotedDefAttrs.length === 0) {
return { show: false }; return {show: false};
} }
return { return {
show: true, show: true,
activate: true, activate: true,
title: "Promoted attributes", title: "Promoted Attributes",
icon: "bx bx-table" icon: "bx bx-table"
}; };
} }
@ -144,7 +147,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
return; return;
} }
attributeValues = attributeValues.map(attribute => ({ value: attribute })); attributeValues = attributeValues.map(attribute => ({value: attribute}));
$input.autocomplete({ $input.autocomplete({
appendTo: document.querySelector('body'), appendTo: document.querySelector('body'),
@ -164,7 +167,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
} }
}]); }]);
$input.on('autocomplete:selected', e => this.promotedAttributeChanged(e)) $input.on('autocomplete:selected', e => this.promotedAttributeChanged(e));
}); });
} }
else if (definition.labelType === 'number') { else if (definition.labelType === 'number') {

View File

@ -180,7 +180,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
return { return {
show: this.isEnabled(), show: this.isEnabled(),
activate: true, activate: true,
title: 'Search parameters', title: 'Search Parameters',
icon: 'bx bx-search' icon: 'bx bx-search'
}; };
} }

View File

@ -39,7 +39,14 @@ export default class SharedInfoWidget extends NoteContextAwareWidget {
this.$sharedText.text("This note is shared publicly on"); this.$sharedText.text("This note is shared publicly on");
} }
else { else {
link = `${location.protocol}//${location.host}${location.pathname}share/${shareId}`; let host = location.host;
if (host.endsWith('/')) {
// seems like IE has trailing slash
// https://github.com/zadam/trilium/issues/3782
host = host.substr(0, host.length - 1);
}
link = `${location.protocol}//${host}${location.pathname}share/${shareId}`;
this.$sharedText.text("This note is shared locally on"); this.$sharedText.text("This note is shared locally on");
} }

View File

@ -280,14 +280,7 @@ export default class TabRowWidget extends BasicWidget {
this.layoutTabs(); this.layoutTabs();
}; };
// ResizeObserver exists only in FF69 new ResizeObserver(resizeListener).observe(this.$widget[0]);
if (typeof ResizeObserver !== "undefined") {
new ResizeObserver(resizeListener).observe(this.$widget[0]);
}
else {
// for older firefox
window.addEventListener('resize', resizeListener);
}
this.tabEls.forEach((tabEl) => this.setTabCloseEvent(tabEl)); this.tabEls.forEach((tabEl) => this.setTabCloseEvent(tabEl));
} }

View File

@ -24,6 +24,7 @@ const FONT_FAMILIES = [
{ value: "Bradley Hand", label: "Bradley Hand" }, { value: "Bradley Hand", label: "Bradley Hand" },
{ value: "Luminari", label: "Luminari" }, { value: "Luminari", label: "Luminari" },
{ value: "Comic Sans MS", label: "Comic Sans MS" }, { value: "Comic Sans MS", label: "Comic Sans MS" },
{ value: "Microsoft YaHei", label: "Microsoft YaHei" },
]; ];
const TPL = ` const TPL = `

View File

@ -94,13 +94,13 @@ function undeleteNote(req) {
function sortChildNotes(req) { function sortChildNotes(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const {sortBy, sortDirection, foldersFirst} = req.body; const {sortBy, sortDirection, foldersFirst, sortNatural, sortLocale} = req.body;
log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}, foldersFirst=${foldersFirst}`); log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}, foldersFirst=${foldersFirst}, sortNatural=${sortNatural}, sortLocale=${sortLocale}`);
const reverse = sortDirection === 'desc'; const reverse = sortDirection === 'desc';
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst); treeService.sortNotes(noteId, sortBy, reverse, foldersFirst, sortNatural, sortLocale);
} }
function protectNote(req) { function protectNote(req) {

View File

@ -74,7 +74,7 @@ function getOptions() {
} }
} }
resultMap['isPasswordSet'] = !!optionMap['passwordVerificationHash'] ? 'true' : 'false'; resultMap['isPasswordSet'] = optionMap['passwordVerificationHash'] ? 'true' : 'false';
return resultMap; return resultMap;
} }

View File

@ -1,4 +1,4 @@
"use strict" "use strict";
function formatAttrForSearch(attr, searchWithValue) { function formatAttrForSearch(attr, searchWithValue) {
let searchStr = ''; let searchStr = '';
@ -28,7 +28,7 @@ function formatAttrForSearch(attr, searchWithValue) {
} }
function formatValue(val) { function formatValue(val) {
if (!/[^\w_]/.test(val)) { if (!/[^\w]/.test(val)) {
return val; return val;
} }
else if (!val.includes('"')) { else if (!val.includes('"')) {
@ -47,4 +47,4 @@ function formatValue(val) {
module.exports = { module.exports = {
formatAttrForSearch formatAttrForSearch
} };

View File

@ -7,7 +7,7 @@ const BAttribute = require('../becca/entities/battribute');
const {formatAttrForSearch} = require("./attribute_formatter"); const {formatAttrForSearch} = require("./attribute_formatter");
const BUILTIN_ATTRIBUTES = require("./builtin_attributes"); const BUILTIN_ATTRIBUTES = require("./builtin_attributes");
const ATTRIBUTE_TYPES = [ 'label', 'relation' ]; const ATTRIBUTE_TYPES = ['label', 'relation'];
/** @returns {BNote[]} */ /** @returns {BNote[]} */
function getNotesWithLabel(name, value = undefined) { function getNotesWithLabel(name, value = undefined) {
@ -122,7 +122,7 @@ function isAttributeType(type) {
function isAttributeDangerous(type, name) { function isAttributeDangerous(type, name) {
return BUILTIN_ATTRIBUTES.some(attr => return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type && attr.type === type &&
attr.name.toLowerCase() === name.trim().toLowerCase() && attr.name.toLowerCase() === name.trim().toLowerCase() &&
attr.isDangerous attr.isDangerous
); );

View File

@ -471,7 +471,7 @@ function BackendScriptApi(currentNote, apiParams) {
if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); } if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); }
if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); } if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); }
const parentNoteId = !!opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers'; const parentNoteId = opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers';
const noteId = 'al_' + opts.id; const noteId = 'al_' + opts.id;
const launcherNote = const launcherNote =

View File

@ -42,6 +42,8 @@ module.exports = [
{ type: 'label', name: 'sorted' }, { type: 'label', name: 'sorted' },
{ type: 'label', name: 'sortDirection' }, { type: 'label', name: 'sortDirection' },
{ type: 'label', name: 'sortFoldersFirst' }, { type: 'label', name: 'sortFoldersFirst' },
{ type: 'label', name: 'sortNatural' },
{ type: 'label', name: 'sortLocale' },
{ type: 'label', name: 'top' }, { type: 'label', name: 'top' },
{ type: 'label', name: 'fullContentWidth' }, { type: 'label', name: 'fullContentWidth' },
{ type: 'label', name: 'shareHiddenFromTree' }, { type: 'label', name: 'shareHiddenFromTree' },

View File

@ -46,7 +46,7 @@ eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETE
if (entityName === 'attributes') { if (entityName === 'attributes') {
runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity); runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity);
if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst'].includes(entity.name)) { if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale'].includes(entity.name)) {
handleSortedAttribute(entity); handleSortedAttribute(entity);
} else if (entity.type === 'label') { } else if (entity.type === 'label') {
handleMaybeSortingLabel(entity); handleMaybeSortingLabel(entity);
@ -101,7 +101,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId); noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
} }
} }
else if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst'].includes(entity.name)) { else if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale'].includes(entity.name)) {
handleSortedAttribute(entity); handleSortedAttribute(entity);
} }
else if (entity.type === 'label') { else if (entity.type === 'label') {

View File

@ -24,7 +24,7 @@ const noteTypesService = require("./note_types");
const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js"); const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js");
function getNewNotePosition(parentNote) { function getNewNotePosition(parentNote) {
if (parentNote.hasLabel('newNotesOnTop')) { if (parentNote.isLabelTruthy('newNotesOnTop')) {
const minNotePos = parentNote.getChildBranches() const minNotePos = parentNote.getChildBranches()
.reduce((min, note) => Math.min(min, note.notePosition), 0); .reduce((min, note) => Math.min(min, note.notePosition), 0);
@ -827,7 +827,7 @@ function duplicateSubtree(origNoteId, newParentNoteId) {
throw new Error('Duplicating root is not possible'); throw new Error('Duplicating root is not possible');
} }
log.info(`Duplicating ${origNoteId} subtree into ${newParentNoteId}`); log.info(`Duplicating '${origNoteId}' subtree into '${newParentNoteId}'`);
const origNote = becca.notes[origNoteId]; const origNote = becca.notes[origNoteId];
// might be null if orig note is not in the target newParentNoteId // might be null if orig note is not in the target newParentNoteId
@ -905,7 +905,8 @@ function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapp
attr.value = noteIdMapping[attr.value]; attr.value = noteIdMapping[attr.value];
} }
attr.save(); // the relation targets may not be created yet, the mapping is pre-generated
attr.save({skipValidation: true});
} }
for (const childBranch of origNote.getChildBranches()) { for (const childBranch of origNote.getChildBranches()) {

View File

@ -125,7 +125,7 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds =
} }
if (root) { if (root) {
scriptEnv = !!backendOverrideContent scriptEnv = backendOverrideContent
? 'backend' ? 'backend'
: note.getScriptEnv(); : note.getScriptEnv();
} }

View File

@ -5,6 +5,7 @@ function lex(str) {
const fulltextTokens = []; const fulltextTokens = [];
const expressionTokens = []; const expressionTokens = [];
/** @type {boolean|string} */
let quotes = false; // otherwise contains used quote - ', " or ` let quotes = false; // otherwise contains used quote - ', " or `
let fulltextEnded = false; let fulltextEnded = false;
let currentWord = ''; let currentWord = '';

View File

@ -155,7 +155,7 @@ function getExpression(tokens, searchContext, level = 0) {
i++; i++;
return new NoteContentFulltextExp(operator.token, {tokens: [tokens[i].token], raw }); return new NoteContentFulltextExp(operator.token, {tokens: [tokens[i].token], raw});
} }
if (tokens[i].token === 'parents') { if (tokens[i].token === 'parents') {
@ -389,7 +389,7 @@ function getExpression(tokens, searchContext, level = 0) {
else if (token === 'note') { else if (token === 'note') {
i++; i++;
expressions.push(parseNoteProperty(tokens)); expressions.push(parseNoteProperty());
continue; continue;
} }

View File

@ -73,7 +73,7 @@ function searchFromRelation(note, relationName) {
return []; return [];
} }
const result = scriptService.executeNote(scriptNote, { originEntity: note }); const result = scriptService.executeNote(scriptNote, {originEntity: note});
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
log.info(`Result from ${scriptNote.noteId} is not an array.`); log.info(`Result from ${scriptNote.noteId} is not an array.`);
@ -288,7 +288,7 @@ function searchNotesForAutocomplete(query) {
noteTitle: beccaService.getNoteTitle(result.noteId), noteTitle: beccaService.getNoteTitle(result.noteId),
notePathTitle: result.notePathTitle, notePathTitle: result.notePathTitle,
highlightedNotePathTitle: result.highlightedNotePathTitle highlightedNotePathTitle: result.highlightedNotePathTitle
} };
}); });
} }
@ -370,7 +370,7 @@ function formatAttribute(attr) {
let label = `#${utils.escapeHtml(attr.name)}`; let label = `#${utils.escapeHtml(attr.name)}`;
if (attr.value) { if (attr.value) {
const val = /[^\w_-]/.test(attr.value) ? `"${attr.value}"` : attr.value; const val = /[^\w-]/.test(attr.value) ? `"${attr.value}"` : attr.value;
label += `=${utils.escapeHtml(val)}`; label += `=${utils.escapeHtml(val)}`;
} }

View File

@ -217,7 +217,7 @@ function wrap(query, func) {
// in these cases error should be simply ignored. // in these cases error should be simply ignored.
console.log(e.message); console.log(e.message);
return null return null;
} }
throw e; throw e;
@ -281,7 +281,7 @@ function fillParamList(paramIds, truncate = true) {
} }
// doing it manually to avoid this showing up on the sloq query list // doing it manually to avoid this showing up on the sloq query list
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`, paramIds); const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`);
s.run(paramIds); s.run(paramIds);
} }

View File

@ -123,11 +123,16 @@ function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) {
} }
} }
function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false) { function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false, sortNatural = false, sortLocale) {
if (!customSortBy) { if (!customSortBy) {
customSortBy = 'title'; customSortBy = 'title';
} }
if (!sortLocale) {
// sortLocale can not be empty string or null value, default value must be set to undefined.
sortLocale = undefined;
}
sql.transactional(() => { sql.transactional(() => {
const notes = becca.getNote(parentNoteId).getChildNotes(); const notes = becca.getNote(parentNoteId).getChildNotes();
@ -153,7 +158,14 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
} }
function compare(a, b) { function compare(a, b) {
return b === null || b === undefined || a < b ? -1 : 1; if (!sortNatural){
// alphabetical sort
return b === null || b === undefined || a < b ? -1 : 1;
} else {
// natural sort
return a.localeCompare(b, sortLocale, {numeric: true, sensitivity: 'base'});
}
} }
const topAEl = fetchValue(a, 'top'); const topAEl = fetchValue(a, 'top');
@ -224,8 +236,11 @@ function sortNotesIfNeeded(parentNoteId) {
const sortReversed = parentNote.getLabelValue('sortDirection')?.toLowerCase() === "desc"; const sortReversed = parentNote.getLabelValue('sortDirection')?.toLowerCase() === "desc";
const sortFoldersFirstLabel = parentNote.getLabel('sortFoldersFirst'); const sortFoldersFirstLabel = parentNote.getLabel('sortFoldersFirst');
const sortFoldersFirst = sortFoldersFirstLabel && sortFoldersFirstLabel.value.toLowerCase() !== "false"; const sortFoldersFirst = sortFoldersFirstLabel && sortFoldersFirstLabel.value.toLowerCase() !== "false";
const sortNaturalLabel = parentNote.getLabel('sortNatural');
const sortNatural = sortNaturalLabel && sortNaturalLabel.value.toLowerCase() !== "false";
const sortLocale = parentNote.getLabelValue('sortLocale');
sortNotes(parentNoteId, sortedLabel.value, sortReversed, sortFoldersFirst); sortNotes(parentNoteId, sortedLabel.value, sortReversed, sortFoldersFirst, sortNatural, sortLocale);
} }
/** /**