mirror of
https://github.com/zadam/trilium.git
synced 2025-11-09 14:55:50 +01:00
Merge branch 'master' into next60
# Conflicts: # package-lock.json # package.json
This commit is contained in:
33
src/app.js
33
src/app.js
@@ -5,12 +5,13 @@ const favicon = require('serve-favicon');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const helmet = require('helmet');
|
||||
const session = require('express-session');
|
||||
const compression = require('compression')
|
||||
const compression = require('compression');
|
||||
const FileStore = require('session-file-store')(session);
|
||||
const sessionSecret = require('./services/session_secret');
|
||||
const dataDir = require('./services/data_dir');
|
||||
const utils = require('./services/utils');
|
||||
const assetPath = require('./services/asset_path');
|
||||
const env = require('./services/env');
|
||||
require('./services/handlers');
|
||||
require('./becca/becca_loader');
|
||||
|
||||
@@ -30,27 +31,37 @@ app.use(helmet({
|
||||
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.json({limit: '500mb'}));
|
||||
app.use(express.raw({limit: '500mb'}));
|
||||
app.use(express.urlencoded({extended: false}));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public/root')));
|
||||
app.use(`/${assetPath}/app`, express.static(path.join(__dirname, 'public/app')));
|
||||
app.use(`/${assetPath}/app-dist`, express.static(path.join(__dirname, 'public/app-dist')));
|
||||
app.use(`/${assetPath}/fonts`, express.static(path.join(__dirname, 'public/fonts')));
|
||||
app.use(`/${assetPath}/app`, persistentCacheStatic(path.join(__dirname, 'public/app')));
|
||||
app.use(`/${assetPath}/app-dist`, persistentCacheStatic(path.join(__dirname, 'public/app-dist')));
|
||||
app.use(`/${assetPath}/fonts`, persistentCacheStatic(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(`/${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')));
|
||||
// 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-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/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`, 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
|
||||
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}/images`, express.static(path.join(__dirname, '..', 'images')));
|
||||
app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/`, persistentCacheStatic(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/')));
|
||||
app.use(`/${assetPath}/images`, persistentCacheStatic(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(`/robots.txt`, express.static(path.join(__dirname, 'public/robots.txt')));
|
||||
@@ -61,7 +72,7 @@ const sessionParser = session({
|
||||
cookie: {
|
||||
// path: "/",
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
|
||||
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
|
||||
},
|
||||
name: 'trilium.sid',
|
||||
store: new FileStore({
|
||||
|
||||
@@ -70,7 +70,9 @@ function reload() {
|
||||
}
|
||||
|
||||
function postProcessEntityUpdate(entityName, entity) {
|
||||
if (entityName === 'branches') {
|
||||
if (entityName === 'notes') {
|
||||
noteUpdated(entity);
|
||||
} else if (entityName === 'branches') {
|
||||
branchUpdated(entity);
|
||||
} else if (entityName === 'attributes') {
|
||||
attributeUpdated(entity);
|
||||
@@ -161,6 +163,15 @@ function branchDeleted(branchId) {
|
||||
delete becca.branches[branch.branchId];
|
||||
}
|
||||
|
||||
function noteUpdated(entity) {
|
||||
const note = becca.notes[entity.noteId];
|
||||
|
||||
if (note) {
|
||||
// type / mime could have been changed, and they are present in flatTextCache
|
||||
note.flatTextCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
function branchUpdated(branch) {
|
||||
const childNote = becca.notes[branch.noteId];
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class BAttribute extends AbstractBeccaEntity {
|
||||
}
|
||||
|
||||
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}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
* @private */
|
||||
this.parents = [];
|
||||
/** @type {BNote[]}
|
||||
* @private*/
|
||||
* @private */
|
||||
this.children = [];
|
||||
/** @type {BAttribute[]}
|
||||
* @private */
|
||||
@@ -110,11 +110,11 @@ class BNote extends AbstractBeccaEntity {
|
||||
* @private */
|
||||
this.__attributeCache = null;
|
||||
/** @type {BAttribute[]|null}
|
||||
* @private*/
|
||||
* @private */
|
||||
this.inheritableAttributeCache = null;
|
||||
|
||||
/** @type {BAttribute[]}
|
||||
* @private*/
|
||||
* @private */
|
||||
this.targetRelations = [];
|
||||
|
||||
this.becca.addNote(this.noteId, this);
|
||||
@@ -472,6 +472,20 @@ class BNote extends AbstractBeccaEntity {
|
||||
*/
|
||||
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} [value] - label value
|
||||
|
||||
@@ -2,7 +2,7 @@ const becca = require('./becca');
|
||||
const log = require('../services/log');
|
||||
const beccaService = require('./becca_service');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const { JSDOM } = require("jsdom");
|
||||
const {JSDOM} = require("jsdom");
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -168,7 +168,6 @@ function trimMime(mime) {
|
||||
}
|
||||
|
||||
mimeCache[mime] = str;
|
||||
mimeCache[mime] = str;
|
||||
}
|
||||
|
||||
return mimeCache[mime];
|
||||
@@ -224,8 +223,8 @@ function splitToWords(text) {
|
||||
*/
|
||||
function hasConnectingRelation(sourceNote, targetNote) {
|
||||
return sourceNote.getAttributes().find(attr => attr.type === 'relation'
|
||||
&& ['includenotelink', 'imagelink'].includes(attr.name)
|
||||
&& attr.value === targetNote.noteId);
|
||||
&& ['includenotelink', 'imagelink'].includes(attr.name)
|
||||
&& attr.value === targetNote.noteId);
|
||||
}
|
||||
|
||||
async function findSimilarNotes(noteId) {
|
||||
@@ -301,7 +300,7 @@ async function findSimilarNotes(noteId) {
|
||||
|
||||
for (const branch of parentNote.getParentBranches()) {
|
||||
score += gatherRewards(branch.prefix, 0.3)
|
||||
+ gatherAncestorRewards(branch.parentNote);
|
||||
+ gatherAncestorRewards(branch.parentNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +313,7 @@ async function findSimilarNotes(noteId) {
|
||||
|
||||
function computeScore(candidateNote) {
|
||||
let score = gatherRewards(trimMime(candidateNote.mime))
|
||||
+ gatherAncestorRewards(candidateNote);
|
||||
+ gatherAncestorRewards(candidateNote);
|
||||
|
||||
if (candidateNote.isDecrypted) {
|
||||
score += gatherRewards(candidateNote.title);
|
||||
@@ -382,7 +381,7 @@ async function findSimilarNotes(noteId) {
|
||||
score += 1;
|
||||
}
|
||||
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) {
|
||||
console.log("Adding reward for same day of creation");
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ function register(router) {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(res, date);
|
||||
throw getDateInvalidError(date);
|
||||
}
|
||||
|
||||
const note = specialNotesService.getInboxNote(date);
|
||||
@@ -31,7 +31,7 @@ function register(router) {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(res, date);
|
||||
throw getDateInvalidError(date);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getDayNote(date);
|
||||
@@ -42,7 +42,7 @@ function register(router) {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(res, date);
|
||||
throw getDateInvalidError(date);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getWeekNote(date);
|
||||
@@ -53,7 +53,7 @@ function register(router) {
|
||||
const {month} = req.params;
|
||||
|
||||
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
|
||||
throw getMonthInvalidError(res, month);
|
||||
throw getMonthInvalidError(month);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getMonthNote(month);
|
||||
@@ -64,7 +64,7 @@ function register(router) {
|
||||
const {year} = req.params;
|
||||
|
||||
if (!/[0-9]{4}/.test(year)) {
|
||||
throw getYearInvalidError(res, year);
|
||||
throw getYearInvalidError(year);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getYearNote(year);
|
||||
|
||||
@@ -608,6 +608,20 @@ class FNote {
|
||||
*/
|
||||
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
|
||||
* @returns {boolean} true if relation exists (excluding inherited)
|
||||
@@ -719,7 +733,14 @@ class FNote {
|
||||
});
|
||||
|
||||
// attrs are not resorted if position changes after initial load
|
||||
promotedAttrs.sort((a, b) => a.position < b.position ? -1 : 1);
|
||||
promotedAttrs.sort((a, b) => {
|
||||
if (a.noteId === b.noteId) {
|
||||
return a.position < b.position ? -1 : 1;
|
||||
} else {
|
||||
// inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||
return a.noteId < b.noteId ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
return promotedAttrs;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,10 @@ export default class DesktopLayout {
|
||||
)
|
||||
.child(
|
||||
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 SearchDefinitionWidget())
|
||||
.ribbon(new EditedNotesWidget())
|
||||
@@ -135,7 +139,6 @@ export default class DesktopLayout {
|
||||
.ribbon(new NotePropertiesWidget())
|
||||
.ribbon(new FilePropertiesWidget())
|
||||
.ribbon(new ImagePropertiesWidget())
|
||||
.ribbon(new PromotedAttributesWidget())
|
||||
.ribbon(new BasicPropertiesWidget())
|
||||
.ribbon(new OwnedAttributeListWidget())
|
||||
.ribbon(new InheritedAttributesWidget())
|
||||
|
||||
@@ -99,11 +99,16 @@ class ContextMenu {
|
||||
const $item = $("<li>")
|
||||
.addClass("dropdown-item")
|
||||
.append($link)
|
||||
.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 => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.which !== 1) { // only left click triggers menu items
|
||||
return false;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
|
||||
if (item.handler) {
|
||||
@@ -142,7 +147,9 @@ class ContextMenu {
|
||||
// "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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
branchIdsToMove = filterSearchBranches(branchIdsToMove);
|
||||
|
||||
const beforeBranch = await froca.getBranch(beforeBranchId);
|
||||
const beforeBranch = froca.getBranch(beforeBranchId);
|
||||
|
||||
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
|
||||
toastService.showError('Cannot move notes here.');
|
||||
@@ -31,7 +31,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
branchIdsToMove = filterSearchBranches(branchIdsToMove);
|
||||
|
||||
const afterNote = await froca.getBranch(afterBranchId).getNote();
|
||||
const afterNote = froca.getBranch(afterBranchId).getNote();
|
||||
|
||||
const forbiddenNoteIds = [
|
||||
'root',
|
||||
@@ -59,7 +59,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
}
|
||||
|
||||
async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
const newParentBranch = await froca.getBranch(newParentBranchId);
|
||||
const newParentBranch = froca.getBranch(newParentBranchId);
|
||||
|
||||
if (newParentBranch.noteId === '_lbRoot') {
|
||||
toastService.showError('Cannot move notes here.');
|
||||
@@ -165,7 +165,7 @@ function filterRootNote(branchIds) {
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
return branchIds.filter(branchId => {
|
||||
const branch = froca.getBranch(branchId);
|
||||
const branch = froca.getBranch(branchId);
|
||||
|
||||
return branch.noteId !== 'root'
|
||||
&& branch.noteId !== hoistedNoteId;
|
||||
|
||||
@@ -14,7 +14,7 @@ async function processEntityChanges(entityChanges) {
|
||||
if (ec.entityName === 'notes') {
|
||||
processNoteChange(loadResults, ec);
|
||||
} else if (ec.entityName === 'branches') {
|
||||
processBranchChange(loadResults, ec);
|
||||
await processBranchChange(loadResults, ec);
|
||||
} else if (ec.entityName === 'attributes') {
|
||||
processAttributeChange(loadResults, ec);
|
||||
} 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) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
|
||||
return;
|
||||
@@ -138,7 +138,15 @@ function processBranchChange(loadResults, ec) {
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
|
||||
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) {
|
||||
branch.update(ec.entity);
|
||||
|
||||
@@ -65,7 +65,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
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 });
|
||||
|
||||
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 {ntxId} = subContexts[subContexts.length - 1];
|
||||
|
||||
appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
|
||||
await appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
|
||||
|
||||
if (activate) {
|
||||
appContext.triggerEvent('focusAndSelectTitle');
|
||||
await appContext.triggerEvent('focusAndSelectTitle');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ export async function uploadFiles(parentNoteId, files, options) {
|
||||
}
|
||||
|
||||
const taskId = utils.randomString(10);
|
||||
let noteId;
|
||||
let counter = 0;
|
||||
|
||||
for (const file of files) {
|
||||
@@ -25,19 +24,19 @@ export async function uploadFiles(parentNoteId, files, options) {
|
||||
formData.append(key, options[key]);
|
||||
}
|
||||
|
||||
({noteId} = await $.ajax({
|
||||
await $.ajax({
|
||||
url: `${baseApiUrl}notes/${parentNoteId}/import`,
|
||||
headers: await server.getHeaders(),
|
||||
data: formData,
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
timeout: 60 * 60 * 1000,
|
||||
error: function(xhr) {
|
||||
error: function (xhr) {
|
||||
toastService.showError(`Import failed: ${xhr.responseText}`);
|
||||
},
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
processData: false, // NEEDED, DON'T REMOVE THIS
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,4 +73,4 @@ ws.subscribeToMessages(async message => {
|
||||
|
||||
export default {
|
||||
uploadFiles
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,86 +12,88 @@ const TPL = `
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.note-list.grid-view .note-list-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
flex-basis: 300px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
.note-list.grid-view .note-expander {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
|
||||
.note-list.grid-view .note-book-card:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--main-border-color);
|
||||
background: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
|
||||
.note-book-card {
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 10px 15px 15px 8px;
|
||||
margin: 5px 5px 5px 0;
|
||||
margin: 5px 5px 5px 5px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
.note-book-card:not(.expanded) .note-book-content {
|
||||
display: none !important;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
|
||||
.note-book-card.expanded .note-book-content {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.note-book-header {
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-bottom: 0;
|
||||
padding-bottom: .5rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
/* not-expanded title is limited to one line only */
|
||||
.note-book-card:not(.expanded) .note-book-header {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.note-book-header .rendered-note-attributes {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
|
||||
.note-book-header .rendered-note-attributes:before {
|
||||
content: "\\00a0\\00a0";
|
||||
}
|
||||
|
||||
|
||||
.note-book-header .note-icon {
|
||||
font-size: 100%;
|
||||
display: inline-block;
|
||||
padding-right: 7px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.note-book-card .note-book-card {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
|
||||
.note-book-content.type-image, .note-book-content.type-file, .note-book-content.type-protectedSession {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -99,46 +101,46 @@ const TPL = `
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.note-book-content.type-image img, .note-book-content.type-canvas svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
.note-book-card.type-image .note-book-content img,
|
||||
.note-book-card.type-text .note-book-content img,
|
||||
.note-book-card.type-canvas .note-book-content img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.note-book-header {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
|
||||
.note-list-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.note-expander {
|
||||
font-size: x-large;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.note-list-pager {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div class="note-list-wrapper">
|
||||
<div class="note-list-pager"></div>
|
||||
|
||||
|
||||
<div class="note-list-container"></div>
|
||||
|
||||
|
||||
<div class="note-list-pager"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -29,7 +29,7 @@ function formatTimeWithSeconds(date) {
|
||||
|
||||
// this is producing local time!
|
||||
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
|
||||
|
||||
return formatDateISO(date);
|
||||
@@ -45,7 +45,7 @@ function formatDateTime(date) {
|
||||
}
|
||||
|
||||
function localNowDateTime() {
|
||||
return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ')
|
||||
return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ');
|
||||
}
|
||||
|
||||
function now() {
|
||||
@@ -101,7 +101,7 @@ async function stopWatch(what, func) {
|
||||
}
|
||||
|
||||
function formatValueWithWhitespace(val) {
|
||||
return /[^\w_-]/.test(val) ? `"${val}"` : val;
|
||||
return /[^\w-]/.test(val) ? `"${val}"` : val;
|
||||
}
|
||||
|
||||
function formatLabel(label) {
|
||||
@@ -329,7 +329,7 @@ function initHelpDropdown($el) {
|
||||
initHelpButtons($dropdownMenu);
|
||||
}
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
|
||||
|
||||
function openHelp(e) {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
@@ -340,7 +340,7 @@ function initHelpButtons($el) {
|
||||
// so we do it manually
|
||||
$el.on("click", e => {
|
||||
if ($(e.target).attr("data-help-page")) {
|
||||
openHelp(e)
|
||||
openHelp(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,7 +285,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$title = this.$widget.find('.attr-detail-title');
|
||||
|
||||
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('autocomplete:closed', () => this.userEditedAttribute());
|
||||
|
||||
@@ -299,7 +303,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$rowValue = this.$widget.find('.attr-row-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('autocomplete:closed', () => this.userEditedAttribute());
|
||||
this.$inputValue.on('focus', () => {
|
||||
@@ -328,7 +336,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$rowInverseRelation = this.$widget.find('.attr-row-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.$inputTargetNote = this.$widget.find('.attr-input-target-note');
|
||||
|
||||
@@ -16,16 +16,17 @@ const TPL = `
|
||||
}
|
||||
|
||||
.global-menu-button {
|
||||
background-image: url("${window.glob.assetPath}/images/icon-black.png");
|
||||
background-image: url("${window.glob.assetPath}/images/icon-black.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 45%;
|
||||
background-position: 50% 80%;
|
||||
background-size: 45px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.global-menu-button:hover {
|
||||
background-image: url("${window.glob.assetPath}/images/icon-color.png");
|
||||
background-image: url("${window.glob.assetPath}/images/icon-color.svg");
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,26 @@ const TPL = `<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" ro
|
||||
sort folders at the top
|
||||
</label>
|
||||
</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 class="modal-footer">
|
||||
<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 sortDirection = this.$form.find("input[name='sort-direction']:checked").val();
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -11175,6 +11175,22 @@ const icons = [
|
||||
}
|
||||
];
|
||||
|
||||
function getIconClass(icon) {
|
||||
if (icon.type_of_icon === 'LOGO') {
|
||||
return `bxl-${icon.name}`;
|
||||
}
|
||||
else if (icon.type_of_icon === 'SOLID') {
|
||||
return `bxs-${icon.name}`;
|
||||
}
|
||||
else {
|
||||
return `bx-${icon.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const icon of icons) {
|
||||
icon.className = getIconClass(icon);
|
||||
}
|
||||
|
||||
export default {
|
||||
categories,
|
||||
icons
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import server from "../services/server.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-icon-widget dropdown">
|
||||
@@ -147,6 +148,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async renderDropdown(categoryId, search) {
|
||||
const iconToCountPromise = this.getIconToCountMap();
|
||||
|
||||
this.$iconList.empty();
|
||||
|
||||
if (this.getIconLabels().length > 0) {
|
||||
@@ -165,41 +168,50 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
|
||||
search = search?.trim()?.toLowerCase();
|
||||
|
||||
for (const icon of icons) {
|
||||
const filteredIcons = icons.filter(icon => {
|
||||
if (categoryId && icon.category_id !== categoryId) {
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
if (!icon.name.includes(search) && !icon.term?.find(t => t.includes(search))) {
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.$iconList.append(
|
||||
$('<span>')
|
||||
.addClass(this.getIconClass(icon))
|
||||
.attr("title", icon.name)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
|
||||
const iconToCount = await iconToCountPromise;
|
||||
|
||||
filteredIcons.sort((a, b) => {
|
||||
const countA = iconToCount[a.className] || 0;
|
||||
const countB = iconToCount[b.className] || 0;
|
||||
|
||||
return countB - countA;
|
||||
});
|
||||
|
||||
for (const icon of filteredIcons) {
|
||||
this.$iconList.append(this.renderIcon(icon));
|
||||
}
|
||||
|
||||
this.$iconSearch.focus();
|
||||
}
|
||||
|
||||
async getIconToCountMap() {
|
||||
const {iconClassToCountMap} = await server.get('other/icon-usage');
|
||||
|
||||
return iconClassToCountMap;
|
||||
}
|
||||
|
||||
renderIcon(icon) {
|
||||
return $('<span>')
|
||||
.addClass("bx " + icon.className)
|
||||
.attr("title", icon.name);
|
||||
}
|
||||
|
||||
getIconLabels() {
|
||||
return this.note.getOwnedLabels()
|
||||
.filter(label => ['workspaceIconClass', 'iconClass'].includes(label.name));
|
||||
}
|
||||
|
||||
getIconClass(icon) {
|
||||
if (icon.type_of_icon === 'LOGO') {
|
||||
return `bx bxl-${icon.name}`;
|
||||
}
|
||||
else if (icon.type_of_icon === 'SOLID') {
|
||||
return `bx bxs-${icon.name}`;
|
||||
}
|
||||
else {
|
||||
return `bx bx-${icon.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
||||
this.$container = this.$widget.find(".note-map-container");
|
||||
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 => {
|
||||
const type = $(e.target).closest("button").attr("data-type");
|
||||
|
||||
@@ -41,7 +41,6 @@ const TPL = `
|
||||
}
|
||||
|
||||
.tree-actions {
|
||||
padding: 4px 0;
|
||||
background-color: var(--launcher-pane-background-color);
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
@@ -49,13 +48,15 @@ const TPL = `
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
right: 11.77px;
|
||||
right: 17px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
button.tree-floating-button {
|
||||
margin: 1px;
|
||||
font-size: 1.5em;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
max-height: 34px;
|
||||
color: var(--launcher-pane-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
|
||||
@@ -92,7 +92,18 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
getInheritedAttributes(note) {
|
||||
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||
const attrs = note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||
|
||||
attrs.sort((a, b) => {
|
||||
if (a.noteId === b.noteId) {
|
||||
return a.position < b.position ? -1 : 1;
|
||||
} else {
|
||||
// inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||
return a.noteId < b.noteId ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -87,8 +87,8 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
|
||||
this.noteMapWidget.setDimensions();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (!this.graph) { // no graph has been even rendered
|
||||
const handleResize = () => {
|
||||
if (!this.noteMapWidget.graph) { // no graph has been even rendered
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,9 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
|
||||
else if (this.openState === 'small') {
|
||||
this.setSmallSize();
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
new ResizeObserver(handleResize).observe(this.$widget[0])
|
||||
}
|
||||
|
||||
setSmallSize() {
|
||||
|
||||
@@ -29,6 +29,9 @@ const TPL = `
|
||||
.promoted-attribute-cell div.input-group {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.promoted-attribute-cell strong {
|
||||
word-break:keep-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="promoted-attributes-container"></div>
|
||||
@@ -54,13 +57,13 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
return { show: false };
|
||||
return {show: false};
|
||||
}
|
||||
|
||||
return {
|
||||
show: true,
|
||||
activate: true,
|
||||
title: "Promoted attributes",
|
||||
title: "Promoted Attributes",
|
||||
icon: "bx bx-table"
|
||||
};
|
||||
}
|
||||
@@ -144,7 +147,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
attributeValues = attributeValues.map(attribute => ({ value: attribute }));
|
||||
attributeValues = attributeValues.map(attribute => ({value: attribute}));
|
||||
|
||||
$input.autocomplete({
|
||||
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') {
|
||||
|
||||
@@ -180,7 +180,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: 'Search parameters',
|
||||
title: 'Search Parameters',
|
||||
icon: 'bx bx-search'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,14 @@ export default class SharedInfoWidget extends NoteContextAwareWidget {
|
||||
this.$sharedText.text("This note is shared publicly on");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class SqlResultWidget extends NoteContextAwareWidget {
|
||||
const $row = $("<tr>");
|
||||
|
||||
for (const key in result) {
|
||||
$row.append($("<th>").html(key));
|
||||
$row.append($("<th>").text(key));
|
||||
}
|
||||
|
||||
$table.append($row);
|
||||
@@ -69,7 +69,7 @@ export default class SqlResultWidget extends NoteContextAwareWidget {
|
||||
const $row = $("<tr>");
|
||||
|
||||
for (const key in result) {
|
||||
$row.append($("<td>").html(result[key]));
|
||||
$row.append($("<td>").text(result[key]));
|
||||
}
|
||||
|
||||
$table.append($row);
|
||||
|
||||
@@ -280,14 +280,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
this.layoutTabs();
|
||||
};
|
||||
|
||||
// ResizeObserver exists only in FF69
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
new ResizeObserver(resizeListener).observe(this.$widget[0]);
|
||||
}
|
||||
else {
|
||||
// for older firefox
|
||||
window.addEventListener('resize', resizeListener);
|
||||
}
|
||||
new ResizeObserver(resizeListener).observe(this.$widget[0]);
|
||||
|
||||
this.tabEls.forEach((tabEl) => this.setTabCloseEvent(tabEl));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const FONT_FAMILIES = [
|
||||
{ value: "Bradley Hand", label: "Bradley Hand" },
|
||||
{ value: "Luminari", label: "Luminari" },
|
||||
{ value: "Comic Sans MS", label: "Comic Sans MS" },
|
||||
{ value: "Microsoft YaHei", label: "Microsoft YaHei" },
|
||||
];
|
||||
|
||||
const TPL = `
|
||||
|
||||
@@ -470,11 +470,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
async createNoteBox(noteId, title, x, y) {
|
||||
const $link = await linkService.createNoteLink(noteId, {title});
|
||||
$link.mousedown(e => {
|
||||
console.log(e);
|
||||
|
||||
linkService.goToLink(e);
|
||||
});
|
||||
$link.mousedown(e => linkService.goToLink(e));
|
||||
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
|
||||
body .global-menu-button {
|
||||
background-image: url("../images/icon-grey.png");
|
||||
background-image: url("../images/icon-grey.svg");
|
||||
}
|
||||
|
||||
body ::-webkit-calendar-picker-indicator {
|
||||
|
||||
@@ -94,13 +94,13 @@ function undeleteNote(req) {
|
||||
|
||||
function sortChildNotes(req) {
|
||||
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';
|
||||
|
||||
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst);
|
||||
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst, sortNatural, sortLocale);
|
||||
}
|
||||
|
||||
function protectNote(req) {
|
||||
|
||||
@@ -74,7 +74,7 @@ function getOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
resultMap['isPasswordSet'] = !!optionMap['passwordVerificationHash'] ? 'true' : 'false';
|
||||
resultMap['isPasswordSet'] = optionMap['passwordVerificationHash'] ? 'true' : 'false';
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
29
src/routes/api/other.js
Normal file
29
src/routes/api/other.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const becca = require("../../becca/becca");
|
||||
|
||||
function getIconUsage() {
|
||||
const iconClassToCountMap = {};
|
||||
|
||||
for (const {value: iconClass, noteId} of becca.findAttributes('label', 'iconClass')) {
|
||||
if (noteId.startsWith("_")) {
|
||||
continue; // ignore icons of "system" notes since they were not set by the user
|
||||
}
|
||||
|
||||
if (!iconClass?.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const clazz of iconClass.trim().split(/\s+/)) {
|
||||
if (clazz === 'bx') {
|
||||
continue;
|
||||
}
|
||||
|
||||
iconClassToCountMap[clazz] = (iconClassToCountMap[clazz] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { iconClassToCountMap };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getIconUsage
|
||||
};
|
||||
@@ -57,6 +57,7 @@ const backendLogRoute = require('./api/backend_log');
|
||||
const statsRoute = require('./api/stats');
|
||||
const fontsRoute = require('./api/fonts');
|
||||
const etapiTokensApiRoutes = require('./api/etapi_tokens');
|
||||
const otherRoute = require('./api/other');
|
||||
const shareRoutes = require('../share/routes');
|
||||
const etapiAuthRoutes = require('../etapi/auth');
|
||||
const etapiAppInfoRoutes = require('../etapi/app_info');
|
||||
@@ -305,6 +306,7 @@ function register(app) {
|
||||
apiRoute(POST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview);
|
||||
|
||||
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
apiRoute(GET, '/api/other/icon-usage', otherRoute.getIconUsage);
|
||||
|
||||
apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens);
|
||||
apiRoute(POST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict"
|
||||
"use strict";
|
||||
|
||||
function formatAttrForSearch(attr, searchWithValue) {
|
||||
let searchStr = '';
|
||||
@@ -28,7 +28,7 @@ function formatAttrForSearch(attr, searchWithValue) {
|
||||
}
|
||||
|
||||
function formatValue(val) {
|
||||
if (!/[^\w_]/.test(val)) {
|
||||
if (!/[^\w]/.test(val)) {
|
||||
return val;
|
||||
}
|
||||
else if (!val.includes('"')) {
|
||||
@@ -47,4 +47,4 @@ function formatValue(val) {
|
||||
|
||||
module.exports = {
|
||||
formatAttrForSearch
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ const BAttribute = require('../becca/entities/battribute');
|
||||
const {formatAttrForSearch} = require("./attribute_formatter");
|
||||
const BUILTIN_ATTRIBUTES = require("./builtin_attributes");
|
||||
|
||||
const ATTRIBUTE_TYPES = [ 'label', 'relation' ];
|
||||
const ATTRIBUTE_TYPES = ['label', 'relation'];
|
||||
|
||||
/** @returns {BNote[]} */
|
||||
function getNotesWithLabel(name, value = undefined) {
|
||||
@@ -122,7 +122,7 @@ function isAttributeType(type) {
|
||||
|
||||
function isAttributeDangerous(type, name) {
|
||||
return BUILTIN_ATTRIBUTES.some(attr =>
|
||||
attr.type === attr.type &&
|
||||
attr.type === type &&
|
||||
attr.name.toLowerCase() === name.trim().toLowerCase() &&
|
||||
attr.isDangerous
|
||||
);
|
||||
|
||||
@@ -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 === '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 launcherNote =
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2023-03-14T21:15:08+01:00", buildRevision: "d8e9086bdeb721db795783b5d92395a9bd6882c9" };
|
||||
module.exports = { buildDate:"", buildRevision: "9881e6de3e4966af39ec6245562dca6ac7b25eaa" };
|
||||
|
||||
@@ -42,6 +42,8 @@ module.exports = [
|
||||
{ type: 'label', name: 'sorted' },
|
||||
{ type: 'label', name: 'sortDirection' },
|
||||
{ type: 'label', name: 'sortFoldersFirst' },
|
||||
{ type: 'label', name: 'sortNatural' },
|
||||
{ type: 'label', name: 'sortLocale' },
|
||||
{ type: 'label', name: 'top' },
|
||||
{ type: 'label', name: 'fullContentWidth' },
|
||||
{ type: 'label', name: 'shareHiddenFromTree' },
|
||||
|
||||
@@ -46,7 +46,7 @@ eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETE
|
||||
if (entityName === 'attributes') {
|
||||
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);
|
||||
} else if (entity.type === 'label') {
|
||||
handleMaybeSortingLabel(entity);
|
||||
@@ -101,7 +101,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
||||
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);
|
||||
}
|
||||
else if (entity.type === 'label') {
|
||||
|
||||
@@ -24,7 +24,7 @@ const noteTypesService = require("./note_types");
|
||||
const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js");
|
||||
|
||||
function getNewNotePosition(parentNote) {
|
||||
if (parentNote.hasLabel('newNotesOnTop')) {
|
||||
if (parentNote.isLabelTruthy('newNotesOnTop')) {
|
||||
const minNotePos = parentNote.getChildBranches()
|
||||
.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');
|
||||
}
|
||||
|
||||
log.info(`Duplicating ${origNoteId} subtree into ${newParentNoteId}`);
|
||||
log.info(`Duplicating '${origNoteId}' subtree into '${newParentNoteId}'`);
|
||||
|
||||
const origNote = becca.notes[origNoteId];
|
||||
// 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.save();
|
||||
// the relation targets may not be created yet, the mapping is pre-generated
|
||||
attr.save({skipValidation: true});
|
||||
}
|
||||
|
||||
for (const childBranch of origNote.getChildBranches()) {
|
||||
|
||||
@@ -125,7 +125,7 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds =
|
||||
}
|
||||
|
||||
if (root) {
|
||||
scriptEnv = !!backendOverrideContent
|
||||
scriptEnv = backendOverrideContent
|
||||
? 'backend'
|
||||
: note.getScriptEnv();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ function lex(str) {
|
||||
const fulltextTokens = [];
|
||||
const expressionTokens = [];
|
||||
|
||||
/** @type {boolean|string} */
|
||||
let quotes = false; // otherwise contains used quote - ', " or `
|
||||
let fulltextEnded = false;
|
||||
let currentWord = '';
|
||||
|
||||
@@ -155,7 +155,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
|
||||
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') {
|
||||
@@ -389,7 +389,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
else if (token === 'note') {
|
||||
i++;
|
||||
|
||||
expressions.push(parseNoteProperty(tokens));
|
||||
expressions.push(parseNoteProperty());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ function searchFromRelation(note, relationName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = scriptService.executeNote(scriptNote, { originEntity: note });
|
||||
const result = scriptService.executeNote(scriptNote, {originEntity: note});
|
||||
|
||||
if (!Array.isArray(result)) {
|
||||
log.info(`Result from ${scriptNote.noteId} is not an array.`);
|
||||
@@ -288,7 +288,7 @@ function searchNotesForAutocomplete(query) {
|
||||
noteTitle: beccaService.getNoteTitle(result.noteId),
|
||||
notePathTitle: result.notePathTitle,
|
||||
highlightedNotePathTitle: result.highlightedNotePathTitle
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -299,7 +299,9 @@ function highlightSearchResults(searchResults, highlightedTokens) {
|
||||
// which would make the resulting HTML string invalid.
|
||||
// { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character)
|
||||
// < and > are used for marking <small> and </small>
|
||||
highlightedTokens = highlightedTokens.map(token => token.replace('/[<\{\}]/g', ''));
|
||||
highlightedTokens = highlightedTokens
|
||||
.map(token => token.replace('/[<\{\}]/g', ''))
|
||||
.filter(token => !!token?.trim());
|
||||
|
||||
// sort by the longest, so we first highlight the longest matches
|
||||
highlightedTokens.sort((a, b) => a.length > b.length ? -1 : 1);
|
||||
@@ -307,7 +309,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
|
||||
for (const result of searchResults) {
|
||||
const note = becca.notes[result.noteId];
|
||||
|
||||
result.highlightedNotePathTitle = result.notePathTitle.replace('/[<\{\}]/g', '');
|
||||
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, '');
|
||||
|
||||
if (highlightedTokens.find(token => note.type.includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "type: ${note.type}'`;
|
||||
@@ -368,7 +370,7 @@ function formatAttribute(attr) {
|
||||
let label = `#${utils.escapeHtml(attr.name)}`;
|
||||
|
||||
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)}`;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ function wrap(query, func) {
|
||||
// in these cases error should be simply ignored.
|
||||
console.log(e.message);
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
throw e;
|
||||
@@ -281,7 +281,7 @@ function fillParamList(paramIds, truncate = true) {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
customSortBy = 'title';
|
||||
}
|
||||
|
||||
if (!sortLocale) {
|
||||
// sortLocale can not be empty string or null value, default value must be set to undefined.
|
||||
sortLocale = undefined;
|
||||
}
|
||||
|
||||
sql.transactional(() => {
|
||||
const notes = becca.getNote(parentNoteId).getChildNotes();
|
||||
|
||||
@@ -153,7 +158,14 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -224,8 +236,11 @@ function sortNotesIfNeeded(parentNoteId) {
|
||||
const sortReversed = parentNote.getLabelValue('sortDirection')?.toLowerCase() === "desc";
|
||||
const sortFoldersFirstLabel = parentNote.getLabel('sortFoldersFirst');
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user