Compare commits

...

29 Commits

Author SHA1 Message Date
zadam
15ed381f85 release 0.55.1 2022-09-21 22:43:34 +02:00
zadam
180051d252 added "scrolling-container" class, fixes #3147 2022-09-19 23:15:54 +02:00
zadam
a19c58703f Merge remote-tracking branch 'origin/master' 2022-09-19 23:12:20 +02:00
zadam
fc43d9222a fix erasing notes - becca should be reloaded afterwards, closes #3146 2022-09-19 23:12:12 +02:00
zadam
af6bf08243 Merge pull request #3143 from charlesdagenais/master
add file properties widget in mobile layout
2022-09-19 22:44:23 +02:00
zadam
fb6a0bc2a6 Merge pull request #3142 from spasche/zip-import-skip-bad-url
let import continue when malformed URLs are encountered
2022-09-19 22:42:31 +02:00
Charles Dagenais
1f61c1b3b6 add file properties info in mobile view 2022-09-18 12:40:00 -04:00
Sylvain Pasche
fc69f3b8f3 let import continue when malformed URLs are encountered
In case of malformed URLs in imported HTML, keep original URL instead
of having the import get stuck.
2022-09-18 18:18:42 +02:00
zadam
d4658b9c2a bring back the possibility to close the floating buttons again, closes #3116 2022-09-18 14:57:44 +02:00
zadam
84f72edf1d use button CSS variables in more places 2022-09-18 13:52:19 +02:00
zadam
552d872047 more standard window-all-closed handling 2022-09-18 10:05:49 +02:00
zadam
47235965d5 api log close button 2022-09-18 08:48:01 +02:00
zadam
24e4455e91 Merge remote-tracking branch 'origin/master' 2022-09-17 23:06:43 +02:00
zadam
ea35b0c800 Merge branch 'api-log-capture' 2022-09-17 23:06:42 +02:00
zadam
1a30087426 api log implementation 2022-09-17 23:06:17 +02:00
zadam
5e9d004ca2 Merge pull request #3139 from jph/add-methods-to-etapi-spec
Add another ETAPI method to the api spec file.
2022-09-17 14:13:31 +02:00
jph
513d1c020c Add another ETAPI method to the api spec file. 2022-09-17 10:47:44 +10:00
zadam
05231bd1c2 stop #workspaceTemplate from being inherited (should have the same special behavior as #template) 2022-09-16 23:03:02 +02:00
zadam
b816773d02 add #workspaceTemplate which works as workspace-scoped template, closes #3137 2022-09-16 22:44:52 +02:00
zadam
3c49ea6cb1 make context sub-menu scrollable, fix #3136 2022-09-16 22:12:09 +02:00
zadam
539eac4be7 Merge pull request #3134 from jph/add-note-content-to-api-spec
Add get note content to ETAPI spec
2022-09-16 21:08:53 +02:00
James Hynes
8a6ead6d86 fix a couple of api shortcomings
* add note content to openapi spec file
* add test for get note content api endpoint
2022-09-16 13:18:01 +10:00
zadam
01a7ed8311 fix zooming in server install 2022-09-15 23:10:54 +02:00
zadam
cf6330dee6 allow deleting notes from note actions button, closes #3131 2022-09-15 23:09:24 +02:00
zadam
6c39b6f548 fix "excludeFromNoteMap" on journal instead of "excludeFromTreeMap" 2022-09-15 00:04:26 +02:00
zadam
e7ef1b86cc zoom buttons in main menu, closes #2894 2022-09-14 23:28:29 +02:00
zadam
3663d56917 api log capture WIP 2022-09-14 16:50:52 +02:00
zadam
135064a18f drag & drop from tree will insert links to notes, closes #227 2022-09-13 23:36:59 +02:00
zadam
7233f58767 flatpak release script fixes 2022-09-12 23:14:00 +02:00
47 changed files with 532 additions and 82 deletions

View File

@@ -13,6 +13,9 @@ then
exit 1
fi
VERSION_DATE=$(git log -1 --format=%aI "v${VERSION}" | cut -c -10)
VERSION_COMMIT=$(git rev-list -n 1 "v${VERSION}")
# expecting the directory at a specific path
cd ~/trilium-flathub
@@ -21,7 +24,7 @@ if ! git diff-index --quiet HEAD --; then
exit 1
fi
if [[ $VERSION == *"beta"* ]]; then
if [[ "$VERSION" == *"beta"* ]]; then
git switch beta
else
git switch master
@@ -29,9 +32,6 @@ fi
git pull
VERSION_DATE=$(git log -1 --format=%aI v${VERSION} | cut -c -10)
VERSION_COMMIT=$(git rev-list -n 1 v${VERSION})
echo "Updating files with version ${VERSION}, date ${VERSION_DATE} and commit ${VERSION_COMMIT}"
flatpak-node-generator npm ../trilium/package-lock.json

Binary file not shown.

View File

@@ -13,11 +13,12 @@ appIconService.installLocalAppIcon();
require('electron-dl')({ saveAs: true });
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform === 'win32') {
app.exit(0); // attempt to fix the issue when app.quit() won't terminate processes on windows
} else {
app.quit();
if (process.platform !== 'darwin') {
app.quit()
}
});

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.55.0-beta",
"version": "0.55.1",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {

View File

@@ -27,6 +27,8 @@ class Becca {
/** @type {Object.<String, EtapiToken>} */
this.etapiTokens = {};
this.dirtyNoteSetCache();
this.loaded = false;
}

View File

@@ -403,7 +403,7 @@ class Note extends AbstractEntity {
templateAttributes.push(
...templateNote.__getAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited
.filter(attr => !(attr.type === 'label' && attr.name === 'template'))
.filter(attr => !(attr.type === 'label' && (attr.name === 'template' || attr.name === 'workspacetemplate')))
);
}
}

View File

@@ -228,6 +228,36 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/notes/{noteId}/content:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns note content idenfied by its ID
operationId: getNoteContent
responses:
'200':
description: note content response
content:
text/html:
schema:
type: string
put:
description: Updates note content idenfied by its ID
operationId: putNoteContentById
requestBody:
description: html content of note
required: true
content:
text/plain:
schema:
type: string
responses:
'204':
description: note content updated
/notes/{noteId}/export:
parameters:
- name: noteId

View File

@@ -267,7 +267,7 @@ class NoteShort {
attrArrs.push(
templateNote.__getCachedAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited
.filter(attr => !(attr.type === 'label' && attr.name === 'template'))
.filter(attr => !(attr.type === 'label' && (attr.name === 'template' || attr.name === 'workspacetemplate')))
);
}
}

View File

@@ -79,6 +79,8 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js";
import EditableCodeButtonsWidget from "../widgets/type_widgets/editable_code_buttons.js";
import ApiLogWidget from "../widgets/api_log.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -185,6 +187,7 @@ export default class DesktopLayout {
.child(new RelationMapButtons())
.child(new MermaidExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(
@@ -197,6 +200,7 @@ export default class DesktopLayout {
.child(new SqlResultWidget())
)
.child(new EditableCodeButtonsWidget())
.child(new ApiLogWidget())
.child(new FindWidget())
.child(
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC

View File

@@ -10,6 +10,7 @@ import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
import ConfirmDialog from "../widgets/dialogs/confirm.js";
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
const MOBILE_CSS = `
<style>
@@ -128,7 +129,7 @@ export default class MobileLayout {
.child(
new NoteDetailWidget()
.css('padding', '5px 20px 10px 0')
)
).child(new FilePropertiesWidget().css('font-size','smaller'))
)
)
.child(new ProtectedSessionPasswordDialog())

View File

@@ -68,7 +68,7 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
}
}
async function deleteNotes(branchIdsToDelete) {
async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
branchIdsToDelete = filterRootNote(branchIdsToDelete);
if (branchIdsToDelete.length === 0) {
@@ -83,7 +83,7 @@ async function deleteNotes(branchIdsToDelete) {
}
else {
({proceed, deleteAllClones, eraseNotes} = await new Promise(res =>
appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res})));
appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res, forceDeleteAllClones})));
}
if (!proceed) {

View File

@@ -13,6 +13,7 @@ import appContext from "./app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import NoteContextCachingWidget from "../widgets/note_context_caching_widget.js";
import BasicWidget from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js";
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -594,6 +595,33 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @returns {string} random string
*/
this.randomString = utils.randomString;
this.logMessages = {};
this.logSpacedUpdates = {};
/**
* Log given message to the log pane in UI
*
* @param message
*/
this.log = message => {
const {noteId} = this.startNote;
message = utils.now() + ": " + message;
console.log(`Script ${noteId}: ${message}`);
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
appContext.triggerEvent("apiLogMessages", {noteId, messages});
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
};
}
export default FrontendScriptApi;

View File

@@ -3,6 +3,7 @@ import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
import appContext from "./app_context.js";
const messageHandlers = [];
@@ -118,6 +119,9 @@ async function handleMessage(event) {
else if (message.type === 'consistency-checks-failed') {
toastService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
}
else if (message.type === 'api-log-messages') {
appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages});
}
}
let entityChangeIdReachedListeners = [];

View File

@@ -0,0 +1,75 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `
<div class="api-log-widget">
<style>
.api-log-widget {
padding: 15px;
flex-grow: 1;
max-height: 40%;
position: relative;
}
.hidden-api-log {
display: none;
}
.api-log-container {
overflow: auto;
height: 100%;
}
.close-api-log-button {
padding: 5px;
border: 1px solid var(--button-border-color);
background-color: var(--button-background-color);
border-radius: var(--button-border-radius);
color: var(--button-text-color);
position: absolute;
top: 10px;
right: 40px;
cursor: pointer;
}
</style>
<div class="bx bx-x close-api-log-button" title="Close"></div>
<div class="api-log-container"></div>
</div>`;
export default class ApiLogWidget extends NoteContextAwareWidget {
isEnabled() {
return this.note
&& this.note.mime.startsWith('application/javascript;env=')
&& super.isEnabled();
}
doRender() {
this.$widget = $(TPL);
this.toggle(false);
this.$logContainer = this.$widget.find('.api-log-container');
this.$closeButton = this.$widget.find(".close-api-log-button");
this.$closeButton.on("click", () => this.toggle(false));
}
async refreshWithNote(note) {
this.$logContainer.empty();
}
apiLogMessagesEvent({messages, noteId}) {
if (!this.isNote(noteId)) {
return;
}
this.toggle(true);
for (const message of messages) {
this.$logContainer.append(message).append($("<br>"));
}
}
toggle(show) {
this.$widget.toggleClass("hidden-api-log", !show);
}
}

View File

@@ -207,6 +207,7 @@ const ATTR_HELP = {
"workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note",
"workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note",
"workspaceCalendarRoot": "Defines per-workspace calendar root",
"workspaceTemplate": "This note will appear in the selection of available template when creating new note, but only when hoisted into a workspace containing this template",
"searchHome": "new search notes will be created as children of this note",
"hoistedSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this note",
"inbox": "default inbox location for new notes",

View File

@@ -57,8 +57,10 @@ const TPL = `
}
.add-new-attribute-button:hover, .save-attributes-button:hover {
border: 1px solid var(--main-border-color);
border-radius: 2px;
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
}
.attribute-errors {

View File

@@ -8,6 +8,28 @@ class BasicWidget extends Component {
style: ''
};
this.classes = [];
this.children = [];
this.childPositionCounter = 10;
}
child(...components) {
if (!components) {
return this;
}
super.child(...components);
for (const component of components) {
if (component.position === undefined) {
component.position = this.childPositionCounter;
this.childPositionCounter += 10;
}
}
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
return this;
}
id(id) {

View File

@@ -35,13 +35,31 @@ const TPL = `
bottom: -30px;
width: 100%;
height: 100%;
pointer-events: none;
}
.update-to-latest-version-button {
display: none;
}
.global-menu .zoom-buttons a {
display: inline-block;
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
color: var(--button-text-color);
background-color: var(--button-background-color);
padding: 3px;
margin-left: 3px;
}
.global-menu .zoom-buttons a:hover {
text-decoration: none;
}
.global-menu .zoom-state {
margin-left: 5px;
margin-right: 5px;
}
</style>
<button type="button" data-toggle="dropdown" data-placement="right"
@@ -92,10 +110,29 @@ const TPL = `
<kbd data-command="reloadFrontendApp"></kbd>
</a>
<span class="zoom-container dropdown-item" style="display: flex; flex-direction: row; justify-content: space-between;">
<div>
<span class="bx bx-empty"></span>
Zoom
</div>
<div class="zoom-buttons">
<a data-trigger-command="toggleFullscreen" title="Toggle fullscreen" class="bx bx-expand-alt"></a>
&nbsp;
<a data-trigger-command="zoomOut" title="Zoom out" class="bx bx-minus"></a>
<span class="zoom-state"></span>
<a data-trigger-command="zoomIn" title="Zoom in" class="bx bx-plus"></a>
</div>
</span>
<a class="dropdown-item" data-trigger-command="toggleFullscreen">
<span class="bx bx-empty"></span>
Toggle fullscreen
<kbd data-command="toggleFullscreen"></kbd>
<kbd ></kbd>
</a>
<a class="dropdown-item" data-trigger-command="showHelp">
@@ -145,9 +182,13 @@ export default class GlobalMenuWidget extends BasicWidget {
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron);
this.$widget.on('click', '.dropdown-item', e => {
if ($(e.target).parent(".zoom-buttons")) {
return;
}
this.$widget.on('click', '.dropdown-item',
() => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
this.$widget.find("[data-toggle='dropdown']").dropdown('toggle');
});
this.$widget.find(".global-menu-button-update-available").append(
this.updateAvailableWidget.render()
@@ -155,11 +196,34 @@ export default class GlobalMenuWidget extends BasicWidget {
this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button");
if (!utils.isElectron()) {
this.$widget.find(".zoom-container").hide();
}
this.$zoomState = this.$widget.find(".zoom-state");
this.$widget.on('show.bs.dropdown', () => this.updateZoomState());
this.$widget.find(".zoom-buttons").on("click",
// delay to wait for the actual zoom change
() => setTimeout(() => this.updateZoomState(), 300)
);
this.updateVersionStatus();
setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000);
}
updateZoomState() {
if (!utils.isElectron()) {
return;
}
const zoomFactor = utils.dynamicRequire('electron').webFrame.getZoomFactor();
const zoomPercent = Math.round(zoomFactor * 100);
this.$zoomState.text(zoomPercent + "%");
}
async updateVersionStatus() {
if (options.get("checkForUpdates") !== 'true') {
return;

View File

@@ -1,5 +1,6 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import utils from "../../services/utils.js";
import branchService from "../../services/branches.js";
const TPL = `
<div class="dropdown note-actions">
@@ -30,6 +31,7 @@ const TPL = `
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
<a class="dropdown-item import-files-button">Import files</a>
<a class="dropdown-item export-note-button">Export note</a>
<a class="dropdown-item delete-note-button">Delete note</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
</div>
</div>`;
@@ -65,6 +67,15 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
this.$deleteNoteButton = this.$widget.find(".delete-note-button");
this.$deleteNoteButton.on("click", () => {
if (this.note.noteId === 'root') {
return;
}
branchService.deleteNotes([this.note.getParentBranches()[0].branchId], true);
});
}
refreshWithNote(note) {

View File

@@ -8,7 +8,7 @@ const TPL = `
height: 21px !important;
padding: 0 !important;
border-radius: 8px;
border-radius: var(--button-border-radius);
transform: scale(0.9);
border: none;
opacity: 0.8;

View File

@@ -1,33 +1,6 @@
import BasicWidget from "../basic_widget.js";
export default class Container extends BasicWidget {
constructor() {
super();
this.children = [];
this.positionCounter = 10;
}
child(...components) {
if (!components) {
return this;
}
super.child(...components);
for (const component of components) {
if (component.position === undefined) {
component.position = this.positionCounter;
this.positionCounter += 10;
}
}
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
return this;
}
doRender() {
this.$widget = $(`<div>`);

View File

@@ -4,6 +4,7 @@ export default class ScrollingContainer extends Container {
constructor() {
super();
this.class("scrolling-container");
this.css('overflow', 'auto');
this.css('position', 'relative');
}

View File

@@ -3,7 +3,7 @@ import BasicWidget from "../basic_widget.js";
const DELETE_NOTE_BUTTON_CLASS = "confirm-dialog-delete-note";
const TPL = `
<div class="confirm-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="confirm-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">

View File

@@ -97,7 +97,7 @@ export default class DeleteNotesDialog extends BasicWidget {
this.resolve({
proceed: true,
deleteAllClones: this.isDeleteAllClonesChecked(),
deleteAllClones: this.forceDeleteAllClones || this.isDeleteAllClonesChecked(),
eraseNotes: this.isEraseNotesChecked()
});
});
@@ -108,7 +108,7 @@ export default class DeleteNotesDialog extends BasicWidget {
async renderDeletePreview() {
const response = await server.post('delete-notes-preview', {
branchIdsToDelete: this.branchIds,
deleteAllClones: this.isDeleteAllClonesChecked()
deleteAllClones: this.forceDeleteAllClones || this.isDeleteAllClonesChecked()
});
this.$deleteNotesList.empty();
@@ -143,14 +143,18 @@ export default class DeleteNotesDialog extends BasicWidget {
}
}
async showDeleteNotesDialogEvent({branchIdsToDelete, callback}) {
async showDeleteNotesDialogEvent({branchIdsToDelete, callback, forceDeleteAllClones}) {
this.branchIds = branchIdsToDelete;
this.forceDeleteAllClones = forceDeleteAllClones;
await this.renderDeletePreview();
utils.openDialog(this.$widget);
this.$deleteAllClones.prop("checked", false);
this.$deleteAllClones
.prop("checked", !!forceDeleteAllClones)
.prop("disabled", !!forceDeleteAllClones);
this.$eraseNotes.prop("checked", false);
this.resolve = callback;

View File

@@ -2,7 +2,7 @@ import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
const TPL = `
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">

View File

@@ -37,11 +37,11 @@ const TPL = `
margin-right: 20px;
font-size: large;
border: 1px solid transparent;
border-radius: 5px;
border-radius: var(--button-border-radius);
}
.token-table-button:hover {
border: 1px solid var(--main-border-color);
border: 1px solid var(--button-border-color);
}
</style>`;

View File

@@ -2,10 +2,10 @@ import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js";
const TPL = `
<div class="prompt-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="prompt-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="prompt-dialog-form">
<form class="prompt-dialog-form">
<div class="modal-header">
<h5 class="prompt-title modal-title mr-auto">Prompt</h5>
@@ -34,7 +34,7 @@ export default class PromptDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.$dialogBody = this.$widget.find(".modal-body");
this.$form = this.$widget.find("#prompt-dialog-form");
this.$form = this.$widget.find(".prompt-dialog-form");
this.$question = null;
this.$answer = null;

View File

@@ -1,4 +1,4 @@
import Container from "../containers/container.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `
<div class="floating-buttons">
@@ -16,7 +16,7 @@ const TPL = `
z-index: 100;
}
.floating-buttons-children > * {
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
margin-left: 10px;
}
@@ -24,12 +24,16 @@ const TPL = `
font-size: 130%;
padding: 5px 10px 4px 10px;
}
.floating-buttons.temporarily-hidden {
display: none;
}
</style>
<div class="floating-buttons-children"></div>
</div>`;
export default class FloatingButtons extends Container {
export default class FloatingButtons extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.$children = this.$widget.find(".floating-buttons-children");
@@ -38,4 +42,16 @@ export default class FloatingButtons extends Container {
this.$children.append(widget.render());
}
}
async refreshWithNote(note) {
this.toggle(true);
}
toggle(show) {
this.$widget.toggleClass("temporarily-hidden", !show);
}
hideFloatingButtonsCommand() {
this.toggle(false);
}
}

View File

@@ -0,0 +1,42 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `
<div class="close-floating-buttons">
<style>
.close-floating-buttons {
display: none;
margin-left: 5px !important;
}
/* conditionally display close button if there's some other button visible */
.floating-buttons *:not(.hidden-int):not(.hidden-no-content) ~ .close-floating-buttons {
display: block;
}
.close-floating-buttons-button {
border: 1px solid transparent;
color: var(--button-text-color);
padding: 6px;
border-radius: 100px;
}
.close-floating-buttons-button:hover {
border: 1px solid var(--button-border-color);
}
</style>
<button type="button"
class="close-floating-buttons-button btn bx bx-x no-print"
title="Hide buttons"></button>
</div>
`;
export default class HideFloatingButtonsButton extends NoteContextAwareWidget {
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.on('click', () => this.triggerCommand('hideFloatingButtons'));
this.contentSized();
}
}

View File

@@ -1,7 +1,4 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import dialogService from "../dialog.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
const TPL = `
<button type="button"

View File

@@ -89,17 +89,21 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
const resp = await server.get(`notes/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
this.$ticker.hide();
this.toggle(false);
return;
}
this.$ticker.show();
this.$ticker.toggle(true);
this.$count.text(
`${resp.count} backlink`
+ (resp.count === 1 ? '' : 's')
);
}
toggle(show) {
this.$widget.toggleClass("hidden-no-content", !show);
}
clearItems() {
this.$items.empty().hide();
}

View File

@@ -13,6 +13,7 @@ import appContext from "../services/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js";
import linkService from "../services/link.js";
import syncService from "../services/sync.js";
import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
@@ -393,6 +394,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
title: node.title
}));
if (notes.length === 1) {
linkService.createNoteLink(notes[0].noteId, {referenceLink: true})
.then($link => data.dataTransfer.setData("text/html", $link[0].outerHTML));
}
else {
Promise.all(notes.map(note => linkService.createNoteLink(note.noteId, {referenceLink: true}))).then(links => {
const $list = $("<ul>").append(...links.map($link => $("<li>").append($link)));
data.dataTransfer.setData("text/html", $list[0].outerHTML);
});
}
data.dataTransfer.setData("text", JSON.stringify(notes));
return true; // allow dragging to start
},
@@ -933,7 +946,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (this.noteContext
&& this.noteContext.notePath
&& !this.noteContext.note.isDeleted
&& !this.noteContext.note?.isDeleted
&& !this.noteContext.notePath.includes("root/hidden")
) {
const newActiveNode = await this.getNodeFromPath(this.noteContext.notePath);

View File

@@ -16,7 +16,10 @@ const TPL = `
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
border-radius: 0.2rem;
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
}
.sql-console-result-container {

View File

@@ -93,7 +93,7 @@ const TAB_ROW_TPL = `
.note-new-tab:hover {
background-color: var(--accented-background-color);
border-radius: 5px;
border-radius: var(--button-border-radius);
}
.tab-row-filler {

View File

@@ -87,4 +87,10 @@ export default class EditableCodeButtonsWidget extends NoteContextAwareWidget {
this.$openTriliumApiDocsButton.toggle(note.mime.startsWith('application/javascript;env='));
}
async noteTypeMimeChangedEvent({noteId}) {
if (this.isNote(noteId)) {
await this.refresh();
}
}
}

View File

@@ -53,11 +53,12 @@ const TPL = `
padding: 5px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 5px;
border-radius: var(--button-border-radius);
color: var(--button-text-color);
}
.edit-text-note-button:hover {
border-color: var(--main-border-color);
border-color: var(--button-border-color);
}
</style>

View File

@@ -109,7 +109,6 @@ button.close:hover {
.icon-action {
border: 1px solid transparent;
border-radius: 3px;
padding: 5px;
width: 35px;
height: 35px;
@@ -117,6 +116,7 @@ button.close:hover {
font-size: 1.5em;
color: var(--button-text-color);
background: var(--button-background-color);
border-radius: var(--button-border-radius);
}
.icon-action:hover:not(.disabled) {
@@ -755,6 +755,9 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
left: 100%;
margin-top: -6px;
min-width: 15rem;
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
max-height: 600px;
overflow: auto;
}
/* rotate caret on hover */
@@ -954,3 +957,7 @@ button.close:hover {
text-shadow: none;
color: currentColor;
}
.hidden-no-content {
display: none;
}

View File

@@ -6,6 +6,7 @@ const log = require('../../services/log');
const scriptService = require('../../services/script');
const searchService = require('../../services/search/services/search');
const bulkActionService = require("../../services/bulk_actions");
const cls = require("../../services/cls");
const {formatAttrForSearch} = require("../../services/attribute_formatter");
function searchFromNoteInt(note) {
@@ -189,7 +190,9 @@ function getRelatedNotes(req) {
}
function searchTemplates() {
const query = formatAttrForSearch({type: 'label', name: "template"}, false);
const query = cls.getHoistedNoteId() === 'root'
? '#template'
: '#template OR #workspaceTemplate';
return searchService.searchNotes(query, {
includeArchivedNotes: true,

View File

@@ -14,6 +14,8 @@ const appInfo = require('./app_info');
const searchService = require('./search/services/search');
const SearchContext = require("./search/search_context");
const becca = require("../becca/becca");
const ws = require("./ws");
const SpacedUpdate = require("./spaced_update");
/**
* This is the main backend API interface for scripts. It's published in the local "api" object.
@@ -288,12 +290,34 @@ function BackendScriptApi(currentNote, apiParams) {
});
};
this.logMessages = {};
this.logSpacedUpdates = {};
/**
* Log given message to trilium logs.
* Log given message to trilium logs and log pane in UI
*
* @param message
*/
this.log = message => log.info(message);
this.log = message => {
log.info(message);
const {noteId} = this.startNote;
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
ws.sendMessageToAllClients({
type: 'api-log-messages',
noteId,
messages
});
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
};
/**
* Returns root note of the calendar.

View File

@@ -1 +1 @@
module.exports = { buildDate:"2022-09-12T22:37:41+02:00", buildRevision: "ca03c412057da043f240689e37bae049d8311891" };
module.exports = { buildDate:"2022-09-21T22:43:34+02:00", buildRevision: "180051d252d254a2543f8192a28e87e134594a38" };

View File

@@ -30,6 +30,7 @@ module.exports = [
{ type: 'label', name: 'workspaceIconClass' },
{ type: 'label', name: 'workspaceTabBackgroundColor' },
{ type: 'label', name: 'workspaceCalendarRoot' },
{ type: 'label', name: 'workspaceTemplate' },
{ type: 'label', name: 'searchHome' },
{ type: 'label', name: 'hoistedInbox' },
{ type: 'label', name: 'hoistedSearchHome' },

View File

@@ -292,7 +292,12 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
content = content.replace(/<\/body>.*<\/html>/gis, "");
content = content.replace(/src="([^"]*)"/g, (match, url) => {
try {
url = decodeURIComponent(url);
} catch (e) {
log.error(`Cannot parse image URL '${url}', keeping original (${e}).`);
return `src="${url}"`;
}
if (isUrlAbsolute(url) || url.startsWith("/")) {
return match;
@@ -304,7 +309,12 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
});
content = content.replace(/href="([^"]*)"/g, (match, url) => {
try {
url = decodeURIComponent(url);
} catch (e) {
log.error(`Cannot parse link URL '${url}', keeping original (${e}).`);
return `href="${url}"`;
}
if (isUrlAbsolute(url)) {
return match;

View File

@@ -735,6 +735,10 @@ function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
eraseAttributes(attributeIdsToErase);
if (noteIdsToErase.length > 0 || branchIdsToErase.length > 0 || attributeIdsToErase.length > 0) {
require('../becca/becca_loader').reload();
}
});
}
@@ -750,6 +754,10 @@ function eraseNotesWithDeleteId(deleteId) {
const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE deleteId = ?", [deleteId]);
eraseAttributes(attributeIdsToErase);
if (noteIdsToErase.length > 0 || branchIdsToErase.length > 0 || attributeIdsToErase.length > 0) {
require('../becca/becca_loader').reload();
}
}
function eraseDeletedNotesNow() {

View File

@@ -28,7 +28,7 @@ class AttributeExistsExp extends Expression {
}
else if (note.isTemplate() &&
// template attr is used as a marker for templates, but it's not meant to be inherited
!(this.attributeType === 'label' && this.attributeName === 'template')) {
!(this.attributeType === 'label' && (this.attributeName === 'template' || this.attributeName === 'workspacetemplate'))) {
resultNoteSet.addAll(note.getTemplatedNotes());
}
else {

View File

@@ -0,0 +1,67 @@
class SpacedUpdate {
constructor(updater, updateInterval = 1000) {
this.updater = updater;
this.lastUpdated = Date.now();
this.changed = false;
this.updateInterval = updateInterval;
}
scheduleUpdate() {
if (!this.changeForbidden) {
this.changed = true;
setTimeout(() => this.triggerUpdate());
}
}
async updateNowIfNecessary() {
if (this.changed) {
this.changed = false; // optimistic...
try {
await this.updater();
}
catch (e) {
this.changed = true;
throw e;
}
}
}
isAllSavedAndTriggerUpdate() {
const allSaved = !this.changed;
this.updateNowIfNecessary();
return allSaved;
}
triggerUpdate() {
if (!this.changed) {
return;
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
this.updater();
this.lastUpdated = Date.now();
this.changed = false;
}
else {
// update not triggered but changes are still pending so we need to schedule another check
this.scheduleUpdate();
}
}
async allowUpdateWithoutChange(callback) {
this.changeForbidden = true;
try {
await callback();
}
finally {
this.changeForbidden = false;
}
}
}
module.exports = SpacedUpdate;

View File

@@ -67,7 +67,7 @@ function sendMessageToAllClients(message) {
const jsonStr = JSON.stringify(message);
if (webSocketServer) {
if (message.type !== 'sync-failed') {
if (message.type !== 'sync-failed' && message.type !== 'api-log-messages') {
log.info("Sending message to all clients: " + jsonStr);
}

View File

@@ -0,0 +1,25 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "text",
"content": "Hi there!"
}
> {%
client.global.set("createdNoteId", response.body.note.noteId);
client.global.set("createdBranchId", response.body.branch.branchId);
%}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
client.assert(response.body === "<p>Hi there!</p>");
%}