\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.html
index c8dc3a86b..0488c7d36 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.html
@@ -8,12 +8,12 @@
Interaction
The contextual menu can operate:
-
On a single note, by right clicking it in the note tree.
On multiple notes, by selecting them first. See Multiple selection on how to do
so.
-
When right clicking, do note that usually the note being right clicked
+
When right clicking, do note that usually the note being right clicked
is also included in the affected notes, regardless of whether it was selected
or not.
@@ -25,133 +25,146 @@
The ones that do support multiple notes will mention this in the list below.
If the note is already archived, it will be unarchived instead.
+
Multiple notes can be selected as well. However, all the selected notes
+ must be in the same state (archived or not), otherwise the option will
+ be disabled.
-
-
Import into note
-
-
Opens the import dialog and places
- the imported notes as child notes of the selected one.
Copies a URL fragment representing the full path to this branch for a
- note, such as #root/Hb2E70L7HPuf/4sRFgMZhYFts/2IVuShedRJ3U/LJVMvKXOFv7n.
-
The URL to manually create Links within
- notes, or for note Navigation.
+
+
Copy note path to clipboard
+
+
Copies a URL fragment representing the full path to this branch for a
+ note, such as #root/Hb2E70L7HPuf/4sRFgMZhYFts/2IVuShedRJ3U/LJVMvKXOFv7n.
+
The URL to manually create Links within
+ notes, or for note Navigation.
-
-
Recent changes in subtree
-
-
This will open Recent Changes,
- but filtered to only the changes related to this note or one of its descendants.
-
-
+
+
Recent changes in subtree
+
+
This will open Recent Changes,
+ but filtered to only the changes related to this note or one of its descendants.
+
+
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Quick edit.html
similarity index 100%
rename from apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit.html
rename to apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Quick edit.html
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit_image.png b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Quick edit_image.png
similarity index 100%
rename from apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit_image.png
rename to apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Quick edit_image.png
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections.html
index 7b255c880..8ec44b5ad 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections.html
@@ -4,29 +4,31 @@
child notes into one continuous view. This makes it ideal for reading extensive
information broken into smaller, manageable segments.
Grid View which
is the default presentation method for child notes (see Note List), where the notes are displayed
as tiles with their title and content being visible.
List View is
similar to Grid View,
but it displays the notes one under the other with the content being expandable/collapsible,
but also works recursively.
More specialized collections were introduced, such as the:
Calendar View which
displays a week, month or year calendar with the notes being shown as events.
New events can be added easily by dragging across the calendar.
Geo Map View which
displays a geographical map in which the notes are represented as markers/pins
on the map. New events can be easily added by pointing on the map.
-
Table View displays
- each note as a row in a table, with Promoted Attributes being
- shown as well. This makes it easy to visualize attributes of notes, as
- well as making them easily editable.
-
Board View (Kanban)
- displays notes in columns, grouped by the value of a label.
+
Table View displays
+ each note as a row in a table, with Promoted Attributes being
+ shown as well. This makes it easy to visualize attributes of notes, as
+ well as making them easily editable.
+
Board View (Kanban)
+ displays notes in columns, grouped by the value of a label.
For a quick presentation of all the supported view types, see the child
notes of this help page, including screenshots.
@@ -42,8 +44,8 @@
Adding a description to a collection
To add a text before the collection, for example to describe it:
In the Ribbon,
go to Basic Properties and select Collection as the note
type.
-
Still in the ribbon, go to Collection Properties and select the
+
Still in the ribbon, go to Collection Properties and select the
desired view type.
-
Consult the help page of the corresponding view type in order to understand
+
Consult the help page of the corresponding view type in order to understand
how to configure them.
+
Archived notes
+
By default, archived notes will not be shown in collections. This behaviour
+ can be changed by going to Collection Properties in the
+ Ribbon and checking Show archived notes.
+
Archived notes will be generally indicated by being greyed out as opposed
+ to the normal ones.
Under the hood
Collections by themselves are simply notes with no content that rely on
the Note List mechanism
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Board View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Board View.html
index 3c13a81d8..6dc4e1029 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Board View.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Board View.html
@@ -15,53 +15,60 @@
in a hierarchy.
Interaction with columns
-
Create a new column by pressing Add Column near the last column.
+
Create a new column by pressing Add Column near the last column.
-
Once pressed, a text box will be displayed to set the name of the column.
- Press Enter to confirm.
+
Once pressed, a text box will be displayed to set the name of the column.
+ Press Enter to confirm, or Escape to dismiss.
-
To reorder a column, simply hold the mouse over the title and drag it
+
To reorder a column, simply hold the mouse over the title and drag it
to the desired position.
-
To delete a column, right click on its title and select Delete column.
-
To rename a column, click on the note title.
+
To delete a column, right click on its title and select Delete column.
+
To rename a column, click on the note title.
-
Press Enter to confirm.
-
Upon renaming a column, the corresponding status attribute of all its
+
Press Enter to confirm.
+
Upon renaming a column, the corresponding status attribute of all its
notes will be changed in bulk.
-
-
If there are many columns, use the mouse wheel to scroll.
+
+
If there are many columns, use the mouse wheel to scroll.
Interaction with notes
-
Create a new note in any column by pressing New item
+
Create a new note in any column by pressing New item
-
Enter the name of the note and press Enter.
-
Doing so will create a new note. The new note will have an attribute (status label
+
Enter the name of the note and press Enter or click away. To
+ dismiss the creation of a new note, simply press Escape or leave
+ the name empty.
+
Once created, the new note will have an attribute (status label
by default) set to the name of the column.
-
To change the state of a note, simply drag a note from one column to the
+
To open the note, simply click on it.
+
To change the title of the note directly from the board, hover the mouse
+ over its card and press the edit button on the right.
+
To change the state of a note, simply drag a note from one column to the
other to change its state.
-
The order of the notes in each column corresponds to their position in
+
The order of the notes in each column corresponds to their position in
the tree.
-
It's possible to reorder notes simply by dragging them to the desired
+
It's possible to reorder notes simply by dragging them to the desired
position within the same columns.
-
It's also possible to drag notes across columns, at the desired position.
+
It's also possible to drag notes across columns, at the desired position.
-
For more options, right click on a note to display a context menu with
+
For more options, right click on a note to display a context menu with
the following options:
-
Open the note in a new tab/split/window or quick edit.
-
Move the note to any column.
-
Insert a new note above/below the current one.
-
Delete the current note.
+
Open the note in a new tab/split/window or quick edit.
+
Move the note to any column.
+
Insert a new note above/below the current one.
+
Archive/unarchive the current note.
+
Delete the current note.
-
If there are many notes within the column, move the mouse over the column
+
If there are many notes within the column, move the mouse over the column
and use the mouse wheel to scroll.
Configuration
@@ -77,5 +84,5 @@ class="admonition note">
Interaction
Limitations
-
It is not possible yet to use group by a relation, only by label.
+
It is not possible yet to use group by a relation, only by label.
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Geo Map View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Geo Map View.html
index cbda5b977..9e0d2cf9c 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Geo Map View.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Geo Map View.html
@@ -12,128 +12,138 @@
on an attribute. It is also possible to add new notes at a specific location
using the built-in interface.
Creating a new geo map
-
-
-
-
-
-
-
-
-
-
-
1
-
-
-
-
-
-
Right click on any note on the note tree and select Insert child note → Geo Map (beta).
-
-
-
2
-
-
-
-
-
-
By default the map will be empty and will show the entire world.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+
Right click on any note on the note tree and select Insert child note → Geo Map (beta).
+
+
+
2
+
+
+
+
+
+
By default the map will be empty and will show the entire world.
+
+
+
+
Repositioning the map
-
Click and drag the map in order to move across the map.
-
Use the mouse wheel, two-finger gesture on a touchpad or the +/- buttons
+
Click and drag the map in order to move across the map.
+
Use the mouse wheel, two-finger gesture on a touchpad or the +/- buttons
on the top-left to adjust the zoom.
The position on the map and the zoom are saved inside the map note and
restored when visiting again the note.
Adding a marker using the map
Adding a new note using the plus button
-
-
-
-
-
-
-
-
-
-
-
1
-
To create a marker, first navigate to the desired point on the map. Then
- press the
- button in the Floating buttons (top-right)
- area.
-
- If the button is not visible, make sure the button section is visible
- by pressing the chevron button (
- ) in the top-right of the map.
-
-
-
-
2
-
-
-
-
Once pressed, the map will enter in the insert mode, as illustrated by
- the notification.
-
- Simply click the point on the map where to place the marker, or the Escape
- key to cancel.
-
-
-
3
-
-
-
-
Enter the name of the marker/note to be created.
-
-
-
4
-
-
-
-
Once confirmed, the marker will show up on the map and it will also be
- displayed as a child note of the map.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
1
+
To create a marker, first navigate to the desired point on the map. Then
+ press the
+ button in the Floating buttons (top-right)
+ area.
+
+ If the button is not visible, make sure the button section is visible
+ by pressing the chevron button (
+ ) in the top-right of the map.
+
+
+
+
2
+
+
+
+
Once pressed, the map will enter in the insert mode, as illustrated by
+ the notification.
+
+ Simply click the point on the map where to place the marker, or the Escape
+ key to cancel.
+
+
+
3
+
+
+
+
Enter the name of the marker/note to be created.
+
+
+
4
+
+
+
+
Once confirmed, the marker will show up on the map and it will also be
+ displayed as a child note of the map.
+
+
+
+
Adding a new note using the contextual menu
-
Right click anywhere on the map, where to place the newly created marker
+
Right click anywhere on the map, where to place the newly created marker
(and corresponding note).
-
Select Add a marker at this location.
-
Enter the name of the newly created note.
-
The map should be updated with the new marker.
+
Select Add a marker at this location.
+
Enter the name of the newly created note.
+
The map should be updated with the new marker.
Adding an existing note on note from the note tree
Hold the mouse on the note and drag it to the map to the desired location.
+
The map should be updated with the new marker.
This works for:
-
Notes that are not part of the geo map, case in which a clone will
+
Notes that are not part of the geo map, case in which a clone will
be created.
-
Notes that are a child of the geo map but not yet positioned on the map.
-
Notes that are a child of the geo map and also positioned, case in which
+
Notes that are a child of the geo map but not yet positioned on the map.
+
Notes that are a child of the geo map and also positioned, case in which
the marker will be relocated to the new position.
+
How the location of the markers is stored
The location of a marker is stored in the #geolocation attribute
of the child notes:
-
+
+
+
This value can be added manually if needed. The value of the attribute
is made up of the latitude and longitude separated by a comma.
Repositioning markers
@@ -145,16 +155,17 @@ height="278">
page (Ctrl+R ) to cancel it.
Interaction with the markers
-
Hovering over a marker will display a Note Tooltip with
+
Hovering over a marker will display a Note Tooltip with
the content of the note it belongs to.
-
Clicking on the note title in the tooltip will navigate to the note in
+
Clicking on the note title in the tooltip will navigate to the note in
the current view.
-
Middle-clicking the marker will open the note in a new tab.
-
Right-clicking the marker will open a contextual menu (as described below).
-
If the map is in read-only mode, clicking on a marker will open a
+
Middle-clicking the marker will open the note in a new tab.
+
Right-clicking the marker will open a contextual menu (as described below).
+
If the map is in read-only mode, clicking on a marker will open a
Quick edit popup for the corresponding note.
@@ -162,24 +173,24 @@ height="278">
It's possible to press the right mouse button to display a contextual
menu.
-
If right-clicking an empty section of the map (not on a marker), it allows
+
If right-clicking an empty section of the map (not on a marker), it allows
to:
-
Displays the latitude and longitude. Clicking this option will copy them
+
Displays the latitude and longitude. Clicking this option will copy them
to the clipboard.
-
Open the location using an external application (if the operating system
+
Open the location using an external application (if the operating system
supports it).
-
Adding a new marker at that location.
+
Adding a new marker at that location.
-
If right-clicking on a marker, it allows to:
+
If right-clicking on a marker, it allows to:
-
Displays the latitude and longitude. Clicking this option will copy them
+
Displays the latitude and longitude. Clicking this option will copy them
to the clipboard.
-
Open the location using an external application (if the operating system
+
Open the location using an external application (if the operating system
supports it).
-
Open the note in a new tab, split or window.
-
Remove the marker from the map, which will remove the #geolocation attribute
+
Open the note in a new tab, split or window.
+
Remove the marker from the map, which will remove the #geolocation attribute
of the note. To add it back again, the coordinates have to be manually
added back in.
@@ -199,209 +210,215 @@ height="278">
The value of the attribute is made up of the latitude and longitude separated
by a comma.
Adding from Google Maps
-
-
-
-
-
-
-
-
-
-
-
1
-
-
-
-
-
-
Go to Google Maps on the web and look for a desired location, right click
- on it and a context menu will show up.
-
- Simply click on the first item displaying the coordinates and they will
- be copied to clipboard.
-
- Then paste the value inside the text box into the #geolocation attribute
- of a child note of the map (don't forget to surround the value with a " character).
-
-
-
2
-
-
-
-
-
-
In Trilium, create a child note under the map.
-
-
-
3
-
-
-
-
-
-
And then go to Owned Attributes and type #geolocation=", then
- paste from the clipboard as-is and then add the ending " character.
- Press Enter to confirm and the map should now be updated to contain the
- new note.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+
Go to Google Maps on the web and look for a desired location, right click
+ on it and a context menu will show up.
+
+ Simply click on the first item displaying the coordinates and they will
+ be copied to clipboard.
+
+ Then paste the value inside the text box into the #geolocation attribute
+ of a child note of the map (don't forget to surround the value with a " character).
+
+
+
2
+
+
+
+
+
+
In Trilium, create a child note under the map.
+
+
+
3
+
+
+
+
+
+
And then go to Owned Attributes and type #geolocation=", then
+ paste from the clipboard as-is and then add the ending " character.
+ Press Enter to confirm and the map should now be updated to contain the
+ new note.
+
+
+
+
Adding from OpenStreetMap
Similarly to the Google Maps approach:
-
-
-
-
-
-
-
-
-
-
-
1
-
-
-
-
Go to any location on openstreetmap.org and right click to bring up the
- context menu. Select the “Show address” item.
-
-
-
2
-
-
-
-
The address will be visible in the top-left of the screen, in the place
- of the search bar.
-
- Select the coordinates and copy them into the clipboard.
-
-
-
3
-
-
-
-
Simply paste the value inside the text box into the #geolocation attribute
- of a child note of the map and then it should be displayed on the map.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
Go to any location on openstreetmap.org and right click to bring up the
+ context menu. Select the “Show address” item.
+
+
+
2
+
+
+
+
The address will be visible in the top-left of the screen, in the place
+ of the search bar.
+
+ Select the coordinates and copy them into the clipboard.
+
+
+
3
+
+
+
+
Simply paste the value inside the text box into the #geolocation attribute
+ of a child note of the map and then it should be displayed on the map.
+
+
+
+
Adding GPS tracks (.gpx)
Trilium has basic support for displaying GPS tracks on the geo map.
-
-
-
-
-
-
-
-
-
-
-
1
-
-
-
-
-
-
To add a track, simply drag & drop a .gpx file inside the geo map
- in the note tree.
-
-
-
2
-
-
-
-
-
-
In order for the file to be recognized as a GPS track, it needs to show
- up as application/gpx+xml in the File type field.
-
-
-
3
-
-
-
-
-
-
When going back to the map, the track should now be visible.
-
- The start and end points of the track are indicated by the two blue markers.
-
-
-
-
-
Read-only mode
-
When a map is in read-only all editing features will be disabled such
- as:
Editing from the contextual menu (removing locations or adding new items).
+
+
To enable read-only mode simply press the Lock icon from the
+ Floating buttons. To disable it, press the button again.
+
Configuration
+
Map Style
+
The styling of the map can be adjusted in the Collection Properties tab
+ in the Ribbon or
+ manually via the #map:style attribute.
+
The geo map comes with two different types of styles:
+
+
Raster styles
+
+
For these styles the map is represented as a grid of images at different
+ zoom levels. This is the traditional way OpenStreetMap used to work.
+
Zoom is slightly restricted.
+
Currently, the only raster theme is the original OpenStreetMap style.
-
-
Vector styles
-
-
Vector styles are not represented as images, but as geometrical shapes.
- This makes the rendering much smoother, especially when zooming and looking
- at the building edges, for example.
-
The map can be zoomed in much further.
-
These come both in a light and a dark version.
-
The vector styles come from VersaTiles,
- a free and open-source project providing map tiles based on OpenStreetMap.
-
-
-
-
-
Scale
-
Activating this option via the Ribbon or
- manually via #map:scale will display an indicator in the bottom-left
- of the scale of the map.
-
Troubleshooting
-
-
-
-
-
Grid-like artifacts on the map
-
This occurs if the application is not at 100% zoom which causes the pixels
- of the map to not render correctly due to fractional scaling. The only
- possible solution is to set the UI zoom at 100% (default keyboard shortcut
- is Ctrl+0).
\ No newline at end of file
+
+
Vector styles
+
+
Vector styles are not represented as images, but as geometrical shapes.
+ This makes the rendering much smoother, especially when zooming and looking
+ at the building edges, for example.
+
The map can be zoomed in much further.
+
These come both in a light and a dark version.
+
The vector styles come from VersaTiles,
+ a free and open-source project providing map tiles based on OpenStreetMap.
+
+
+
+
+
Scale
+
Activating this option via the Ribbon or
+ manually via #map:scale will display an indicator in the bottom-left
+ of the scale of the map.
+
Troubleshooting
+
+
+
+
Grid-like artifacts on the map
+
This occurs if the application is not at 100% zoom which causes the pixels
+ of the map to not render correctly due to fractional scaling. The only
+ possible solution is to set the UI zoom at 100% (default keyboard shortcut
+ is Ctrl+0).
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Table View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Table View.html
index 3c6e3fc9b..e256b4cb3 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Table View.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Collections/Table View.html
@@ -8,31 +8,31 @@
How it works
The tabular structure is represented as such:
-
Each child note is a row in the table.
-
If child rows also have children, they will be displayed under an expander
+
Each child note is a row in the table.
+
If child rows also have children, they will be displayed under an expander
(nested notes).
The editing will respect the type of the promoted attribute, by presenting
+
The editing will respect the type of the promoted attribute, by presenting
a normal text box, a number selector or a date selector for example.
-
It also possible to change the title of a note.
-
Editing relations is also possible
-
-
Simply click on a relation and it will become editable. Enter the text
- to look for a note and click on it.
-
To remove a relation, remove the title of the note from the text box and
- click outside the cell.
-
-
+
It also possible to change the title of a note.
+
Editing relations is also possible
+
+
Simply click on a relation and it will become editable. Enter the text
+ to look for a note and click on it.
+
To remove a relation, remove the title of the note from the text box and
+ click outside the cell.
+
+
Editing columns
It is possible to edit a column by right clicking it and selecting Edit column. This
@@ -114,18 +119,19 @@
href="#root/_help_oPVyFC7WL2Lp">Note Tree. However, it is possible
to sort the data by the values of a column:
-
To do so, simply click on a column.
-
To switch between ascending or descending sort, simply click again on
+
To do so, simply click on a column.
+
To switch between ascending or descending sort, simply click again on
the same column. The arrow next to the column will indicate the direction
of the sort.
-
To disable sorting and fall back to the original order, right click any
+
To disable sorting and fall back to the original order, right click any
column on the header and select Clear sorting.
Reordering and hiding columns
-
Columns can be reordered by dragging the header of the columns.
-
Columns can be hidden or shown by right clicking on a column and clicking
+
Columns can be reordered by dragging the header of the columns.
+
Columns can be hidden or shown by right clicking on a column and clicking
the item corresponding to the column.
If the parent note has #sorted, reordering will be disabled.
-
If using nested tables, then reordering will also be disabled.
-
Currently, it's possible to reorder notes even if column sorting is used,
- but the result might be inconsistent.
+
If the parent note has #sorted, reordering will be disabled.
+
If using nested tables, then reordering will also be disabled.
+
Currently, it's possible to reorder notes even if column sorting is used,
+ but the result might be inconsistent.
Nested trees
If the child notes of the collection also have their own child notes,
@@ -150,27 +158,27 @@
to a certain number of levels or even disable it completely. To do so,
either:
Columns are supported, by being defined as Promoted Attributes to the
diff --git a/apps/server/src/becca/entities/bbranch.ts b/apps/server/src/becca/entities/bbranch.ts
index 00e3ec4b7..cd50fe09b 100644
--- a/apps/server/src/becca/entities/bbranch.ts
+++ b/apps/server/src/becca/entities/bbranch.ts
@@ -137,13 +137,13 @@ class BBranch extends AbstractBeccaEntity {
*
* @returns true if note has been deleted, false otherwise
*/
- deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean {
+ deleteBranch(deleteId?: string, taskContext?: TaskContext<"deleteNotes">): boolean {
if (!deleteId) {
deleteId = utils.randomString(10);
}
if (!taskContext) {
- taskContext = new TaskContext("no-progress-reporting");
+ taskContext = new TaskContext("no-progress-reporting", "deleteNotes", null);
}
taskContext.increaseProgressCount();
diff --git a/apps/server/src/becca/entities/bnote.ts b/apps/server/src/becca/entities/bnote.ts
index 68c82702b..1a724b1b0 100644
--- a/apps/server/src/becca/entities/bnote.ts
+++ b/apps/server/src/becca/entities/bnote.ts
@@ -1512,7 +1512,7 @@ class BNote extends AbstractBeccaEntity {
*
* @param deleteId - optional delete identified
*/
- deleteNote(deleteId: string | null = null, taskContext: TaskContext | null = null) {
+ deleteNote(deleteId: string | null = null, taskContext: TaskContext<"deleteNotes"> | null = null) {
if (this.isDeleted) {
return;
}
@@ -1522,7 +1522,7 @@ class BNote extends AbstractBeccaEntity {
}
if (!taskContext) {
- taskContext = new TaskContext("no-progress-reporting");
+ taskContext = new TaskContext("no-progress-reporting", "deleteNotes", null);
}
// needs to be run before branches and attributes are deleted and thus attached relations disappear
diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts
index e7a1c0a1c..07d6a3e68 100644
--- a/apps/server/src/etapi/notes.ts
+++ b/apps/server/src/etapi/notes.ts
@@ -108,7 +108,7 @@ function register(router: Router) {
return res.sendStatus(204);
}
- note.deleteNote(null, new TaskContext("no-progress-reporting"));
+ note.deleteNote(null, new TaskContext("no-progress-reporting", "deleteNotes", null));
res.sendStatus(204);
});
@@ -153,7 +153,7 @@ function register(router: Router) {
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
}
- const taskContext = new TaskContext("no-progress-reporting");
+ const taskContext = new TaskContext("no-progress-reporting", "export", null);
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
@@ -164,7 +164,7 @@ function register(router: Router) {
eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
- const taskContext = new TaskContext("no-progress-reporting");
+ const taskContext = new TaskContext("no-progress-reporting", "importNotes", null);
zipImportService.importZip(taskContext, req.body, note).then((importedNote) => {
res.status(201).json({
diff --git a/apps/server/src/routes/api/branches.ts b/apps/server/src/routes/api/branches.ts
index 4cd570fcb..810deaba7 100644
--- a/apps/server/src/routes/api/branches.ts
+++ b/apps/server/src/routes/api/branches.ts
@@ -237,7 +237,7 @@ function deleteBranch(req: Request) {
const eraseNotes = req.query.eraseNotes === "true";
const branch = becca.getBranchOrThrow(req.params.branchId);
- const taskContext = TaskContext.getInstance(req.query.taskId as string, "deleteNotes");
+ const taskContext = TaskContext.getInstance(req.query.taskId as string, "deleteNotes", null);
const deleteId = utils.randomString(10);
let noteDeleted;
@@ -252,7 +252,7 @@ function deleteBranch(req: Request) {
}
if (last) {
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
}
return {
diff --git a/apps/server/src/routes/api/export.ts b/apps/server/src/routes/api/export.ts
index 7433cd552..4bc0c2177 100644
--- a/apps/server/src/routes/api/export.ts
+++ b/apps/server/src/routes/api/export.ts
@@ -23,7 +23,7 @@ function exportBranch(req: Request, res: Response) {
return;
}
- const taskContext = new TaskContext(taskId, "export");
+ const taskContext = new TaskContext(taskId, "export", null);
try {
if (type === "subtree" && (format === "html" || format === "markdown")) {
diff --git a/apps/server/src/routes/api/import.ts b/apps/server/src/routes/api/import.ts
index c7253f2d6..273dc1e1d 100644
--- a/apps/server/src/routes/api/import.ts
+++ b/apps/server/src/routes/api/import.ts
@@ -116,7 +116,7 @@ function importAttachmentsToNote(req: Request) {
}
const parentNote = becca.getNoteOrThrow(parentNoteId);
- const taskContext = TaskContext.getInstance(taskId, "importAttachment", options);
+ const taskContext = TaskContext.getInstance(taskId, "importNotes", options);
// unlike in note import, we let the events run, because a huge number of attachments is not likely
diff --git a/apps/server/src/routes/api/llm.ts b/apps/server/src/routes/api/llm.ts
index a15660676..78573ceda 100644
--- a/apps/server/src/routes/api/llm.ts
+++ b/apps/server/src/routes/api/llm.ts
@@ -1,18 +1,9 @@
import type { Request, Response } from "express";
import log from "../../services/log.js";
-import options from "../../services/options.js";
import restChatService from "../../services/llm/rest_chat_service.js";
import chatStorageService from '../../services/llm/chat_storage_service.js';
-
-// Define basic interfaces
-interface ChatMessage {
- role: 'user' | 'assistant' | 'system';
- content: string;
- timestamp?: Date;
-}
-
-
+import { WebSocketMessage } from "@triliumnext/commons";
/**
* @swagger
@@ -419,7 +410,7 @@ async function sendMessage(req: Request, res: Response) {
*/
async function streamMessage(req: Request, res: Response) {
log.info("=== Starting streamMessage ===");
-
+
try {
const chatNoteId = req.params.chatNoteId;
const { content, useAdvancedContext, showThinking, mentions } = req.body;
@@ -434,7 +425,7 @@ async function streamMessage(req: Request, res: Response) {
(res as any).triliumResponseHandled = true;
return;
}
-
+
// Send immediate success response
res.status(200).json({
success: true,
@@ -442,12 +433,12 @@ async function streamMessage(req: Request, res: Response) {
});
// Mark response as handled to prevent further processing
(res as any).triliumResponseHandled = true;
-
+
// Start background streaming process after sending response
handleStreamingProcess(chatNoteId, content, useAdvancedContext, showThinking, mentions)
.catch(error => {
log.error(`Background streaming error: ${error.message}`);
-
+
// Send error via WebSocket since HTTP response was already sent
import('../../services/ws.js').then(wsModule => {
wsModule.default.sendMessageToAllClients({
@@ -460,11 +451,11 @@ async function streamMessage(req: Request, res: Response) {
log.error(`Could not send WebSocket error: ${wsError}`);
});
});
-
+
} catch (error) {
// Handle any synchronous errors
log.error(`Synchronous error in streamMessage: ${error}`);
-
+
if (!res.headersSent) {
res.status(500).json({
success: false,
@@ -481,21 +472,21 @@ async function streamMessage(req: Request, res: Response) {
* This is separate from the HTTP request/response cycle
*/
async function handleStreamingProcess(
- chatNoteId: string,
- content: string,
- useAdvancedContext: boolean,
- showThinking: boolean,
+ chatNoteId: string,
+ content: string,
+ useAdvancedContext: boolean,
+ showThinking: boolean,
mentions: any[]
) {
log.info("=== Starting background streaming process ===");
-
+
// Get or create chat directly from storage
let chat = await chatStorageService.getChat(chatNoteId);
if (!chat) {
chat = await chatStorageService.createChat('New Chat');
log.info(`Created new chat with ID: ${chat.id} for stream request`);
}
-
+
// Add the user message to the chat immediately
chat.messages.push({
role: 'user',
@@ -544,9 +535,9 @@ async function handleStreamingProcess(
thinking: showThinking ? 'Initializing streaming LLM response...' : undefined
});
- // Instead of calling the complex handleSendMessage service,
+ // Instead of calling the complex handleSendMessage service,
// let's implement streaming directly to avoid response conflicts
-
+
try {
// Check if AI is enabled
const optionsModule = await import('../../services/options.js');
@@ -570,7 +561,7 @@ async function handleStreamingProcess(
// Get selected model
const { getSelectedModelConfig } = await import('../../services/llm/config/configuration_helpers.js');
const modelConfig = await getSelectedModelConfig();
-
+
if (!modelConfig) {
throw new Error("No valid AI model configuration found");
}
@@ -590,7 +581,7 @@ async function handleStreamingProcess(
chatNoteId: chatNoteId
},
streamCallback: (data, done, rawChunk) => {
- const message = {
+ const message: WebSocketMessage = {
type: 'llm-stream' as const,
chatNoteId: chatNoteId,
done: done
@@ -634,7 +625,7 @@ async function handleStreamingProcess(
// Execute the pipeline
await pipeline.execute(pipelineInput);
-
+
} catch (error: any) {
log.error(`Error in direct streaming: ${error.message}`);
wsService.sendMessageToAllClients({
diff --git a/apps/server/src/routes/api/notes.ts b/apps/server/src/routes/api/notes.ts
index 8a426dea3..3c6db4054 100644
--- a/apps/server/src/routes/api/notes.ts
+++ b/apps/server/src/routes/api/notes.ts
@@ -184,7 +184,7 @@ function deleteNote(req: Request) {
if (typeof taskId !== "string") {
throw new ValidationError("Missing or incorrect type for task ID.");
}
- const taskContext = TaskContext.getInstance(taskId, "deleteNotes");
+ const taskContext = TaskContext.getInstance(taskId, "deleteNotes", null);
note.deleteNote(deleteId, taskContext);
@@ -193,16 +193,16 @@ function deleteNote(req: Request) {
}
if (last) {
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
}
}
function undeleteNote(req: Request) {
- const taskContext = TaskContext.getInstance(utils.randomString(10), "undeleteNotes");
+ const taskContext = TaskContext.getInstance(utils.randomString(10), "undeleteNotes", null);
noteService.undeleteNote(req.params.noteId, taskContext);
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
}
function sortChildNotes(req: Request) {
@@ -226,7 +226,7 @@ function protectNote(req: Request) {
noteService.protectNoteRecursively(note, protect, includingSubTree, taskContext);
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
}
function setNoteTypeMime(req: Request) {
diff --git a/apps/server/src/routes/api/sync.ts b/apps/server/src/routes/api/sync.ts
index 0e4ae2678..5e1c53041 100644
--- a/apps/server/src/routes/api/sync.ts
+++ b/apps/server/src/routes/api/sync.ts
@@ -12,11 +12,10 @@ import syncOptions from "../../services/sync_options.js";
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
import ws from "../../services/ws.js";
import type { Request } from "express";
-import type { EntityChange } from "../../services/entity_changes_interface.js";
import ValidationError from "../../errors/validation_error.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import { t } from "i18next";
-import { SyncTestResponse } from "@triliumnext/commons";
+import { SyncTestResponse, type EntityChange } from "@triliumnext/commons";
async function testSync(): Promise {
try {
diff --git a/apps/server/src/services/cls.ts b/apps/server/src/services/cls.ts
index cd213748b..7636be7dd 100644
--- a/apps/server/src/services/cls.ts
+++ b/apps/server/src/services/cls.ts
@@ -1,5 +1,5 @@
import clsHooked from "cls-hooked";
-import type { EntityChange } from "./entity_changes_interface.js";
+import type { EntityChange } from "@triliumnext/commons";
const namespace = clsHooked.createNamespace("trilium");
type Callback = (...args: any[]) => any;
diff --git a/apps/server/src/services/consistency_checks.ts b/apps/server/src/services/consistency_checks.ts
index ec7850572..7b4ba72ad 100644
--- a/apps/server/src/services/consistency_checks.ts
+++ b/apps/server/src/services/consistency_checks.ts
@@ -15,7 +15,7 @@ import eraseService from "../services/erase.js";
import sanitizeAttributeName from "./sanitize_attribute_name.js";
import noteTypesService from "../services/note_types.js";
import type { BranchRow } from "@triliumnext/commons";
-import type { EntityChange } from "./entity_changes_interface.js";
+import type { EntityChange } from "@triliumnext/commons";
import becca_loader from "../becca/becca_loader.js";
const noteTypes = noteTypesService.getNoteTypeNames();
diff --git a/apps/server/src/services/entity_changes.ts b/apps/server/src/services/entity_changes.ts
index 66c2613ce..c0a97c7d6 100644
--- a/apps/server/src/services/entity_changes.ts
+++ b/apps/server/src/services/entity_changes.ts
@@ -6,7 +6,7 @@ import { randomString } from "./utils.js";
import instanceId from "./instance_id.js";
import becca from "../becca/becca.js";
import blobService from "../services/blob.js";
-import type { EntityChange } from "./entity_changes_interface.js";
+import type { EntityChange } from "@triliumnext/commons";
import type { Blob } from "./blob-interface.js";
import eventService from "./events.js";
diff --git a/apps/server/src/services/entity_changes_interface.ts b/apps/server/src/services/entity_changes_interface.ts
deleted file mode 100644
index e69eb5c37..000000000
--- a/apps/server/src/services/entity_changes_interface.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export interface EntityChange {
- id?: number | null;
- noteId?: string;
- entityName: string;
- entityId: string;
- entity?: any;
- positions?: Record;
- hash: string;
- utcDateChanged?: string;
- utcDateModified?: string;
- utcDateCreated?: string;
- isSynced: boolean | 1 | 0;
- isErased: boolean | 1 | 0;
- componentId?: string | null;
- changeId?: string | null;
- instanceId?: string | null;
-}
-
-export interface EntityRow {
- isDeleted?: boolean;
- content?: Buffer | string;
-}
-
-export interface EntityChangeRecord {
- entityChange: EntityChange;
- entity?: EntityRow;
-}
diff --git a/apps/server/src/services/erase.ts b/apps/server/src/services/erase.ts
index d5f4d2b1d..92b28e573 100644
--- a/apps/server/src/services/erase.ts
+++ b/apps/server/src/services/erase.ts
@@ -5,7 +5,7 @@ import optionService from "./options.js";
import dateUtils from "./date_utils.js";
import sqlInit from "./sql_init.js";
import cls from "./cls.js";
-import type { EntityChange } from "./entity_changes_interface.js";
+import type { EntityChange } from "@triliumnext/commons";
function eraseNotes(noteIdsToErase: string[]) {
if (noteIdsToErase.length === 0) {
diff --git a/apps/server/src/services/export/opml.ts b/apps/server/src/services/export/opml.ts
index 74d2b2c4e..52e60a60f 100644
--- a/apps/server/src/services/export/opml.ts
+++ b/apps/server/src/services/export/opml.ts
@@ -6,7 +6,7 @@ import type TaskContext from "../task_context.js";
import type BBranch from "../../becca/entities/bbranch.js";
import type { Response } from "express";
-function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string, res: Response) {
+function exportToOpml(taskContext: TaskContext<"export">, branch: BBranch, version: string, res: Response) {
if (!["1.0", "2.0"].includes(version)) {
throw new Error(`Unrecognized OPML version ${version}`);
}
@@ -77,7 +77,7 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
`);
res.end();
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
}
function prepareText(text: string) {
diff --git a/apps/server/src/services/export/single.ts b/apps/server/src/services/export/single.ts
index b626bf919..678fb39e5 100644
--- a/apps/server/src/services/export/single.ts
+++ b/apps/server/src/services/export/single.ts
@@ -10,7 +10,7 @@ import type BBranch from "../../becca/entities/bbranch.js";
import type { Response } from "express";
import type BNote from "../../becca/entities/bnote.js";
-function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) {
+function exportSingleNote(taskContext: TaskContext<"export">, branch: BBranch, format: "html" | "markdown", res: Response) {
const note = branch.getNote();
if (note.type === "image" || note.type === "file") {
@@ -30,7 +30,7 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
res.send(payload);
taskContext.increaseProgressCount();
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
}
export function mapByNoteType(note: BNote, content: string | Buffer, format: "html" | "markdown") {
diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts
index 91d01c8c7..116a841b2 100644
--- a/apps/server/src/services/export/zip.ts
+++ b/apps/server/src/services/export/zip.ts
@@ -40,7 +40,7 @@ export interface AdvancedExportOptions {
customRewriteLinks?: (originalRewriteLinks: RewriteLinksFn, getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null) => RewriteLinksFn;
}
-async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
+async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
if (!["html", "markdown"].includes(format)) {
throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
}
@@ -611,7 +611,7 @@ ${markdownContent}`;
archive.pipe(res);
await archive.finalize();
- taskContext.taskSucceeded();
+ taskContext.taskSucceeded(null);
} catch (e: unknown) {
const message = `Export failed with error: ${e instanceof Error ? e.message : String(e)}`;
log.error(message);
@@ -627,7 +627,7 @@ ${markdownContent}`;
async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string, zipExportOptions?: AdvancedExportOptions) {
const fileOutputStream = fs.createWriteStream(zipFilePath);
- const taskContext = new TaskContext("no-progress-reporting");
+ const taskContext = new TaskContext("no-progress-reporting", "export", null);
const note = becca.getNote(noteId);
diff --git a/apps/server/src/services/import/enex.ts b/apps/server/src/services/import/enex.ts
index 4699ca32e..5a13e0960 100644
--- a/apps/server/src/services/import/enex.ts
+++ b/apps/server/src/services/import/enex.ts
@@ -55,7 +55,7 @@ interface Note {
let note: Partial = {};
let resource: Resource;
-function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Promise {
+function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote): Promise {
const saxStream = sax.createStream(true);
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") ? file.originalname.substr(0, file.originalname.length - 5) : file.originalname;
diff --git a/apps/server/src/services/import/mime.ts b/apps/server/src/services/import/mime.ts
index 479bd3494..0a129ae1e 100644
--- a/apps/server/src/services/import/mime.ts
+++ b/apps/server/src/services/import/mime.ts
@@ -2,8 +2,7 @@
import mimeTypes from "mime-types";
import path from "path";
-import type { TaskData } from "../task_context_interface.js";
-import type { NoteType } from "@triliumnext/commons";
+import type { NoteType, TaskData } from "@triliumnext/commons";
const CODE_MIME_TYPES = new Set([
"application/json",
@@ -92,14 +91,14 @@ function getMime(fileName: string) {
return mimeFromExt || mimeTypes.lookup(fileNameLc);
}
-function getType(options: TaskData, mime: string): NoteType {
+function getType(options: TaskData<"importNotes">, mime: string): NoteType {
const mimeLc = mime?.toLowerCase();
switch (true) {
- case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown", "text/mdx"].includes(mimeLc):
+ case options?.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown", "text/mdx"].includes(mimeLc):
return "text";
- case options.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc):
+ case options?.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc):
return "code";
case mime.startsWith("image/"):
diff --git a/apps/server/src/services/import/opml.ts b/apps/server/src/services/import/opml.ts
index 934578c70..130eb8197 100644
--- a/apps/server/src/services/import/opml.ts
+++ b/apps/server/src/services/import/opml.ts
@@ -28,7 +28,7 @@ interface OpmlOutline {
outline: OpmlOutline[];
}
-async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer, parentNote: BNote) {
+async function importOpml(taskContext: TaskContext<"importNotes">, fileBuffer: string | Buffer, parentNote: BNote) {
const xml = await new Promise(function (resolve, reject) {
parseString(fileBuffer, function (err: any, result: OpmlXml) {
if (err) {
diff --git a/apps/server/src/services/import/single.spec.ts b/apps/server/src/services/import/single.spec.ts
index b16124cbb..0d0af35f7 100644
--- a/apps/server/src/services/import/single.spec.ts
+++ b/apps/server/src/services/import/single.spec.ts
@@ -14,7 +14,7 @@ const scriptDir = dirname(fileURLToPath(import.meta.url));
async function testImport(fileName: string, mimetype: string) {
const buffer = fs.readFileSync(path.join(scriptDir, "samples", fileName));
- const taskContext = TaskContext.getInstance("import-mdx", "import", {
+ const taskContext = TaskContext.getInstance("import-mdx", "importNotes", {
textImportedAsText: true,
codeImportedAsCode: true
});
diff --git a/apps/server/src/services/import/single.ts b/apps/server/src/services/import/single.ts
index 7603cd625..ac52a43f4 100644
--- a/apps/server/src/services/import/single.ts
+++ b/apps/server/src/services/import/single.ts
@@ -14,7 +14,7 @@ import htmlSanitizer from "../html_sanitizer.js";
import type { File } from "./common.js";
import type { NoteType } from "@triliumnext/commons";
-function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importSingleFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const mime = mimeService.getMime(file.originalname) || file.mimetype;
if (taskContext?.data?.textImportedAsText) {
@@ -42,7 +42,7 @@ function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNot
return importFile(taskContext, file, parentNote);
}
-function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
+function importImage(file: File, parentNote: BNote, taskContext: TaskContext<"importNotes">) {
if (typeof file.buffer === "string") {
throw new Error("Invalid file content for image.");
}
@@ -53,7 +53,7 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
return note;
}
-function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const originalName = file.originalname;
const { note } = noteService.createNewNote({
@@ -72,7 +72,7 @@ function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
return note;
}
-function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importCodeNote(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const content = processStringOrBuffer(file.buffer);
const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
@@ -97,7 +97,7 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote)
return note;
}
-function importCustomType(taskContext: TaskContext, file: File, parentNote: BNote, type: NoteType, mime: string) {
+function importCustomType(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote, type: NoteType, mime: string) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const content = processStringOrBuffer(file.buffer);
@@ -115,7 +115,7 @@ function importCustomType(taskContext: TaskContext, file: File, parentNote: BNot
return note;
}
-function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importPlainText(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const plainTextContent = processStringOrBuffer(file.buffer);
const htmlContent = convertTextToHtml(plainTextContent);
@@ -150,7 +150,7 @@ function convertTextToHtml(text: string) {
return text;
}
-function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importMarkdown(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const markdownContent = processStringOrBuffer(file.buffer);
@@ -174,7 +174,7 @@ function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote)
return note;
}
-function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importHtml(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
let content = processStringOrBuffer(file.buffer);
// Try to get title from HTML first, fall back to filename
@@ -202,7 +202,7 @@ function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
return note;
}
-function importAttachment(taskContext: TaskContext, file: File, parentNote: BNote) {
+function importAttachment(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const mime = mimeService.getMime(file.originalname) || file.mimetype;
if (mime.startsWith("image/") && typeof file.buffer !== "string") {
diff --git a/apps/server/src/services/import/zip.spec.ts b/apps/server/src/services/import/zip.spec.ts
index db2c7ba76..a74c243c4 100644
--- a/apps/server/src/services/import/zip.spec.ts
+++ b/apps/server/src/services/import/zip.spec.ts
@@ -14,7 +14,7 @@ const scriptDir = dirname(fileURLToPath(import.meta.url));
async function testImport(fileName: string) {
const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", fileName));
- const taskContext = TaskContext.getInstance("import-mdx", "import", {
+ const taskContext = TaskContext.getInstance("import-mdx", "importNotes", {
textImportedAsText: true
});
diff --git a/apps/server/src/services/import/zip.ts b/apps/server/src/services/import/zip.ts
index b2d83bdc6..c1ac90b91 100644
--- a/apps/server/src/services/import/zip.ts
+++ b/apps/server/src/services/import/zip.ts
@@ -30,7 +30,7 @@ interface ImportZipOpts {
preserveIds?: boolean;
}
-async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRootNote: BNote, opts?: ImportZipOpts): Promise {
+async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Buffer, importRootNote: BNote, opts?: ImportZipOpts): Promise {
/** maps from original noteId (in ZIP file) to newly generated noteId */
const noteIdMap: Record = {};
/** type maps from original attachmentId (in ZIP file) to newly generated attachmentId */
@@ -174,7 +174,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
return noteId;
}
- function detectFileTypeAndMime(taskContext: TaskContext, filePath: string) {
+ function detectFileTypeAndMime(taskContext: TaskContext<"importNotes">, filePath: string) {
const mime = mimeService.getMime(filePath) || "application/octet-stream";
const type = mimeService.getType(taskContext.data || {}, mime);
diff --git a/apps/server/src/services/llm/chat/handlers/stream_handler.ts b/apps/server/src/services/llm/chat/handlers/stream_handler.ts
index 3aeb26d83..1ebca5d5a 100644
--- a/apps/server/src/services/llm/chat/handlers/stream_handler.ts
+++ b/apps/server/src/services/llm/chat/handlers/stream_handler.ts
@@ -4,7 +4,6 @@
import log from "../../../log.js";
import type { Response } from "express";
import type { StreamChunk } from "../../ai_interface.js";
-import type { LLMStreamMessage } from "../../interfaces/chat_ws_messages.js";
import type { ChatSession } from "../../interfaces/chat_session.js";
/**
@@ -46,7 +45,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
thinking: 'Preparing response...'
- } as LLMStreamMessage);
+ });
try {
// Import the tool handler
@@ -66,7 +65,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
thinking: 'Analyzing tools needed for this request...'
- } as LLMStreamMessage);
+ });
try {
// Execute the tools
@@ -82,7 +81,7 @@ export class StreamHandler {
tool: toolResult.name,
result: toolResult.content.substring(0, 100) + (toolResult.content.length > 100 ? '...' : '')
}
- } as LLMStreamMessage);
+ });
}
// Make follow-up request with tool results
@@ -123,7 +122,7 @@ export class StreamHandler {
chatNoteId,
error: `Error executing tools: ${toolError instanceof Error ? toolError.message : 'Unknown error'}`,
done: true
- } as LLMStreamMessage);
+ });
}
} else if (response.stream) {
// Handle standard streaming through the stream() method
@@ -152,7 +151,7 @@ export class StreamHandler {
chatNoteId,
content: messageContent,
done: true
- } as LLMStreamMessage);
+ });
log.info(`Complete response sent`);
@@ -174,14 +173,14 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
error: `Error generating response: ${streamingError instanceof Error ? streamingError.message : 'Unknown error'}`
- } as LLMStreamMessage);
+ });
// Signal completion
wsService.sendMessageToAllClients({
type: 'llm-stream',
chatNoteId,
done: true
- } as LLMStreamMessage);
+ });
}
}
@@ -218,7 +217,7 @@ export class StreamHandler {
done: !!chunk.done, // Include done flag with each chunk
// Include any raw data from the provider that might contain thinking/tool info
...(chunk.raw ? { raw: chunk.raw } : {})
- } as LLMStreamMessage);
+ });
// Log the first chunk (useful for debugging)
if (messageContent.length === chunk.text.length) {
@@ -232,7 +231,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
thinking: chunk.raw.thinking
- } as LLMStreamMessage);
+ });
}
// If the provider indicates tool execution, relay that
@@ -241,7 +240,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
toolExecution: chunk.raw.toolExecution
- } as LLMStreamMessage);
+ });
}
// Handle direct tool_calls in the response (for OpenAI)
@@ -252,7 +251,7 @@ export class StreamHandler {
wsService.sendMessageToAllClients({
type: 'tool_execution_start',
chatNoteId
- } as LLMStreamMessage);
+ });
// Process each tool call
for (const toolCall of chunk.tool_calls) {
@@ -277,7 +276,7 @@ export class StreamHandler {
toolCallId: toolCall.id,
args: args
}
- } as LLMStreamMessage);
+ });
}
}
@@ -337,7 +336,7 @@ export class StreamHandler {
type: 'llm-stream',
chatNoteId,
done: true
- } as LLMStreamMessage);
+ });
}
// Store the full response in the session
@@ -360,7 +359,7 @@ export class StreamHandler {
chatNoteId,
error: `Error during streaming: ${streamError instanceof Error ? streamError.message : 'Unknown error'}`,
done: true
- } as LLMStreamMessage);
+ });
throw streamError;
}
diff --git a/apps/server/src/services/llm/chat/index.ts b/apps/server/src/services/llm/chat/index.ts
index 79b587a09..622f65374 100644
--- a/apps/server/src/services/llm/chat/index.ts
+++ b/apps/server/src/services/llm/chat/index.ts
@@ -7,7 +7,6 @@ import { ToolHandler } from './handlers/tool_handler.js';
import { StreamHandler } from './handlers/stream_handler.js';
import * as messageFormatter from './utils/message_formatter.js';
import type { ChatSession, ChatMessage, NoteSource } from '../interfaces/chat_session.js';
-import type { LLMStreamMessage } from '../interfaces/chat_ws_messages.js';
// Export components
export {
@@ -22,6 +21,5 @@ export {
export type {
ChatSession,
ChatMessage,
- NoteSource,
- LLMStreamMessage
+ NoteSource
};
diff --git a/apps/server/src/services/llm/chat/rest_chat_service.ts b/apps/server/src/services/llm/chat/rest_chat_service.ts
index 5bf57c042..45af7e944 100644
--- a/apps/server/src/services/llm/chat/rest_chat_service.ts
+++ b/apps/server/src/services/llm/chat/rest_chat_service.ts
@@ -4,18 +4,15 @@
*/
import log from "../../log.js";
import type { Request, Response } from "express";
-import type { Message, ChatCompletionOptions } from "../ai_interface.js";
+import type { Message } from "../ai_interface.js";
import aiServiceManager from "../ai_service_manager.js";
import { ChatPipeline } from "../pipeline/chat_pipeline.js";
import type { ChatPipelineInput } from "../pipeline/interfaces.js";
import options from "../../options.js";
import { ToolHandler } from "./handlers/tool_handler.js";
-import type { LLMStreamMessage } from "../interfaces/chat_ws_messages.js";
import chatStorageService from '../chat_storage_service.js';
-import {
- isAIEnabled,
- getSelectedModelConfig,
-} from '../config/configuration_helpers.js';
+import { getSelectedModelConfig } from '../config/configuration_helpers.js';
+import { WebSocketMessage } from "@triliumnext/commons";
/**
* Simplified service to handle chat API interactions
@@ -79,7 +76,7 @@ class RestChatService {
throw new Error("Database is not initialized");
}
- // Get or create AI service - will throw meaningful error if not possible
+ // Get or create AI service - will throw meaningful error if not possible
await aiServiceManager.getOrCreateAnyService();
// Load or create chat directly from storage
@@ -204,7 +201,7 @@ class RestChatService {
accumulatedContentRef: { value: string },
chat: { id: string; messages: Message[]; title: string }
) {
- const message: LLMStreamMessage = {
+ const message: WebSocketMessage = {
type: 'llm-stream',
chatNoteId: chatNoteId,
done: done
@@ -237,7 +234,7 @@ class RestChatService {
// Send WebSocket message
wsService.sendMessageToAllClients(message);
-
+
// When streaming is complete, save the accumulated content to the chat note
if (done) {
try {
@@ -248,7 +245,7 @@ class RestChatService {
role: 'assistant',
content: accumulatedContentRef.value
});
-
+
// Save the updated chat back to storage
await chatStorageService.updateChat(chat.id, chat.messages, chat.title);
log.info(`Saved streaming assistant response: ${accumulatedContentRef.value.length} characters`);
@@ -257,7 +254,7 @@ class RestChatService {
// Log error but don't break the response flow
log.error(`Error saving streaming response: ${error}`);
}
-
+
// Note: For WebSocket-only streaming, we don't end the HTTP response here
// since it was already handled by the calling endpoint
}
diff --git a/apps/server/src/services/llm/interfaces/chat_ws_messages.ts b/apps/server/src/services/llm/interfaces/chat_ws_messages.ts
deleted file mode 100644
index f75d399f4..000000000
--- a/apps/server/src/services/llm/interfaces/chat_ws_messages.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Interfaces for WebSocket LLM streaming messages
- */
-
-/**
- * Interface for WebSocket LLM streaming messages
- */
-export interface LLMStreamMessage {
- type: 'llm-stream' | 'tool_execution_start' | 'tool_result' | 'tool_execution_error' | 'tool_completion_processing';
- chatNoteId: string;
- content?: string;
- thinking?: string;
- toolExecution?: {
- action?: string;
- tool?: string;
- toolCallId?: string;
- result?: string | Record;
- error?: string;
- args?: Record;
- };
- done?: boolean;
- error?: string;
- raw?: unknown;
-}
diff --git a/apps/server/src/services/notes.ts b/apps/server/src/services/notes.ts
index e225cdb52..3ecf98e0a 100644
--- a/apps/server/src/services/notes.ts
+++ b/apps/server/src/services/notes.ts
@@ -296,7 +296,7 @@ function createNewNoteWithTarget(target: "into" | "after" | "before", targetBran
}
}
-function protectNoteRecursively(note: BNote, protect: boolean, includingSubTree: boolean, taskContext: TaskContext) {
+function protectNoteRecursively(note: BNote, protect: boolean, includingSubTree: boolean, taskContext: TaskContext<"protectNotes">) {
protectNote(note, protect);
taskContext.increaseProgressCount();
@@ -765,7 +765,7 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
}
}
-function undeleteNote(noteId: string, taskContext: TaskContext) {
+function undeleteNote(noteId: string, taskContext: TaskContext<"undeleteNotes">) {
const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!noteRow.isDeleted || !noteRow.deleteId) {
@@ -785,7 +785,7 @@ function undeleteNote(noteId: string, taskContext: TaskContext) {
}
}
-function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskContext) {
+function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskContext<"undeleteNotes">) {
const branchRow = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]);
if (!branchRow.isDeleted) {
diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts
index 541e487a0..926d61bba 100644
--- a/apps/server/src/services/sql_init.ts
+++ b/apps/server/src/services/sql_init.ts
@@ -122,7 +122,7 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
log.info("Importing demo content ...");
- const dummyTaskContext = new TaskContext("no-progress-reporting", "import", false);
+ const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
if (demoFile) {
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
diff --git a/apps/server/src/services/sync.ts b/apps/server/src/services/sync.ts
index a26fabf82..ef3bd6cba 100644
--- a/apps/server/src/services/sync.ts
+++ b/apps/server/src/services/sync.ts
@@ -17,7 +17,7 @@ import ws from "./ws.js";
import entityChangesService from "./entity_changes.js";
import entityConstructor from "../becca/entity_constructor.js";
import becca from "../becca/becca.js";
-import type { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
+import type { EntityChange, EntityChangeRecord, EntityRow } from "@triliumnext/commons";
import type { CookieJar, ExecOpts } from "./request_interface.js";
import setupService from "./setup.js";
import consistency_checks from "./consistency_checks.js";
diff --git a/apps/server/src/services/sync_update.ts b/apps/server/src/services/sync_update.ts
index a22ba6717..9d4ff5c4c 100644
--- a/apps/server/src/services/sync_update.ts
+++ b/apps/server/src/services/sync_update.ts
@@ -4,7 +4,7 @@ import entityChangesService from "./entity_changes.js";
import eventService from "./events.js";
import entityConstructor from "../becca/entity_constructor.js";
import ws from "./ws.js";
-import type { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
+import type { EntityChange, EntityChangeRecord, EntityRow } from "@triliumnext/commons";
interface UpdateContext {
alreadyErased: number;
diff --git a/apps/server/src/services/task_context.ts b/apps/server/src/services/task_context.ts
index f83154147..79122895b 100644
--- a/apps/server/src/services/task_context.ts
+++ b/apps/server/src/services/task_context.ts
@@ -1,20 +1,20 @@
"use strict";
-import type { TaskData } from "./task_context_interface.js";
+import type { TaskData, TaskResult, TaskType, WebSocketMessage } from "@triliumnext/commons";
import ws from "./ws.js";
// taskId => TaskContext
-const taskContexts: Record = {};
+const taskContexts: Record> = {};
-class TaskContext {
+class TaskContext {
private taskId: string;
- private taskType: string | null;
+ private taskType: TaskType;
private progressCount: number;
private lastSentCountTs: number;
- data: TaskData | null;
+ data: TaskData;
noteDeletionHandlerTriggered: boolean;
- constructor(taskId: string, taskType: string | null = null, data: {} | null = {}) {
+ constructor(taskId: string, taskType: T, data: TaskData) {
this.taskId = taskId;
this.taskType = taskType;
this.data = data;
@@ -31,7 +31,7 @@ class TaskContext {
this.increaseProgressCount();
}
- static getInstance(taskId: string, taskType: string, data: {} | null = null): TaskContext {
+ static getInstance(taskId: string, taskType: T, data: TaskData): TaskContext {
if (!taskContexts[taskId]) {
taskContexts[taskId] = new TaskContext(taskId, taskType, data);
}
@@ -51,7 +51,7 @@ class TaskContext {
taskType: this.taskType,
data: this.data,
progressCount: this.progressCount
- });
+ } as WebSocketMessage);
}
}
@@ -61,18 +61,18 @@ class TaskContext {
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
- message: message
- });
+ message
+ } as WebSocketMessage);
}
- taskSucceeded(result?: string | Record) {
+ taskSucceeded(result: TaskResult) {
ws.sendMessageToAllClients({
type: "taskSucceeded",
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
- result: result
- });
+ result
+ } as WebSocketMessage);
}
}
diff --git a/apps/server/src/services/task_context_interface.ts b/apps/server/src/services/task_context_interface.ts
deleted file mode 100644
index 3c359d742..000000000
--- a/apps/server/src/services/task_context_interface.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface TaskData {
- safeImport?: boolean;
- textImportedAsText?: boolean;
- codeImportedAsCode?: boolean;
- shrinkImages?: boolean;
- replaceUnderscoresWithSpaces?: boolean;
-}
diff --git a/apps/server/src/services/ws.ts b/apps/server/src/services/ws.ts
index c37cf7550..9dfcbc019 100644
--- a/apps/server/src/services/ws.ts
+++ b/apps/server/src/services/ws.ts
@@ -1,5 +1,5 @@
import { WebSocketServer as WebSocketServer, WebSocket } from "ws";
-import { isDev, isElectron, randomString } from "./utils.js";
+import { isElectron, randomString } from "./utils.js";
import log from "./log.js";
import sql from "./sql.js";
import cls from "./cls.js";
@@ -10,56 +10,10 @@ import becca from "../becca/becca.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type { IncomingMessage, Server as HttpServer } from "http";
-import type { EntityChange } from "./entity_changes_interface.js";
+import { WebSocketMessage, type EntityChange } from "@triliumnext/commons";
let webSocketServer!: WebSocketServer;
-let lastSyncedPush: number | null = null;
-
-interface Message {
- type: string;
- data?: {
- lastSyncedPush?: number | null;
- entityChanges?: any[];
- shrinkImages?: boolean;
- } | null;
- lastSyncedPush?: number | null;
-
- progressCount?: number;
- taskId?: string;
- taskType?: string | null;
- message?: string;
- reason?: string;
- result?: string | Record;
-
- script?: string;
- params?: any[];
- noteId?: string;
- messages?: string[];
- startNoteId?: string;
- currentNoteId?: string;
- entityType?: string;
- entityId?: string;
- originEntityName?: "notes";
- originEntityId?: string | null;
- lastModifiedMs?: number;
- filePath?: string;
-
- // LLM streaming specific fields
- chatNoteId?: string;
- content?: string;
- thinking?: string;
- toolExecution?: {
- action?: string;
- tool?: string;
- toolCallId?: string;
- result?: string | Record;
- error?: string;
- args?: Record;
- };
- done?: boolean;
- error?: string;
- raw?: unknown;
-}
+let lastSyncedPush: number;
type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;
function init(httpServer: HttpServer, sessionParser: SessionParser) {
@@ -106,7 +60,7 @@ Stack: ${message.stack}`);
});
}
-function sendMessage(client: WebSocket, message: Message) {
+function sendMessage(client: WebSocket, message: WebSocketMessage) {
const jsonStr = JSON.stringify(message);
if (client.readyState === WebSocket.OPEN) {
@@ -114,7 +68,7 @@ function sendMessage(client: WebSocket, message: Message) {
}
}
-function sendMessageToAllClients(message: Message) {
+function sendMessageToAllClients(message: WebSocketMessage) {
const jsonStr = JSON.stringify(message);
if (webSocketServer) {
diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json
index 7a1d0831b..2a90f721a 100644
--- a/docs/User Guide/!!!meta.json
+++ b/docs/User Guide/!!!meta.json
@@ -1728,6 +1728,13 @@
"value": "bx bx-menu",
"isInheritable": false,
"position": 10
+ },
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "MKmLg5x6xkor",
+ "isInheritable": false,
+ "position": 200
}
],
"format": "markdown",
diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.md b/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.md
index 2df2536cf..5ecb4bbd3 100644
--- a/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.md
+++ b/docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu.md
@@ -52,6 +52,10 @@ The contextual menu can operate:
* Creates a copy of the note and its descendants.
* This process is different from Cloning Notes since the duplicated note can be edited independently from the original.
* An alternative to this, if done regularly, would be Templates.
+* **Archive/Unarchive**
+ * Marks a note as [archived](../../Notes/Archived%20Notes.md).
+ * If the note is already archived, it will be unarchived instead.
+ * Multiple notes can be selected as well. However, all the selected notes must be in the same state (archived or not), otherwise the option will be disabled.
* **Delete**
* Will delete the given notes, asking for confirmation first.
* In the dialog, the following options can be configured:
diff --git a/docs/User Guide/User Guide/Note Types/Collections.md b/docs/User Guide/User Guide/Note Types/Collections.md
index 81f177fd5..03bc5140c 100644
--- a/docs/User Guide/User Guide/Note Types/Collections.md
+++ b/docs/User Guide/User Guide/Note Types/Collections.md
@@ -47,6 +47,12 @@ By default, collections come with a default configuration and sometimes even sam
3. Still in the ribbon, go to _Collection Properties_ and select the desired view type.
4. Consult the help page of the corresponding view type in order to understand how to configure them.
+## Archived notes
+
+By default, archived notes will not be shown in collections. This behaviour can be changed by going to _Collection Properties_ in the Ribbon and checking _Show archived notes_.
+
+Archived notes will be generally indicated by being greyed out as opposed to the normal ones.
+
## Under the hood
Collections by themselves are simply notes with no content that rely on the Note List mechanism (the one that lists the children notes at the bottom of a note) to display information.
diff --git a/docs/User Guide/User Guide/Note Types/Collections/Board View.md b/docs/User Guide/User Guide/Note Types/Collections/Board View.md
index 6a631bd75..4b8bcbdbc 100644
--- a/docs/User Guide/User Guide/Note Types/Collections/Board View.md
+++ b/docs/User Guide/User Guide/Note Types/Collections/Board View.md
@@ -12,7 +12,7 @@ Notes are displayed recursively, so even the child notes of the child notes will
## Interaction with columns
* Create a new column by pressing _Add Column_ near the last column.
- * Once pressed, a text box will be displayed to set the name of the column. Press Enter to confirm.
+ * Once pressed, a text box will be displayed to set the name of the column. Press Enter to confirm, or Escape to dismiss.
* To reorder a column, simply hold the mouse over the title and drag it to the desired position.
* To delete a column, right click on its title and select _Delete column_.
* To rename a column, click on the note title.
@@ -23,8 +23,10 @@ Notes are displayed recursively, so even the child notes of the child notes will
## Interaction with notes
* Create a new note in any column by pressing _New item_
- * Enter the name of the note and press _Enter_.
- * Doing so will create a new note. The new note will have an attribute (`status` label by default) set to the name of the column.
+ * Enter the name of the note and press Enter or click away. To dismiss the creation of a new note, simply press Escape or leave the name empty.
+ * Once created, the new note will have an attribute (`status` label by default) set to the name of the column.
+* To open the note, simply click on it.
+* To change the title of the note directly from the board, hover the mouse over its card and press the edit button on the right.
* To change the state of a note, simply drag a note from one column to the other to change its state.
* The order of the notes in each column corresponds to their position in the tree.
* It's possible to reorder notes simply by dragging them to the desired position within the same columns.
@@ -33,6 +35,7 @@ Notes are displayed recursively, so even the child notes of the child notes will
* Open the note in a new tab/split/window or quick edit.
* Move the note to any column.
* Insert a new note above/below the current one.
+ * Archive/unarchive the current note.
* Delete the current note.
* If there are many notes within the column, move the mouse over the column and use the mouse wheel to scroll.
diff --git a/docs/User Guide/User Guide/Note Types/Collections/Geo Map View.md b/docs/User Guide/User Guide/Note Types/Collections/Geo Map View.md
index ae0be91ce..a060d385e 100644
--- a/docs/User Guide/User Guide/Note Types/Collections/Geo Map View.md
+++ b/docs/User Guide/User Guide/Note Types/Collections/Geo Map View.md
@@ -26,8 +26,8 @@ The position on the map and the zoom are saved inside the map note and restored
| | | |
| --- | --- | --- |
-| 1 | To create a marker, first navigate to the desired point on the map. Then press the  button in the [Floating buttons](../../Basic%20Concepts%20and%20Features/UI%20Elements/Floating%20buttons.md) (top-right) area.
If the button is not visible, make sure the button section is visible by pressing the chevron button () in the top-right of the map. | |
-| 2 | | Once pressed, the map will enter in the insert mode, as illustrated by the notification.
Simply click the point on the map where to place the marker, or the Escape key to cancel. |
+| 1 | To create a marker, first navigate to the desired point on the map. Then press the  button in the [Floating buttons](../../Basic%20Concepts%20and%20Features/UI%20Elements/Floating%20buttons.md) (top-right) area.
If the button is not visible, make sure the button section is visible by pressing the chevron button () in the top-right of the map. | |
+| 2 | | Once pressed, the map will enter in the insert mode, as illustrated by the notification.
Simply click the point on the map where to place the marker, or the Escape key to cancel. |
| 3 | | Enter the name of the marker/note to be created. |
| 4 | | Once confirmed, the marker will show up on the map and it will also be displayed as a child note of the map. |
@@ -50,6 +50,9 @@ This works for:
* Notes that are a child of the geo map but not yet positioned on the map.
* Notes that are a child of the geo map and also positioned, case in which the marker will be relocated to the new position.
+> [!NOTE]
+> Dragging existing notes only works if the map is in editing mode. See the _Read-only_ section for more information.
+
## How the location of the markers is stored
The location of a marker is stored in the `#geolocation` attribute of the child notes:
@@ -106,7 +109,7 @@ The value of the attribute is made up of the latitude and longitude separated by
| | | |
| --- | --- | --- |
-| 1 | | Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.
Simply click on the first item displaying the coordinates and they will be copied to clipboard.
Then paste the value inside the text box into the `#geolocation` attribute of a child note of the map (don't forget to surround the value with a `"` character). |
+| 1 | | Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.
Simply click on the first item displaying the coordinates and they will be copied to clipboard.
Then paste the value inside the text box into the `#geolocation` attribute of a child note of the map (don't forget to surround the value with a `"` character). |
| 2 | | In Trilium, create a child note under the map. |
| 3 | | And then go to Owned Attributes and type `#geolocation="`, then paste from the clipboard as-is and then add the ending `"` character. Press Enter to confirm and the map should now be updated to contain the new note. |
@@ -117,7 +120,7 @@ Similarly to the Google Maps approach:
| | | |
| --- | --- | --- |
| 1 | | Go to any location on openstreetmap.org and right click to bring up the context menu. Select the “Show address” item. |
-| 2 | | The address will be visible in the top-left of the screen, in the place of the search bar.
Select the coordinates and copy them into the clipboard. |
+| 2 | | The address will be visible in the top-left of the screen, in the place of the search bar.
Select the coordinates and copy them into the clipboard. |
| 3 | | Simply paste the value inside the text box into the `#geolocation` attribute of a child note of the map and then it should be displayed on the map. |
## Adding GPS tracks (.gpx)
@@ -128,7 +131,7 @@ Trilium has basic support for displaying GPS tracks on the geo map.
| --- | --- | --- |
| 1 | | To add a track, simply drag & drop a .gpx file inside the geo map in the note tree. |
| 2 | | In order for the file to be recognized as a GPS track, it needs to show up as `application/gpx+xml` in the _File type_ field. |
-| 3 | | When going back to the map, the track should now be visible.
The start and end points of the track are indicated by the two blue markers. |
+| 3 | | When going back to the map, the track should now be visible.
The start and end points of the track are indicated by the two blue markers. |
> [!NOTE]
> The starting point of the track will be displayed as a marker, with the name of the note underneath. The start marker will also respect the icon and the `color` of the note. The end marker is displayed with a distinct icon.
diff --git a/docs/User Guide/User Guide/Note Types/Collections/Table View.md b/docs/User Guide/User Guide/Note Types/Collections/Table View.md
index 0454b0a93..78774734a 100644
--- a/docs/User Guide/User Guide/Note Types/Collections/Table View.md
+++ b/docs/User Guide/User Guide/Note Types/Collections/Table View.md
@@ -65,7 +65,8 @@ There are multiple menus:
* Adding new columns.
* Right clicking on a row, allows:
* Opening the corresponding note of the row in a new tab, split, window or quick editing it.
- * Inserting rows above, below or as a child note.
+ * Inserting a new note above or below the selected row. These options are only enabled if the table is not sorted.
+ * Inserting a new child note for the selected row.
* Deleting the row.
### Editing data
diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts
index 432990bc0..ef60f29be 100644
--- a/packages/commons/src/index.ts
+++ b/packages/commons/src/index.ts
@@ -8,3 +8,5 @@ export * from "./lib/mime_type.js";
export * from "./lib/bulk_actions.js";
export * from "./lib/server_api.js";
export * from "./lib/shared_constants.js";
+export * from "./lib/ws_api.js";
+export * from "./lib/attribute_names.js";
diff --git a/packages/commons/src/lib/attribute_names.ts b/packages/commons/src/lib/attribute_names.ts
new file mode 100644
index 000000000..8b8de89c1
--- /dev/null
+++ b/packages/commons/src/lib/attribute_names.ts
@@ -0,0 +1,58 @@
+/**
+ * A listing of all the labels used by the system (i.e. not user-defined). Labels defined here have a data type which is not enforced, but offers type safety.
+ */
+type Labels = {
+ color: string;
+ iconClass: string;
+ workspaceIconClass: string;
+ executeDescription: string;
+ executeTitle: string;
+ limit: string; // should be probably be number
+ calendarRoot: boolean;
+ workspaceCalendarRoot: boolean;
+ archived: boolean;
+ sorted: boolean;
+ template: boolean;
+ autoReadOnlyDisabled: boolean;
+ language: string;
+ originalFileName: string;
+ pageUrl: string;
+
+ // Search
+ searchString: string;
+ ancestorDepth: string;
+ orderBy: string;
+ orderDirection: string;
+
+ // Collection-specific
+ viewType: string;
+ status: string;
+ pageSize: number;
+ geolocation: string;
+ readOnly: boolean;
+ expanded: boolean;
+ "calendar:hideWeekends": boolean;
+ "calendar:weekNumbers": boolean;
+ "calendar:view": string;
+ "map:style": string;
+ "map:scale": boolean;
+ "board:groupBy": string;
+ maxNestingDepth: number;
+ includeArchived: boolean;
+}
+
+/**
+ * A listing of all relations used by the system (i.e. not user-defined). Unlike labels, relations
+ * always point to a note ID, so no specific data type is necessary.
+ */
+type Relations = [
+ "searchScript",
+ "ancestor"
+];
+
+export type LabelNames = keyof Labels;
+export type RelationNames = Relations[number];
+
+export type FilterLabelsByType = {
+ [K in keyof Labels]: Labels[K] extends U ? K : never;
+}[keyof Labels];
diff --git a/packages/commons/src/lib/ws_api.ts b/packages/commons/src/lib/ws_api.ts
new file mode 100644
index 000000000..67beb0b42
--- /dev/null
+++ b/packages/commons/src/lib/ws_api.ts
@@ -0,0 +1,158 @@
+export interface EntityChange {
+ id?: number | null;
+ noteId?: string;
+ entityName: string;
+ entityId: string;
+ entity?: any;
+ positions?: Record;
+ hash: string;
+ utcDateChanged?: string;
+ utcDateModified?: string;
+ utcDateCreated?: string;
+ isSynced: boolean | 1 | 0;
+ isErased: boolean | 1 | 0;
+ componentId?: string | null;
+ changeId?: string | null;
+ instanceId?: string | null;
+}
+
+export interface EntityRow {
+ isDeleted?: boolean;
+ content?: Buffer | string;
+}
+
+export interface EntityChangeRecord {
+ entityChange: EntityChange;
+ entity?: EntityRow;
+}
+
+type TaskDataDefinitions = {
+ empty: null,
+ deleteNotes: null,
+ undeleteNotes: null,
+ export: null,
+ protectNotes: {
+ protect: boolean;
+ }
+ importNotes: {
+ textImportedAsText?: boolean;
+ codeImportedAsCode?: boolean;
+ replaceUnderscoresWithSpaces?: boolean;
+ shrinkImages?: boolean;
+ safeImport?: boolean;
+ } | null,
+ importAttachments: null
+}
+
+type TaskResultDefinitions = {
+ empty: null,
+ deleteNotes: null,
+ undeleteNotes: null,
+ export: null,
+ protectNotes: null,
+ importNotes: {
+ parentNoteId?: string;
+ importedNoteId?: string
+ };
+ importAttachments: {
+ parentNoteId?: string;
+ importedNoteId?: string
+ };
+}
+
+export type TaskType = keyof TaskDataDefinitions | keyof TaskResultDefinitions;
+export type TaskData = TaskDataDefinitions[T];
+export type TaskResult = TaskResultDefinitions[T];
+
+type TaskDefinition = {
+ type: "taskProgressCount",
+ taskId: string;
+ taskType: T;
+ data: TaskData,
+ progressCount: number
+} | {
+ type: "taskError",
+ taskId: string;
+ taskType: T;
+ data: TaskData,
+ message: string;
+} | {
+ type: "taskSucceeded",
+ taskId: string;
+ taskType: T;
+ data: TaskData,
+ result: TaskResult;
+}
+
+export interface OpenedFileUpdateStatus {
+ entityType: string;
+ entityId: string;
+ lastModifiedMs?: number;
+ filePath: string;
+}
+
+type AllTaskDefinitions =
+ | TaskDefinition<"empty">
+ | TaskDefinition<"deleteNotes">
+ | TaskDefinition<"undeleteNotes">
+ | TaskDefinition<"export">
+ | TaskDefinition<"protectNotes">
+ | TaskDefinition<"importNotes">
+ | TaskDefinition<"importAttachments">;
+
+export type WebSocketMessage = AllTaskDefinitions | {
+ type: "ping"
+} | {
+ type: "frontend-update",
+ data: {
+ lastSyncedPush: number,
+ entityChanges: EntityChange[]
+ }
+} | {
+ type: "openNote",
+ noteId: string
+} | OpenedFileUpdateStatus & {
+ type: "openedFileUpdated"
+} | {
+ type: "protectedSessionLogin"
+} | {
+ type: "protectedSessionLogout"
+} | {
+ type: "toast",
+ message: string;
+} | {
+ type: "api-log-messages",
+ noteId: string,
+ messages: string[]
+} | {
+ type: "execute-script";
+ script: string;
+ params: unknown[];
+ startNoteId?: string;
+ currentNoteId: string;
+ originEntityName: string;
+ originEntityId?: string | null;
+} | {
+ type: "reload-frontend";
+ reason: string;
+} | {
+ type: "sync-pull-in-progress" | "sync-push-in-progress" | "sync-finished" | "sync-failed";
+ lastSyncedPush: number;
+} | {
+ type: "consistency-checks-failed"
+} | {
+ type: "llm-stream",
+ chatNoteId: string;
+ done?: boolean;
+ error?: string;
+ thinking?: string;
+ content?: string;
+ toolExecution?: {
+ action?: string;
+ tool?: string;
+ toolCallId?: string;
+ result?: string | Record;
+ error?: string;
+ args?: Record;
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1475e2ebb..00c3b4989 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15149,8 +15149,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 46.1.0
'@ckeditor/ckeditor5-undo': 46.1.0
ckeditor5: 46.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-export-inline-styles@46.1.0':
dependencies:
@@ -17298,7 +17296,7 @@ snapshots:
'@jridgewell/source-map@0.3.10':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.29
+ '@jridgewell/trace-mapping': 0.3.31
'@jridgewell/source-map@0.3.6':
dependencies:
@@ -19438,7 +19436,6 @@ snapshots:
'@types/node@24.3.0':
dependencies:
undici-types: 7.10.0
- optional: true
'@types/parse-json@4.0.2': {}
@@ -24353,7 +24350,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 22.18.1
+ '@types/node': 24.3.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -29474,8 +29471,7 @@ snapshots:
undici-types@6.21.0: {}
- undici-types@7.10.0:
- optional: true
+ undici-types@7.10.0: {}
undici@6.21.3: {}