Compare commits

...

71 Commits

Author SHA1 Message Date
zadam
0a0de7312c release 0.40.7 2020-03-28 20:58:20 +01:00
zadam
b4b22d9353 workaround for overflowing component wrapper in landscape mobile frontend, fixes #933 2020-03-28 19:39:14 +01:00
zadam
d3eb640aa2 fix upload image from mobile frontend, closes #931 2020-03-28 19:25:19 +01:00
zadam
8ccc48c25d images in include note should have max 100% width, #922 2020-03-21 09:52:13 +01:00
zadam
9a1a76605a fix OPML export of book notes, closes #919 2020-03-19 09:42:41 +01:00
zadam
a717ee00fb release 0.40.6 2020-03-15 11:21:43 +01:00
zadam
f5e27278ab fix migration script to preserve sync IDs 2020-03-15 11:19:30 +01:00
zadam
20c24e26cc added entity constructors for "content" tables 2020-03-14 21:09:07 +01:00
zadam
3bafc396fc fix consistency checks 2020-03-14 13:13:27 +01:00
zadam
3fa3e912a4 not sending ping to clients after every sync addition, only after commit which significantly speeds up imports 2020-03-14 12:39:55 +01:00
zadam
44219e7ccc do not load the note content during ENEX import again since it's already available 2020-03-13 22:23:44 +01:00
zadam
5b67854cbe clear history in the code mirror after setting a value, closes #766 2020-03-10 22:48:21 +01:00
zadam
c6d912dcb7 sync only changes with isSynced = true 2020-03-09 21:34:03 +01:00
zadam
da53c1eaa8 updated schema.sql 2020-03-09 21:23:11 +01:00
zadam
73bf2dcb02 added isSynced to sync table to allow forward compatibility with 0.41 2020-03-09 20:56:43 +01:00
zadam
719f10981e fix sync 2020-03-08 22:00:12 +01:00
zadam
deb67d6275 release 0.40.5 2020-03-08 21:05:52 +01:00
zadam
e4039ea5e1 fixed printing relation map 2020-03-08 09:24:03 +01:00
zadam
78a50be663 don't print control buttons 2020-03-07 22:18:12 +01:00
zadam
3d3ad3b99b include themes.css for printing to have default CSS variables available 2020-03-07 22:14:07 +01:00
zadam
0d9cdcac85 load appropriate styles for printing book notes, closes #899 2020-03-07 22:01:34 +01:00
zadam
350331e2ef fix 500px constant height of relation maps, closes #882 2020-02-28 15:20:54 +01:00
zadam
e8a9e49e9e release 0.40.4 2020-02-24 22:59:22 +01:00
zadam
fb55cdaea6 release 0.40.4 2020-02-24 22:58:18 +01:00
zadam
b9b2cc8364 make sure $rendered is always jquery object 2020-02-24 22:55:12 +01:00
zadam
8dfdd090f5 use note's css class also in book and included note, closes #879 2020-02-24 22:46:27 +01:00
zadam
fe7705524a fix include note in mobile frontend, closes #878 2020-02-24 22:37:45 +01:00
zadam
2b1b7774f8 make images in text notes rendered responsively in a book, fixes #871 2020-02-19 22:24:31 +01:00
zadam
2d58019d6e allow to setup web version as a sync client 2020-02-19 22:10:40 +01:00
zadam
fe31f08c0d show setup window if DB is not initialized 2020-02-19 22:09:49 +01:00
zadam
ad7a55d305 always autofix note_contents.content = NULL sync issue after note erasion 2020-02-19 19:51:36 +01:00
zadam
4ce4ac9584 release 0.40.3 2020-02-09 10:48:23 +01:00
zadam
88bd65c679 external links are also parsed and label is created for them, closes #851 2020-02-09 10:45:07 +01:00
zadam
9eab3026bb display advanced item in the tree context menu in the middle to avoid being hidden, closes #853 2020-02-09 10:15:35 +01:00
zadam
7abaedbf31 add possibility to change clipper parent to a fixed note instead of day notes, fixes #854 2020-02-09 10:12:02 +01:00
zadam
402718d293 return focus to the previously focused element after closing the dialog, fixes #861 2020-02-09 10:00:13 +01:00
zadam
990a84c202 Merge remote-tracking branch 'origin/master' 2020-02-09 08:38:47 +01:00
zadam
d8e181a828 Merge branch 'stable'
# Conflicts:
#	package.json
#	src/services/build.js
2020-02-09 08:38:29 +01:00
jasontan056
adb8caa8a2 Fix corner case preventing notes from being created before ckeditor is initialized (#849)
* Pass deleteId to deleteBranch in ensureNoteIsAbsentFromParent

* Add checks for whether window.cutToNote is defined.

* check ckEditor initialized.
2020-02-02 09:28:19 +01:00
zadam
7651c53363 release 0.40.2 2020-02-01 10:17:51 +01:00
zadam
0f25c8a95f autobook should not be active on the mobile interface 2020-02-01 10:17:03 +01:00
zadam
1a49894adf fix tree loading on mobile interface, closes #839 2020-02-01 10:04:18 +01:00
zadam
bd8c078fb9 Merge remote-tracking branch 'origin/master' 2020-02-01 09:30:14 +01:00
zadam
6e060b87b8 fix date parsing in local timezone, closes #845 2020-02-01 09:29:56 +01:00
jasontan056
2375b170ba Pass deleteId to deleteBranch in ensureNoteIsAbsentFromParent (#846) 2020-02-01 09:05:23 +01:00
zadam
828cce0d78 release 0.40.1 2020-01-19 15:45:06 +01:00
zadam
ab535bf147 fixes of the new CopyWithoutFormatting 2020-01-19 09:25:35 +01:00
Heniker
1876664dfb add hotkey to copy contents with line breaks, fixes #349 (#831) 2020-01-19 09:16:36 +01:00
zadam
1690248e24 migration script to fix contentLength = -1 in new notes 2020-01-19 09:08:33 +01:00
zadam
cbeb8ea17e fix setting contentLength 2020-01-19 09:03:26 +01:00
zadam
9a13edd490 release 0.39.6 2020-01-18 20:52:14 +01:00
zadam
c9113ae752 Merge branch 'stable' 2020-01-18 09:24:39 +01:00
zadam
0ec11d29ba fix creating root calendar note when missing, #752 2020-01-18 08:59:46 +01:00
zadam
a6cd25071e more robust handling of sync error, fixes #830 2020-01-18 08:48:36 +01:00
zadam
20fdeee048 better error handling for search notes 2020-01-13 19:35:06 +01:00
zadam
a79a063d17 release 0.40.0-beta 2020-01-11 09:54:31 +01:00
zadam
5e91b1b5e0 package updates 2020-01-11 09:50:05 +01:00
zadam
7877443fb4 Merge branch 'stable' 2020-01-11 09:41:42 +01:00
zadam
759e47bfcf using included note should create a relation, closes #820 2020-01-10 21:41:00 +01:00
zadam
67bdffb27b expose text editor instance and method to add text to editor, closes #819 2020-01-10 20:10:17 +01:00
zadam
3386dace3b provide context menu in text editor also with disabled spellcheck 2020-01-10 19:56:27 +01:00
zadam
f3a2e2cbde Merge branch 'stable' 2020-01-08 21:39:17 +01:00
zadam
4c7c3105e8 Merge branch 'stable'
# Conflicts:
#	src/services/notes.js
2020-01-07 22:29:54 +01:00
zadam
1cd2711097 Merge branch 'stable' 2020-01-05 20:02:19 +01:00
zadam
54d89a9f47 allow configuring the time period before deleted notes are erased (and changed the default to 7 days) 2020-01-03 22:32:49 +01:00
zadam
1699646b39 fix undelete info messages 2020-01-03 21:32:41 +01:00
zadam
94a0a31f17 refactoring of tree handling to recommended fancytree 2020-01-03 21:15:45 +01:00
zadam
10219fb9dd Merge branch 'stable' 2020-01-03 20:13:13 +01:00
zadam
17b23d92ef delete/undelete fixes 2020-01-03 13:41:44 +01:00
zadam
14f3c783f2 undelete note WIP 2020-01-03 13:14:43 +01:00
zadam
c1d0a1e07b undelete note WIP 2020-01-03 10:48:36 +01:00
97 changed files with 1471 additions and 640 deletions

3
.idea/.gitignore generated vendored
View File

@@ -2,4 +2,5 @@
/workspace.xml
# Datasource local storage ignored files
/dataSources.local.xml
/dataSources.local.xml
/dataSources/

3
.idea/dataSources.xml generated
View File

@@ -6,9 +6,6 @@
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source>
</component>
</project>

View File

@@ -2,7 +2,7 @@
<dataSource name="document.db">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17">
<root id="1">
<ServerVersion>3.16.1</ServerVersion>
<ServerVersion>3.25.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
@@ -111,529 +111,550 @@
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="35" parent="7" name="hash">
<column id="35" parent="7" name="deleteId">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="36" parent="7" name="hash">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="36" parent="7" name="isInheritable">
<Position>11</Position>
<column id="37" parent="7" name="isInheritable">
<Position>12</Position>
<DataType>int|0s</DataType>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="37" parent="7" name="sqlite_autoindex_attributes_1">
<index id="38" parent="7" name="sqlite_autoindex_attributes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>attributeId</ColNames>
<Unique>1</Unique>
</index>
<index id="38" parent="7" name="IDX_attributes_noteId_index">
<index id="39" parent="7" name="IDX_attributes_noteId_index">
<ColNames>noteId</ColNames>
</index>
<index id="39" parent="7" name="IDX_attributes_name_value">
<index id="40" parent="7" name="IDX_attributes_name_value">
<ColNames>name
value</ColNames>
</index>
<index id="40" parent="7" name="IDX_attributes_value_index">
<index id="41" parent="7" name="IDX_attributes_value_index">
<ColNames>value</ColNames>
</index>
<key id="41" parent="7">
<key id="42" parent="7">
<ColNames>attributeId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName>
</key>
<column id="42" parent="8" name="branchId">
<column id="43" parent="8" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="43" parent="8" name="noteId">
<column id="44" parent="8" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="8" name="parentNoteId">
<column id="45" parent="8" name="parentNoteId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="8" name="notePosition">
<column id="46" parent="8" name="notePosition">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="46" parent="8" name="prefix">
<column id="47" parent="8" name="prefix">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="47" parent="8" name="isExpanded">
<column id="48" parent="8" name="isExpanded">
<Position>6</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="48" parent="8" name="isDeleted">
<column id="49" parent="8" name="isDeleted">
<Position>7</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="49" parent="8" name="utcDateModified">
<column id="50" parent="8" name="deleteId">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="50" parent="8" name="utcDateCreated">
<column id="51" parent="8" name="utcDateModified">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="8" name="hash">
<column id="52" parent="8" name="utcDateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="53" parent="8" name="hash">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="52" parent="8" name="sqlite_autoindex_branches_1">
<index id="54" parent="8" name="sqlite_autoindex_branches_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<Unique>1</Unique>
</index>
<index id="53" parent="8" name="IDX_branches_noteId_parentNoteId">
<index id="55" parent="8" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
</index>
<index id="54" parent="8" name="IDX_branches_parentNoteId">
<index id="56" parent="8" name="IDX_branches_parentNoteId">
<ColNames>parentNoteId</ColNames>
</index>
<key id="55" parent="8">
<key id="57" parent="8">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="56" parent="9" name="noteId">
<column id="58" parent="9" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="57" parent="9" name="content">
<column id="59" parent="9" name="content">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="58" parent="9" name="hash">
<column id="60" parent="9" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="59" parent="9" name="utcDateModified">
<column id="61" parent="9" name="utcDateModified">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="60" parent="9" name="sqlite_autoindex_note_contents_1">
<index id="62" parent="9" name="sqlite_autoindex_note_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<Unique>1</Unique>
</index>
<key id="61" parent="9">
<key id="63" parent="9">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_contents_1</UnderlyingIndexName>
</key>
<column id="62" parent="10" name="noteRevisionId">
<column id="64" parent="10" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="63" parent="10" name="content">
<column id="65" parent="10" name="content">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="64" parent="10" name="hash">
<column id="66" parent="10" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="65" parent="10" name="utcDateModified">
<column id="67" parent="10" name="utcDateModified">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="66" parent="10" name="sqlite_autoindex_note_revision_contents_1">
<index id="68" parent="10" name="sqlite_autoindex_note_revision_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<Unique>1</Unique>
</index>
<key id="67" parent="10">
<key id="69" parent="10">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revision_contents_1</UnderlyingIndexName>
</key>
<column id="68" parent="11" name="noteRevisionId">
<column id="70" parent="11" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="69" parent="11" name="noteId">
<column id="71" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="70" parent="11" name="title">
<column id="72" parent="11" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="71" parent="11" name="contentLength">
<column id="73" parent="11" name="contentLength">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="72" parent="11" name="isErased">
<column id="74" parent="11" name="isErased">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="73" parent="11" name="isProtected">
<column id="75" parent="11" name="isProtected">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="74" parent="11" name="utcDateLastEdited">
<column id="76" parent="11" name="utcDateLastEdited">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="75" parent="11" name="utcDateCreated">
<column id="77" parent="11" name="utcDateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="76" parent="11" name="utcDateModified">
<column id="78" parent="11" name="utcDateModified">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="77" parent="11" name="dateLastEdited">
<column id="79" parent="11" name="dateLastEdited">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="78" parent="11" name="dateCreated">
<column id="80" parent="11" name="dateCreated">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="79" parent="11" name="type">
<column id="81" parent="11" name="type">
<Position>12</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="80" parent="11" name="mime">
<column id="82" parent="11" name="mime">
<Position>13</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="81" parent="11" name="hash">
<column id="83" parent="11" name="hash">
<Position>14</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<index id="82" parent="11" name="sqlite_autoindex_note_revisions_1">
<index id="84" parent="11" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<Unique>1</Unique>
</index>
<index id="83" parent="11" name="IDX_note_revisions_noteId">
<index id="85" parent="11" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
</index>
<index id="84" parent="11" name="IDX_note_revisions_utcDateLastEdited">
<index id="86" parent="11" name="IDX_note_revisions_utcDateLastEdited">
<ColNames>utcDateLastEdited</ColNames>
</index>
<index id="85" parent="11" name="IDX_note_revisions_utcDateCreated">
<index id="87" parent="11" name="IDX_note_revisions_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
</index>
<index id="86" parent="11" name="IDX_note_revisions_dateLastEdited">
<index id="88" parent="11" name="IDX_note_revisions_dateLastEdited">
<ColNames>dateLastEdited</ColNames>
</index>
<index id="87" parent="11" name="IDX_note_revisions_dateCreated">
<index id="89" parent="11" name="IDX_note_revisions_dateCreated">
<ColNames>dateCreated</ColNames>
</index>
<key id="88" parent="11">
<key id="90" parent="11">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="89" parent="12" name="noteId">
<column id="91" parent="12" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="90" parent="12" name="title">
<column id="92" parent="12" name="title">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;note&quot;</DefaultExpression>
</column>
<column id="91" parent="12" name="contentLength">
<column id="93" parent="12" name="contentLength">
<Position>3</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="92" parent="12" name="isProtected">
<column id="94" parent="12" name="isProtected">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="93" parent="12" name="type">
<column id="95" parent="12" name="type">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="94" parent="12" name="mime">
<column id="96" parent="12" name="mime">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column>
<column id="95" parent="12" name="hash">
<column id="97" parent="12" name="hash">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="96" parent="12" name="isDeleted">
<column id="98" parent="12" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="97" parent="12" name="isErased">
<column id="99" parent="12" name="deleteId">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="100" parent="12" name="isErased">
<Position>10</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="98" parent="12" name="dateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="99" parent="12" name="dateModified">
<column id="101" parent="12" name="dateCreated">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="100" parent="12" name="utcDateCreated">
<column id="102" parent="12" name="dateModified">
<Position>12</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="101" parent="12" name="utcDateModified">
<column id="103" parent="12" name="utcDateCreated">
<Position>13</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="102" parent="12" name="sqlite_autoindex_notes_1">
<column id="104" parent="12" name="utcDateModified">
<Position>14</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="105" parent="12" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<Unique>1</Unique>
</index>
<index id="103" parent="12" name="IDX_notes_title">
<index id="106" parent="12" name="IDX_notes_title">
<ColNames>title</ColNames>
</index>
<index id="104" parent="12" name="IDX_notes_type">
<index id="107" parent="12" name="IDX_notes_type">
<ColNames>type</ColNames>
</index>
<index id="105" parent="12" name="IDX_notes_isDeleted">
<index id="108" parent="12" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
</index>
<index id="106" parent="12" name="IDX_notes_dateCreated">
<index id="109" parent="12" name="IDX_notes_dateCreated">
<ColNames>dateCreated</ColNames>
</index>
<index id="107" parent="12" name="IDX_notes_dateModified">
<index id="110" parent="12" name="IDX_notes_dateModified">
<ColNames>dateModified</ColNames>
</index>
<index id="108" parent="12" name="IDX_notes_utcDateCreated">
<index id="111" parent="12" name="IDX_notes_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
</index>
<index id="109" parent="12" name="IDX_notes_utcDateModified">
<index id="112" parent="12" name="IDX_notes_utcDateModified">
<ColNames>utcDateModified</ColNames>
</index>
<key id="110" parent="12">
<key id="113" parent="12">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="111" parent="13" name="name">
<column id="114" parent="13" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="112" parent="13" name="value">
<column id="115" parent="13" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="113" parent="13" name="isSynced">
<column id="116" parent="13" name="isSynced">
<Position>3</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="114" parent="13" name="hash">
<column id="117" parent="13" name="hash">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="115" parent="13" name="utcDateCreated">
<column id="118" parent="13" name="utcDateCreated">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="116" parent="13" name="utcDateModified">
<column id="119" parent="13" name="utcDateModified">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="117" parent="13" name="sqlite_autoindex_options_1">
<index id="120" parent="13" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<Unique>1</Unique>
</index>
<key id="118" parent="13">
<key id="121" parent="13">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="119" parent="14" name="noteId">
<column id="122" parent="14" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="120" parent="14" name="notePath">
<column id="123" parent="14" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="121" parent="14" name="hash">
<column id="124" parent="14" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="122" parent="14" name="utcDateCreated">
<column id="125" parent="14" name="utcDateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="123" parent="14" name="isDeleted">
<column id="126" parent="14" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
</column>
<index id="124" parent="14" name="sqlite_autoindex_recent_notes_1">
<index id="127" parent="14" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<Unique>1</Unique>
</index>
<key id="125" parent="14">
<key id="128" parent="14">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="126" parent="15" name="sourceId">
<column id="129" parent="15" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="127" parent="15" name="utcDateCreated">
<column id="130" parent="15" name="utcDateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="128" parent="15" name="sqlite_autoindex_source_ids_1">
<index id="131" parent="15" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<Unique>1</Unique>
</index>
<index id="129" parent="15" name="IDX_source_ids_utcDateCreated">
<index id="132" parent="15" name="IDX_source_ids_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
</index>
<key id="130" parent="15">
<key id="133" parent="15">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="131" parent="16" name="type">
<column id="134" parent="16" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="132" parent="16" name="name">
<column id="135" parent="16" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="133" parent="16" name="tbl_name">
<column id="136" parent="16" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="134" parent="16" name="rootpage">
<column id="137" parent="16" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
<DataType>int|0s</DataType>
</column>
<column id="135" parent="16" name="sql">
<column id="138" parent="16" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="136" parent="17" name="name">
<column id="139" parent="17" name="name">
<Position>1</Position>
</column>
<column id="137" parent="17" name="seq">
<column id="140" parent="17" name="seq">
<Position>2</Position>
</column>
<column id="138" parent="18" name="id">
<column id="141" parent="18" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="139" parent="18" name="entityName">
<column id="142" parent="18" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="140" parent="18" name="entityId">
<column id="143" parent="18" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="141" parent="18" name="sourceId">
<column id="144" parent="18" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="142" parent="18" name="utcSyncDate">
<column id="145" parent="18" name="isSynced">
<Position>5</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="146" parent="18" name="utcSyncDate">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="143" parent="18" name="IDX_sync_entityName_entityId">
<index id="147" parent="18" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<Unique>1</Unique>
</index>
<index id="144" parent="18" name="IDX_sync_utcSyncDate">
<index id="148" parent="18" name="IDX_sync_utcSyncDate">
<ColNames>utcSyncDate</ColNames>
</index>
<key id="145" parent="18">
<key id="149" parent="18">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>

View File

@@ -0,0 +1,81 @@
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
INSERT INTO notes_mig (noteId, title, contentLength, isProtected, type, mime, hash, isDeleted, isErased, dateCreated, dateModified, utcDateCreated, utcDateModified)
SELECT noteId, title, -1, isProtected, type, mime, hash, isDeleted, isErased, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "branches_mig" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
INSERT INTO branches_mig (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated, hash)
SELECT branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated, hash FROM branches;
DROP TABLE branches;
ALTER TABLE branches_mig RENAME TO branches;
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
INSERT INTO attributes_mig (attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, hash, isInheritable)
SELECT attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, hash, isInheritable FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);

View File

@@ -0,0 +1 @@
UPDATE notes SET contentLength = COALESCE((SELECT COALESCE(LENGTH(content), 0) FROM note_contents WHERE note_contents.noteId = notes.noteId), -1);

View File

@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS "sync_mig" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER default 0 not null,
`utcSyncDate` TEXT NOT NULL);
INSERT INTO sync_mig (id, entityName, entityId, sourceId, isSynced, utcSyncDate)
SELECT id, entityName, entityId, sourceId, 1, utcSyncDate FROM sync;
DROP TABLE sync;
ALTER TABLE sync_mig RENAME TO sync;
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
`utcSyncDate`
);

View File

@@ -1,16 +1,8 @@
CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`utcSyncDate` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "source_ids" (
`sourceId` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
@@ -27,32 +19,6 @@ CREATE TABLE IF NOT EXISTS "options"
utcDateCreated TEXT not null,
utcDateModified TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "attributes"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
`utcSyncDate`
);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
CREATE TABLE IF NOT EXISTS "note_contents" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
@@ -68,46 +34,10 @@ CREATE TABLE recent_notes
utcDateCreated TEXT not null,
isDeleted INT
);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT,
hash TEXT DEFAULT '' NOT NULL,
`utcDateModified` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
`title` TEXT,
@@ -127,3 +57,77 @@ CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCr
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "attributes"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER default 0 not null,
`utcSyncDate` TEXT NOT NULL);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
`utcSyncDate`
);

View File

@@ -1240,6 +1240,143 @@
<h4 class="name" id="addTextToActiveTabEditor"><span class="type-signature"></span>addTextToActiveTabEditor<span class="signature">(text)</span><span class="type-signature"></span></h4>
<div class="description">
Adds given text to the editor cursor
</div>
<h5>Parameters:</h5>
<table class="params">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>text</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line305">line 305</a>
</li></ul></dd>
</dl>
@@ -1366,7 +1503,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line390">line 390</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line406">line 406</a>
</li></ul></dd>
@@ -1785,7 +1922,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line303">line 303</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line311">line 311</a>
</li></ul></dd>
@@ -1891,7 +2028,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line309">line 309</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line325">line 325</a>
</li></ul></dd>
@@ -1949,6 +2086,119 @@
<h4 class="name" id="getActiveTabTextEditor"><span class="type-signature"></span>getActiveTabTextEditor<span class="signature">()</span><span class="type-signature"> &rarr; {Editor|null}</span></h4>
<div class="description">
See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
</div>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line319">line 319</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<div class="param-desc">
CKEditor instance or null (e.g. if active note is not a text note)
</div>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Editor</span>
|
<span class="param-type">null</span>
</dd>
</dl>
<h4 class="name" id="getDateNote"><span class="type-signature"></span>getDateNote<span class="signature">(date)</span><span class="type-signature"> &rarr; {Promise.&lt;<a href="NoteShort.html">NoteShort</a>>}</span></h4>
@@ -2050,7 +2300,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line356">line 356</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line372">line 372</a>
</li></ul></dd>
@@ -2312,7 +2562,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line365">line 365</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line381">line 381</a>
</li></ul></dd>
@@ -2775,7 +3025,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line347">line 347</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line363">line 363</a>
</li></ul></dd>
@@ -2930,7 +3180,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line374">line 374</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line390">line 390</a>
</li></ul></dd>
@@ -3039,7 +3289,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line320">line 320</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line336">line 336</a>
</li></ul></dd>
@@ -3194,7 +3444,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line328">line 328</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line344">line 344</a>
</li></ul></dd>
@@ -3433,7 +3683,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line339">line 339</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line355">line 355</a>
</li></ul></dd>
@@ -4606,7 +4856,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line383">line 383</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line399">line 399</a>
</li></ul></dd>
@@ -4757,7 +5007,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line334">line 334</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line350">line 350</a>
</li></ul></dd>
@@ -5123,7 +5373,7 @@ Typical use case is when new note has been created, we should wait until it is s
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line401">line 401</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line417">line 417</a>
</li></ul></dd>

View File

@@ -324,12 +324,28 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
*/
this.createNoteLink = linkService.createNoteLink;
/**
* Adds given text to the editor cursor
*
* @param {string} text
* @method
*/
this.addTextToActiveTabEditor = linkService.addTextToEditor;
/**
* @method
* @returns {NoteFull} active note (loaded into right pane)
*/
this.getActiveTabNote = noteDetailService.getActiveTabNote;
/**
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
*
* @method
* @returns {Editor|null} CKEditor instance or null (e.g. if active note is not a text note)
*/
this.getActiveTabTextEditor = noteDetailService.getActiveEditor;
/**
* @method
* @returns {Promise&lt;string|null>} returns note path of active note or null if there isn't active note

View File

@@ -26,9 +26,9 @@ app.on('ready', async () => {
await sqlInit.dbConnection;
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
// if db is not initialized -> setup process
// if db is initialized, then we need to wait until the migration process is finished
if (await sqlInit.isDbInitialized()) {
await sqlInit.dbReady;
await windowService.createMainWindow();

115
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.39.3",
"version": "0.40.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -552,6 +552,11 @@
"defer-to-connect": "^1.0.1"
}
},
"@tokenizer/token": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz",
"integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w=="
},
"@types/debug": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
@@ -975,12 +980,11 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.1.tgz",
"integrity": "sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
"follow-redirects": "1.5.10"
}
},
"bagpipe": {
@@ -1771,13 +1775,13 @@
}
},
"commonmark": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.29.0.tgz",
"integrity": "sha512-Wc3kvAIm0EK85pHsM95Fev31wEN6/zQpwd2qcLDL8psjHRoUFvUeGHevIJAdToWUuFoX8WI/gmeDauqy32xgJQ==",
"version": "0.29.1",
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.29.1.tgz",
"integrity": "sha512-DafPdNYFXoEhsSiR4O+dJ45UJBfDL4cBTks4B+agKiaWt7qjG0bIhg5xuCE0RqU71ikJcBIf4/sRHh9vYQVF8Q==",
"requires": {
"entities": "~ 1.1.1",
"mdurl": "~ 1.0.1",
"minimist": "~ 1.2.0",
"entities": "~1.1.1",
"mdurl": "~1.0.1",
"minimist": "~1.2.0",
"string.prototype.repeat": "^0.2.0"
}
},
@@ -2081,9 +2085,9 @@
"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI="
},
"dayjs": {
"version": "1.8.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.18.tgz",
"integrity": "sha512-JBMJZghNK8TtuoPnKNIzW9xavVVigld/zmZNpZSyQbkb2Opp55YIfZUpE4OEqPF/iyUVQTKcn1bC2HtC8B7s3g=="
"version": "1.8.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.19.tgz",
"integrity": "sha512-7kqOoj3oQSmqbvtvGFLU5iYqies+SqUiEGNT0UtUPPxcPYgY1BrkXR0Cq2R9HYSimBXN+xHkEN4Hi399W+Ovlg=="
},
"debug": {
"version": "4.1.1",
@@ -3619,9 +3623,15 @@
}
},
"file-type": {
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz",
"integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg=="
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-13.0.3.tgz",
"integrity": "sha512-fe3+9mnMLbpP8nRQm/cJgehypv375SOFOXFdLYyYe7hWxdWgS5hkDpWnld5At/vvfVEN6rVqKA3EOKMP03tJGg==",
"requires": {
"readable-web-to-node-stream": "^2.0.0",
"strtok3": "^5.0.1",
"token-types": "^2.0.0",
"typedarray-to-buffer": "^3.1.5"
}
},
"filename-regex": {
"version": "2.0.1",
@@ -4772,6 +4782,13 @@
"make-dir": "^3.0.0",
"p-pipe": "^3.0.0",
"replace-ext": "^1.0.0"
},
"dependencies": {
"file-type": {
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz",
"integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg=="
}
}
},
"imagemin-giflossy": {
@@ -4969,11 +4986,6 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
},
"is-bzip2": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-bzip2/-/is-bzip2-1.0.0.tgz",
@@ -5973,17 +5985,17 @@
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.25",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz",
"integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==",
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"requires": {
"mime-db": "1.42.0"
"mime-db": "1.43.0"
},
"dependencies": {
"mime-db": {
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz",
"integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ=="
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
}
}
},
@@ -7463,6 +7475,11 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
},
"peek-readable": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz",
"integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA=="
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -8436,6 +8453,11 @@
"util-deprecate": "~1.0.1"
}
},
"readable-web-to-node-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz",
"integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA=="
},
"redent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@@ -9162,9 +9184,9 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-similarity": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-3.0.0.tgz",
"integrity": "sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ=="
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.1.tgz",
"integrity": "sha512-v36MJzloekKVvKAsYi6O/qpn2mIuvwEFIT9Gx3yg4spkNjXYsk7yxc37g4ZTyMVIBvt/9PZGxnqEtme8XHK+Mw=="
},
"string-width": {
"version": "1.0.2",
@@ -9271,6 +9293,16 @@
"escape-string-regexp": "^1.0.2"
}
},
"strtok3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-5.0.1.tgz",
"integrity": "sha512-AWliiIjyb87onqO8pM+1Hozm+PPcR4YYIWbFUT5OKQ+tOMwgdT8HwJd/IS8v3/gKdAtE5aE2p3FhcWqryuZPLQ==",
"requires": {
"@tokenizer/token": "^0.1.0",
"debug": "^4.1.1",
"peek-readable": "^3.1.0"
}
},
"sum-up": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz",
@@ -9560,6 +9592,15 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"token-types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz",
"integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==",
"requires": {
"@tokenizer/token": "^0.1.0",
"ieee754": "^1.1.13"
}
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
@@ -9673,6 +9714,14 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.39.5",
"version": "0.40.7",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -21,13 +21,13 @@
},
"dependencies": {
"async-mutex": "0.1.4",
"axios": "0.19.0",
"axios": "0.19.1",
"body-parser": "1.19.0",
"cls-hooked": "4.2.2",
"commonmark": "0.29.0",
"commonmark": "0.29.1",
"cookie-parser": "1.4.4",
"csurf": "1.10.0",
"dayjs": "1.8.18",
"dayjs": "1.8.19",
"debug": "4.1.1",
"ejs": "2.7.4",
"electron-debug": "3.0.1",
@@ -37,7 +37,7 @@
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.0",
"file-type": "12.4.2",
"file-type": "13.0.3",
"fs-extra": "8.1.0",
"helmet": "3.21.2",
"html": "1.0.0",
@@ -51,7 +51,7 @@
"imagemin-pngquant": "8.0.0",
"ini": "1.3.5",
"jimp": "0.9.3",
"mime-types": "2.1.25",
"mime-types": "2.1.26",
"moment": "2.24.0",
"multer": "1.4.2",
"node-abi": "2.13.0",
@@ -69,7 +69,7 @@
"simple-node-logger": "18.12.23",
"sqlite": "3.0.3",
"sqlite3": "4.1.1",
"string-similarity": "3.0.0",
"string-similarity": "4.0.1",
"tar-stream": "2.1.0",
"turndown": "5.0.3",
"turndown-plugin-gfm": "1.0.2",

View File

@@ -16,6 +16,7 @@ const sql = require('../services/sql');
* @property {int} position
* @property {boolean} isInheritable
* @property {boolean} isDeleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {string} utcDateCreated
* @property {string} utcDateModified
*

View File

@@ -16,6 +16,7 @@ const sql = require('../services/sql');
* @property {string} prefix
* @property {boolean} isExpanded
* @property {boolean} isDeleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {string} utcDateModified
* @property {string} utcDateCreated
*
@@ -25,7 +26,7 @@ class Branch extends Entity {
static get entityName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "deleteId", "prefix"]; }
constructor(row = {}) {
super(row);

View File

@@ -11,7 +11,9 @@ const ENTITY_NAME_TO_ENTITY = {
"attributes": Attribute,
"branches": Branch,
"notes": Note,
"note_contents": Note,
"note_revisions": NoteRevision,
"note_revision_contents": NoteRevision,
"recent_notes": RecentNote,
"options": Option,
"api_tokens": ApiToken,

View File

@@ -24,6 +24,7 @@ const RELATION_DEFINITION = 'relation-definition';
* @property {int} contentLength - length of content
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {boolean} isErased - true if note's content is erased after it has been deleted
* @property {string} dateCreated - local date time (with offset)
* @property {string} dateModified - local date time (with offset)
@@ -35,7 +36,7 @@ const RELATION_DEFINITION = 'relation-definition';
class Note extends Entity {
static get entityName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "type", "isProtected", "isDeleted"]; }
static get hashedProperties() { return ["noteId", "title", "type", "isProtected", "isDeleted", "deleteId"]; }
/**
* @param row - object containing database row from "notes" table
@@ -109,6 +110,10 @@ class Note extends Entity {
async getJsonContent() {
const content = await this.getContent();
if (!content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
@@ -118,9 +123,11 @@ class Note extends Entity {
throw new Error(`Cannot set null content to note ${this.noteId}`);
}
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
// force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.length;
this.contentLength = content.byteLength;
await this.save();
this.content = content;
@@ -129,7 +136,7 @@ class Note extends Entity {
noteId: this.noteId,
content: content,
utcDateModified: dateUtils.utcNowDateTime(),
hash: utils.hash(this.noteId + "|" + content)
hash: utils.hash(this.noteId + "|" + content.toString())
};
if (this.isProtected) {
@@ -804,8 +811,10 @@ class Note extends Entity {
FROM attributes
WHERE noteId = ? AND
isDeleted = 0 AND
type = 'relation' AND
name IN ('internalLink', 'imageLink', 'relationMapLink')`, [this.noteId]);
((type = 'relation' AND
name IN ('internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'))
OR
(type = 'label' AND name = 'externalLink'))`, [this.noteId]);
}
/**

View File

@@ -161,7 +161,13 @@ async function printActiveNote() {
importCSS: false,
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css"
"libraries/ckeditor/ckeditor-content.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/bootstrap/css/bootstrap.min.css",
"stylesheets/print.css",
"stylesheets/relation_map.css",
"stylesheets/themes.css",
"stylesheets/detail.css"
],
debug: true
});

View File

@@ -10,8 +10,6 @@ const $buildRevision = $("#build-revision");
const $dataDirectory = $("#data-directory");
export async function showDialog() {
utils.closeActiveDialog();
const appInfo = await server.get('app-info');
$appVersion.text(appInfo.appVersion);
@@ -22,7 +20,5 @@ export async function showDialog() {
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
$dataDirectory.text(appInfo.dataDirectory);
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
}

View File

@@ -11,13 +11,9 @@ const $linkTitle = $("#link-title");
const $addLinkTitleFormGroup = $("#add-link-title-form-group");
export async function showDialog() {
utils.closeActiveDialog();
$addLinkTitleFormGroup.toggle(!hasSelection());
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
$autoComplete.val('').trigger('focus');
$linkTitle.val('');

View File

@@ -287,8 +287,6 @@ function initKoPlugins() {
}
export async function showDialog() {
utils.closeActiveDialog();
await libraryLoader.requireLibrary(libraryLoader.KNOCKOUT);
// lazily apply bindings on first use
@@ -300,11 +298,9 @@ export async function showDialog() {
ko.applyBindings(attributesModel, $dialog[0]);
}
glob.activeDialog = $dialog;
await attributesModel.loadAttributes();
$dialog.modal();
utils.openDialog($dialog);
}
$dialog.on('focus', '.attribute-name', function (e) {

View File

@@ -6,11 +6,7 @@ const $backendLogTextArea = $("#backend-log-textarea");
const $refreshBackendLog = $("#refresh-backend-log-button");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
load();
}

View File

@@ -13,10 +13,6 @@ const $noteTitle = $('#branch-prefix-note-title');
let branchId;
export async function showDialog(node) {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
branchId = node.data.branchId;
const branch = treeCache.getBranch(branchId);
@@ -30,7 +26,7 @@ export async function showDialog(node) {
return;
}
$dialog.modal();
utils.openDialog($dialog);
$treePrefixInput.val(branch.prefix);

View File

@@ -22,11 +22,7 @@ export async function showDialog(noteIds) {
}
}
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
$noteAutoComplete.val('').trigger('focus');

View File

@@ -17,8 +17,6 @@ let taskId = '';
let branchId = null;
export async function showDialog(node, defaultType) {
utils.closeActiveDialog();
// each opening of the dialog resets the taskId so we don't associate it with previous exports anymore
taskId = '';
$exportButton.removeAttr("disabled");
@@ -38,9 +36,7 @@ export async function showDialog(node, defaultType) {
$("#opml-v2").prop("checked", true); // setting default
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
branchId = node.data.branchId;

View File

@@ -3,9 +3,5 @@ import utils from "../services/utils.js";
const $dialog = $("#help-dialog");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
}

View File

@@ -16,8 +16,6 @@ const $explodeArchivesCheckbox = $("#explode-archives-checkbox");
let parentNoteId = null;
export async function showDialog(node) {
utils.closeActiveDialog();
$fileUploadInput.val('').trigger('change'); // to trigger Import button disabling listener below
$safeImportCheckbox.prop("checked", true);
@@ -26,13 +24,11 @@ export async function showDialog(node) {
$codeImportedAsCodeCheckbox.prop("checked", true);
$explodeArchivesCheckbox.prop("checked", true);
glob.activeDialog = $dialog;
parentNoteId = node.data.noteId;
$noteTitle.text(await treeUtils.getNoteTitle(parentNoteId));
$dialog.modal();
utils.openDialog($dialog);
}
$form.on('submit', () => {

View File

@@ -10,13 +10,9 @@ let callback = null;
export async function showDialog(cb) {
callback = cb;
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val('');
$dialog.modal();
utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true });
noteAutocompleteService.showRecentNotes($autoComplete);

View File

@@ -10,13 +10,9 @@ let $originallyFocused; // element focused before the dialog was opened so we ca
export function info(message) {
$originallyFocused = $(':focus');
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$infoContent.text(message);
$dialog.modal();
utils.openDialog($dialog);
return new Promise((res, rej) => { resolve = res; });
}

View File

@@ -8,13 +8,9 @@ const $autoComplete = $("#jump-to-note-autocomplete");
const $showInFullTextButton = $("#show-in-full-text-button");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val('');
$dialog.modal();
utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
.on('autocomplete:selected', function(event, suggestion, dataset) {

View File

@@ -16,10 +16,6 @@ function getOptions() {
}
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
// set default settings
$maxNotesInput.val(20);
@@ -27,7 +23,7 @@ export async function showDialog() {
$linkMapContainer.empty();
$dialog.modal();
utils.openDialog($dialog);
}
$dialog.on('shown.bs.modal', () => {

View File

@@ -37,9 +37,7 @@ export async function importMarkdownInline() {
convertMarkdownToHtml(text);
}
else {
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
}
}

View File

@@ -1,7 +1,5 @@
import noteAutocompleteService from "../services/note_autocomplete.js";
import utils from "../services/utils.js";
import cloningService from "../services/cloning.js";
import treeUtils from "../services/tree_utils.js";
import toastService from "../services/toast.js";
import treeCache from "../services/tree_cache.js";
import treeChangesService from "../services/branches.js";
@@ -18,11 +16,7 @@ let movedNodes;
export async function showDialog(nodes) {
movedNodes = nodes;
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
$noteAutoComplete.val('').trigger('focus');

View File

@@ -10,11 +10,7 @@ const $mime = $("#note-info-mime");
const $okButton = $("#note-info-ok-button");
export function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
const activeNote = noteDetailService.getActiveTabNote();

View File

@@ -29,11 +29,7 @@ export async function showCurrentNoteRevisions() {
}
export async function showNoteRevisionsDialog(noteId, noteRevisionId) {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
await loadNoteRevisions(noteId, noteRevisionId);
}

View File

@@ -5,11 +5,7 @@ const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source");
export function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
const noteText = noteDetailService.getActiveTabNote().content;

View File

@@ -6,13 +6,9 @@ import utils from "../services/utils.js";
const $dialog = $("#options-dialog");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
const options = await server.get('options');
$dialog.modal();
utils.openDialog($dialog);
(await Promise.all([
import('./options/advanced.js'),

View File

@@ -6,7 +6,7 @@ const TPL = `
<div>
<h4>Spell check</h4>
<p>These options apply only for desktop builds, browsers will use their own native spell check.</p>
<p>These options apply only for desktop builds, browsers will use their own native spell check. App restart is required after change.</p>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="spell-check-enabled">
@@ -37,6 +37,20 @@ const TPL = `
</div>
</div>
<div>
<h4>Note erasure timeout</h4>
<p>Deleted notes are at first only marked as deleted and it is possible to recover them
from Recent Notes dialog. After period of time, deleted notes are "erased" which means
their content is not recoverable anymore. This setting allows you to configure the length
of the period between deleting and erasing the note.</p>
<div class="form-group">
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label>
<input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
</div>
</div>
<div>
<h4>Protected session timeout</h4>
@@ -81,6 +95,20 @@ export default class ProtectedSessionOptions {
return false;
});
this.$eraseNotesAfterTimeInSeconds = $("#erase-notes-after-time-in-seconds");
this.$eraseNotesAfterTimeInSeconds.on('change', () => {
const eraseNotesAfterTimeInSeconds = this.$eraseNotesAfterTimeInSeconds.val();
server.put('options', { 'eraseNotesAfterTimeInSeconds': eraseNotesAfterTimeInSeconds }).then(() => {
optionsService.reloadOptions();
toastService.showMessage("Options change have been saved.");
});
return false;
});
this.$protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on('change', () => {
@@ -126,6 +154,7 @@ export default class ProtectedSessionOptions {
this.$spellCheckEnabled.prop("checked", options['spellCheckEnabled'] === 'true');
this.$spellCheckLanguageCode.val(options['spellCheckLanguageCode']);
this.$eraseNotesAfterTimeInSeconds.val(options['eraseNotesAfterTimeInSeconds']);
this.$protectedSessionTimeout.val(options['protectedSessionTimeout']);
this.$noteRevisionsTimeInterval.val(options['noteRevisionSnapshotTimeInterval']);

View File

@@ -12,10 +12,6 @@ let resolve;
let shownCb;
export function ask({ message, defaultValue, shown }) {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
shownCb = shown;
$question = $("<label>")
@@ -34,7 +30,7 @@ export function ask({ message, defaultValue, shown }) {
.append($question)
.append($answer));
$dialog.modal();
utils.openDialog($dialog);
return new Promise((res, rej) => { resolve = res; });
}

View File

@@ -1,11 +1,12 @@
import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $dialog.find(".protected-session-password-form");
const $passwordInput = $dialog.find(".protected-session-password");
export function show() {
$dialog.modal();
utils.openDialog($dialog);
$passwordInput.trigger('focus');
}

View File

@@ -8,11 +8,7 @@ const $dialog = $("#recent-changes-dialog");
const $content = $("#recent-changes-content");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
utils.openDialog($dialog);
const result = await server.get('recent-changes');
@@ -28,31 +24,60 @@ export async function showDialog() {
const groupedByDate = groupByDate(result);
for (const [dateDay, dayChanges] of groupedByDate) {
const changesListEl = $('<ul>');
const $changesList = $('<ul>');
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl);
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append($changesList);
for (const change of dayChanges) {
const formattedTime = utils.formatTime(utils.parseDate(change.date));
let noteLink;
let $noteLink;
if (change.current_isDeleted) {
noteLink = change.current_title;
$noteLink = $("<span>").text(change.current_title);
if (change.canBeUndeleted) {
const $undeleteLink = $(`<a href="javascript:">`)
.text("undelete")
.on('click', async () => {
const confirmDialog = await import('../dialogs/confirm.js');
const text = 'Do you want to undelete this note and its sub-notes?';
if (await confirmDialog.confirm(text)) {
await server.put(`notes/${change.noteId}/undelete`);
$dialog.modal('hide');
await treeCache.reloadNotes([change.noteId]);
treeService.activateNote(change.noteId);
}
});
$noteLink
.append(' (')
.append($undeleteLink)
.append(')');
}
}
else {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
noteLink = await linkService.createNoteLink(notePath, {
title: change.title,
showNotePath: true
});
if (notePath) {
$noteLink = await linkService.createNoteLink(notePath, {
title: change.title,
showNotePath: true
});
}
else {
$noteLink = $("<span>").text(note.title);
}
}
changesListEl.append($('<li>')
$changesList.append($('<li>')
.append(formattedTime + ' - ')
.append(noteLink));
.append($noteLink));
}
$content.append(dayEl);
@@ -85,5 +110,6 @@ function groupByDate(result) {
groupedByDate.get(dateDay).push(row);
}
return groupedByDate;
}

View File

@@ -14,13 +14,9 @@ let codeEditor;
$dialog.on("shown.bs.modal", e => initEditor());
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
await showTableSchemas();
$dialog.modal();
utils.openDialog($dialog);
}
async function initEditor() {

View File

@@ -7,10 +7,34 @@ import contextMenuWidget from "./services/context_menu.js";
import treeChangesService from "./services/branches.js";
import utils from "./services/utils.js";
import treeUtils from "./services/tree_utils.js";
import linkService from "./services/link.js";
import noteContentRenderer from "./services/note_content_renderer.js";
import server from "./services/server.js";
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
// required for CKEditor image upload plugin
window.glob.getActiveNode = treeService.getActiveNode;
window.glob.getHeaders = server.getHeaders;
window.glob.noteChanged = noteDetailService.noteChanged;
window.glob.refreshTree = treeService.reload;
window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog());
window.glob.showIncludeNoteDialog = cb => import('./dialogs/include_note.js').then(d => d.showDialog(cb));
window.glob.loadIncludedNote = async (noteId, el) => {
const note = await treeCache.getNote(noteId);
if (note) {
$(el).empty().append($("<h3>").append(await linkService.createNoteLink(note.noteId, {
showTooltip: false
})));
const {renderedContent} = await noteContentRenderer.getRenderedContent(note);
$(el).append(renderedContent);
}
};
const $leftPane = $("#left-pane");
const $tree = $("#tree");
const $detail = $("#detail");
@@ -37,12 +61,12 @@ $detail.on("click", ".close-detail-button",() => {
});
async function showTree() {
const tree = await treeService.loadTree();
const treeData = await treeService.loadTreeData();
$tree.fancytree({
autoScroll: true,
extensions: ["dnd5", "clones"],
source: tree,
source: treeData,
scrollParent: $tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
@@ -87,6 +111,8 @@ async function showTree() {
});
}
});
treeService.setTree($.ui.fancytree.getTree("#tree"));
}
$detail.on("click", ".note-menu-button", async e => {

View File

@@ -265,6 +265,24 @@ ws.subscribeToMessages(async message => {
}
});
ws.subscribeToMessages(async message => {
if (message.taskType !== 'undelete-notes') {
return;
}
if (message.type === 'task-error') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'task-progress-count') {
toastService.showPersistent(makeToast(message.taskId, "Undeleting notes in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') {
const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
toast.closeAfter = 5000;
toastService.showPersistent(toast);
}
});
export default {
moveBeforeNode,
moveAfterNode,

View File

@@ -1,4 +1,4 @@
import treeUtils from "./tree_utils.js";
import treeService from "./tree.js";
import treeChangesService from "./branches.js";
import cloningService from "./cloning.js";
import toastService from "./toast.js";
@@ -19,7 +19,7 @@ async function pasteAfter(afterNode) {
}
if (clipboardMode === 'cut') {
const nodes = clipboardNodeKeys.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
const nodes = clipboardNodeKeys.map(nodeKey => treeService.getNodeByKey(nodeKey));
await treeChangesService.moveAfterNode(nodes, afterNode);
@@ -28,7 +28,7 @@ async function pasteAfter(afterNode) {
}
else if (clipboardMode === 'copy') {
for (const nodeKey of clipboardNodeKeys) {
const clipNode = treeUtils.getNodeByKey(nodeKey);
const clipNode = treeService.getNodeByKey(nodeKey);
await cloningService.cloneNoteAfter(clipNode.data.noteId, afterNode.data.branchId);
}
@@ -46,7 +46,7 @@ async function pasteInto(parentNode) {
}
if (clipboardMode === 'cut') {
const nodes = clipboardNodeKeys.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
const nodes = clipboardNodeKeys.map(nodeKey => treeService.getNodeByKey(nodeKey));
await treeChangesService.moveToNode(nodes, parentNode);
@@ -57,7 +57,7 @@ async function pasteInto(parentNode) {
}
else if (clipboardMode === 'copy') {
for (const nodeKey of clipboardNodeKeys) {
const clipNode = treeUtils.getNodeByKey(nodeKey);
const clipNode = treeService.getNodeByKey(nodeKey);
await cloningService.cloneNoteTo(clipNode.data.noteId, parentNode.data.noteId);
}
@@ -92,7 +92,7 @@ function cut(nodes) {
}
function isClipboardEmpty() {
clipboardNodeKeys = clipboardNodeKeys.filter(key => !!treeUtils.getNodeByKey(key));
clipboardNodeKeys = clipboardNodeKeys.filter(key => !!treeService.getNodeByKey(key));
return clipboardNodeKeys.length === 0;
}

View File

@@ -241,7 +241,7 @@ function registerEntrypoints() {
d.showDialog(selectedOrActiveNodes);
}));
keyboardActionService.setGlobalActionHandler("CreateNoteIntoDayNote", async () => {
const todayNote = await dateNoteService.getTodayNote();
@@ -288,6 +288,8 @@ function registerEntrypoints() {
searchNotesService.searchInSubtree(node.data.noteId);
});
keyboardActionService.setGlobalActionHandler("CopyWithoutFormatting", utils.copySelectionToClipboard);
}
export default {

View File

@@ -296,12 +296,28 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
*/
this.createNoteLink = linkService.createNoteLink;
/**
* Adds given text to the editor cursor
*
* @param {string} text - this must be clear text, HTML is not supported.
* @method
*/
this.addTextToActiveTabEditor = linkService.addTextToEditor;
/**
* @method
* @returns {NoteFull} active note (loaded into right pane)
*/
this.getActiveTabNote = noteDetailService.getActiveTabNote;
/**
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
*
* @method
* @returns {Editor|null} CKEditor instance or null (e.g. if active note is not a text note)
*/
this.getActiveTabTextEditor = noteDetailService.getActiveEditor;
/**
* @method
* @returns {Promise<string|null>} returns note path of active note or null if there isn't active note

View File

@@ -10,6 +10,12 @@ function getNotePathFromUrl(url) {
}
async function createNoteLink(notePath, options = {}) {
if (!notePath || !notePath.trim()) {
console.error("Missing note path");
return $("<span>").text("[missing note]");
}
let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;

View File

@@ -7,31 +7,22 @@ import protectedSessionHolder from "./protected_session_holder.js";
async function getRenderedContent(note) {
const type = getRenderingType(note);
let rendered;
let $rendered;
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
rendered = "";
}
else {
rendered = $content;
}
$rendered = $("<div>").html(fullNote.content);
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
rendered = "";
}
rendered = $("<pre>").text(fullNote.content);
$rendered = $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
rendered = $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
$rendered = $("<img>")
.attr("src", `api/images/${note.noteId}/${note.title}`)
.css("max-width", "100%");
}
else if (type === 'file') {
function getFileUrl() {
@@ -56,33 +47,35 @@ async function getRenderedContent(note) {
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
rendered = $('<div>')
$rendered = $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
$rendered = $('<div>');
await renderService.render(note, $el, this.ctx);
rendered = $el;
await renderService.render(note, $rendered, this.ctx);
}
else if (type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
rendered = $("<div>")
$rendered = $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
}
else {
rendered = "<em>Content of this note cannot be displayed in the book format</em>";
$rendered = $("<em>Content of this note cannot be displayed in the book format</em>");
}
if (note.cssClass) {
$rendered.addClass(note.cssClass);
}
return {
renderedContent: rendered,
renderedContent: $rendered,
type
};
}

View File

@@ -148,8 +148,8 @@ class NoteDetailBook {
const label = `${childCount} child${childCount > 1 ? 'ren' : ''}`;
$card.append($('<div class="note-book-children">')
.append($(`<a class="note-book-open-children-button" href="javascript:">+ Show ${label}</a>`))
.append($(`<a class="note-book-hide-children-button" href="javascript:">- Hide ${label}</a>`).hide())
.append($(`<a class="note-book-open-children-button no-print" href="javascript:">+ Show ${label}</a>`))
.append($(`<a class="note-book-hide-children-button no-print" href="javascript:">- Hide ${label}</a>`).hide())
.append($('<div class="note-book-children-content">'))
);
}

View File

@@ -60,6 +60,7 @@ class NoteDetailCode {
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
this.codeEditor.clearHistory();
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);

View File

@@ -2,6 +2,7 @@ import treeService from './tree.js';
import treeCache from "./tree_cache.js";
import server from './server.js';
import toastService from "./toast.js";
import utils from "./utils.js";
const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("#reset-search-button");
@@ -28,6 +29,8 @@ const helpText = `
</p>`;
function showSearch() {
utils.saveFocusedElement();
$searchBox.slideDown();
$searchBox.tooltip({
@@ -49,6 +52,8 @@ function hideSearch() {
$searchResults.hide();
$searchBox.slideUp();
utils.focusSavedElement();
}
function toggleSearch() {

View File

@@ -5,6 +5,7 @@ import splitService from "./split.js";
import optionService from "./options.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
import utils from "./utils.js";
const $sidebar = $("#right-pane");
const $sidebarContainer = $('#sidebar-container');
@@ -15,6 +16,10 @@ const $hideSidebarButton = $("#hide-sidebar-button");
optionService.waitForOptions().then(options => toggleSidebar(options.is('rightPaneVisible')));
function toggleSidebar(show) {
if (utils.isMobile()) {
return;
}
$sidebar.toggle(show);
$showSidebarButton.toggle(!show);
$hideSidebarButton.toggle(show);

View File

@@ -3,15 +3,16 @@ import optionsService from "./options.js";
export async function initSpellCheck() {
const options = await optionsService.waitForOptions();
if (!options.is('spellCheckEnabled')) {
return;
}
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
const {remote, shell} = require('electron');
const spellCheckHandler = new SpellCheckHandler();
spellCheckHandler.attachToInput();
// not fully disabling the spellcheck since we want to preserve the context menu
// this will just get rid of the "red squiggles"
if (options.is('spellCheckEnabled')) {
spellCheckHandler.attachToInput();
}
spellCheckHandler.switchLanguage(options.get('spellCheckLanguageCode'));

View File

@@ -303,7 +303,11 @@ class TabContext {
let type = this.note.type;
if (type === 'text' && !disableAutoBook && utils.isHtmlEmpty(this.note.content) && this.note.hasChildren()) {
if (type === 'text'
&& !disableAutoBook
&& utils.isHtmlEmpty(this.note.content)
&& this.note.hasChildren()
&& utils.isDesktop()) {
type = 'book';
}

View File

@@ -16,6 +16,8 @@ import TreeContextMenu from "./tree_context_menu.js";
import bundle from "./bundle.js";
import keyboardActionService from "./keyboard_actions.js";
let tree;
const $tree = $("#tree");
const $createTopLevelNoteButton = $("#create-top-level-note-button");
const $collapseTreeButton = $("#collapse-tree-button");
@@ -30,8 +32,6 @@ const frontendLoaded = new Promise(resolve => { setFrontendAsLoaded = resolve; }
* @return {FancytreeNode|null}
*/
function getFocusedNode() {
const tree = $tree.fancytree("getTree");
return tree.getFocusNode();
}
@@ -40,7 +40,7 @@ function getFocusedNode() {
* @return {FancytreeNode|null}
*/
function getActiveNode() {
return $tree.fancytree("getActiveNode");
return tree.getActiveNode();
}
/** @return {FancytreeNode[]} */
@@ -56,7 +56,7 @@ async function getNodesByBranchId(branchId) {
function getNodesByNoteId(noteId) {
utils.assertArguments(noteId);
const list = getTree().getNodesByRef(noteId);
const list = tree.getNodesByRef(noteId);
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
}
@@ -309,7 +309,7 @@ async function getSomeNotePath(note) {
const parents = await cur.getParentNotes();
if (!parents.length) {
toastService.throwError(`Can't find parents for note ${cur.noteId}`);
console.error(`Can't find parents for note ${cur.noteId}`);
return;
}
@@ -331,7 +331,7 @@ async function setExpandedToServer(branchId, isExpanded) {
/** @return {FancytreeNode[]} */
function getSelectedNodes(stopOnParents = false) {
return getTree().getSelectedNodes(stopOnParents);
return tree.getSelectedNodes(stopOnParents);
}
/** @return {FancytreeNode[]} */
@@ -429,14 +429,14 @@ async function treeInitialized() {
setFrontendAsLoaded();
}
async function initFancyTree(tree) {
utils.assertArguments(tree);
async function initFancyTree(treeData) {
utils.assertArguments(treeData);
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "dnd5", "clones"],
source: tree,
source: treeData,
scrollParent: $tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
@@ -523,19 +523,16 @@ async function initFancyTree(tree) {
return false; // blocks default browser right click menu
});
}
/** @return {Fancytree} */
function getTree() {
return $tree.fancytree('getTree');
tree = $.ui.fancytree.getTree("#tree");
}
async function reload() {
const notes = await loadTree();
const notes = await loadTreeData();
const activeNotePath = getActiveNode() !== null ? await treeUtils.getNotePath(getActiveNode()) : null;
await getTree().reload(notes);
await tree.reload(notes);
// reactivate originally activated node, but don't trigger note loading
if (activeNotePath) {
@@ -559,7 +556,7 @@ function getHashValueFromAddress() {
return str.split("-");
}
async function loadTree() {
async function loadTreeData() {
const resp = await server.get('tree');
treeCache.load(resp.notes, resp.branches);
@@ -580,7 +577,7 @@ async function collapseTree(node = null) {
}
function focusTree() {
getTree().setFocus();
tree.setFocus();
}
async function scrollToActiveNote() {
@@ -638,7 +635,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
extraOptions.saveSelection = false;
}
if (extraOptions.saveSelection) {
if (extraOptions.saveSelection && utils.isCKEditorInitialized()) {
[extraOptions.title, extraOptions.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml());
}
@@ -651,7 +648,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
type: extraOptions.type
});
if (extraOptions.saveSelection) {
if (extraOptions.saveSelection && utils.isCKEditorInitialized()) {
// we remove the selection only after it was saved to server to make sure we don't lose anything
window.cutToNote.removeSelection();
}
@@ -754,9 +751,9 @@ async function sortAlphabetically(noteId) {
}
async function showTree() {
const tree = await loadTree();
const treeData = await loadTreeData();
await initFancyTree(tree);
await initFancyTree(treeData);
}
ws.subscribeToMessages(message => {
@@ -873,7 +870,7 @@ window.glob.cutIntoNote = () => createNoteInto(true);
keyboardActionService.setGlobalActionHandler('CutIntoNote', () => createNoteInto(true));
keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto);
keyboardActionService.setGlobalActionHandler('CreateNoteInto', () => createNoteInto(true));
keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote);
@@ -912,6 +909,13 @@ async function duplicateNote(noteId, parentNoteId) {
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
}
function getNodeByKey(key) {
return tree.getNodeByKey(key);
}
function setTree(treeInstance) {
tree = treeInstance;
}
keyboardActionService.setGlobalActionHandler('CollapseTree', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
$collapseTreeButton.on('click', () => collapseTree());
@@ -931,13 +935,12 @@ export default {
setNoteTitle,
setPrefix,
createNote,
createNoteInto,
getSelectedNodes,
getSelectedOrActiveNodes,
clearSelectedNodes,
sortAlphabetically,
showTree,
loadTree,
loadTreeData,
treeInitialized,
setExpandedToServer,
getNodesByNoteId,
@@ -949,5 +952,7 @@ export default {
getSomeNotePath,
focusTree,
scrollToActiveNote,
duplicateNote
duplicateNote,
getNodeByKey,
setTree
};

View File

@@ -62,6 +62,11 @@ class TreeContextMenu {
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "unhoist", uiIcon: "arrow-up" },
{ title: 'Edit branch prefix <kbd data-kb-action="EditBranchPrefix"></kbd>', cmd: "editBranchPrefix", uiIcon: "empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-kb-action="CollapseSubtree"></kbd>', cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-kb-action="SortChildNotes"></kbd>', cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
] },
{ title: "----" },
{ title: "Protect subtree", cmd: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes },
{ title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes },
@@ -84,12 +89,7 @@ class TreeContextMenu {
{ title: "Export", cmd: "export", uiIcon: "empty",
enabled: notSearch && noSelectedNotes },
{ title: "Import into note", cmd: "importIntoNote", uiIcon: "empty",
enabled: notSearch && noSelectedNotes },
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-kb-action="CollapseSubtree"></kbd>', cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-kb-action="SortChildNotes"></kbd>', cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
] },
enabled: notSearch && noSelectedNotes }
].filter(row => row !== null);
}

View File

@@ -2,16 +2,10 @@ import utils from './utils.js';
import hoistedNoteService from './hoisted_note.js';
import treeCache from "./tree_cache.js";
const $tree = $("#tree");
async function getParentProtectedStatus(node) {
return await hoistedNoteService.isRootNode(node) ? 0 : node.getParent().data.isProtected;
}
function getNodeByKey(key) {
return $tree.fancytree('getNodeByKey', key);
}
function getNoteIdFromNotePath(notePath) {
if (!notePath) {
return null;
@@ -123,7 +117,6 @@ async function getNotePathTitle(notePath) {
export default {
getParentProtectedStatus,
getNodeByKey,
getNotePath,
getNoteIdFromNotePath,
getNoteIdAndParentIdFromNotePath,

View File

@@ -209,9 +209,50 @@ function getMimeTypeClass(mime) {
function closeActiveDialog() {
if (glob.activeDialog) {
glob.activeDialog.modal('hide');
glob.activeDialog = null;
}
}
let $lastFocusedElement = null;
function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
import("./note_detail.js").then(noteDetail => {
noteDetail.default.getActiveEditor().editing.view.focus();
});
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}
function openDialog($dialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
saveFocusedElement();
$dialog.modal();
$dialog.on('hidden.bs.modal', () => {
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
}
function isHtmlEmpty(html) {
html = html.toLowerCase();
@@ -241,6 +282,17 @@ function getUrlForDownload(url) {
}
}
function copySelectionToClipboard() {
const text = window.getSelection().toString();
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
}
}
function isCKEditorInitialized() {
return !!(window && window.cutToNote);
}
export default {
reloadApp,
parseDate,
@@ -270,8 +322,13 @@ export default {
getNoteTypeClass,
getMimeTypeClass,
closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
isHtmlEmpty,
clearBrowserCache,
getUrlForDownload,
normalizeShortcut
normalizeShortcut,
copySelectionToClipboard,
isCKEditorInitialized
};

View File

@@ -71,7 +71,19 @@ class AttributesWidget extends StandardWidget {
async renderAttributes(attributes, $container) {
for (const attribute of attributes) {
if (attribute.type === 'label') {
$container.append(utils.formatLabel(attribute) + " ");
if (attribute.name === 'externalLink') {
$container.append('@' + attribute.name + "=");
$container.append(
$('<a>')
.text(attribute.value)
.attr('href', attribute.value)
.addClass('external')
);
$container.append(" ");
}
else {
$container.append(utils.formatLabel(attribute) + " ");
}
} else if (attribute.type === 'relation') {
if (attribute.value) {
$container.append('@' + attribute.name + "=");

View File

@@ -41,7 +41,7 @@ class CalendarWidget extends StandardWidget {
}
init($el, activeDate) {
this.activeDate = new Date(Date.parse(activeDate));
this.activeDate = new Date(activeDate + "T12:00:00"); // attaching time fixes local timezone handling
this.todaysDate = new Date();
this.date = new Date(this.activeDate.getTime());

View File

@@ -43,6 +43,7 @@ body {
min-height: 0;
padding-left: 10px;
width: 100%;
height: 100%;
}
#search-box {

View File

@@ -0,0 +1,69 @@
.note-detail-book {
height: 100%;
}
.note-detail-book-content {
display: flex;
flex-wrap: wrap;
overflow: auto;
height: 100%;
align-content: start;
}
.note-book-card {
border-radius: 10px;
background-color: var(--accented-background-color);
padding: 15px;
padding-bottom: 5px;
margin: 5px;
margin-left: 0;
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.note-book-card .note-book-card {
border: 1px solid var(--main-border-color);
}
.note-book-content {
overflow: hidden;
}
.note-book-card.type-image .note-book-content, .note-book-card.type-file .note-book-content, .note-book-card.type-protected-session .note-book-content {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.note-book-card.type-image .note-book-content img, .note-book-card.type-text .note-book-content img {
max-width: 100%;
max-height: 100%;
}
.note-book-title {
flex-grow: 0;
}
.note-book-content {
flex-grow: 1;
}
.note-book-auto-message {
background-color: var(--accented-background-color);
text-align: center;
width: 100%;
border-radius: 10px;
padding: 5px;
margin-top: 5px;
}
.note-detail-image {
text-align: center;
}
.note-detail-image-view {
max-width: 100%;
}

View File

@@ -115,4 +115,5 @@ span.fancytree-expander {
/* large left padding is necessary for ckeditor gutter in detail-only (smartphone) layout */
padding-left: 35px;
padding-top: 10px;
min-height: 150px !important;
}

View File

@@ -0,0 +1,11 @@
@media print
{
.no-print, .no-print *
{
display: none !important;
}
.relation-map-wrapper {
height: 100vh !important;
}
}

View File

@@ -518,14 +518,6 @@ button.icon-button {
padding: 5px;
}
.note-detail-image {
text-align: center;
}
.note-detail-image-view {
max-width: 100%;
}
pre:not(.CodeMirror-line) {
color: var(--main-text-color) !important;
white-space: pre-wrap;
@@ -857,68 +849,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
z-index: 100;
}
.note-detail-book {
height: 100%;
}
.note-detail-book-content {
display: flex;
flex-wrap: wrap;
overflow: auto;
height: 100%;
align-content: start;
}
.note-book-card {
border-radius: 10px;
background-color: var(--accented-background-color);
padding: 15px;
padding-bottom: 5px;
margin: 5px;
margin-left: 0;
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.note-book-card .note-book-card {
border: 1px solid var(--main-border-color);
}
.note-book-content {
overflow: hidden;
}
.note-book-card.type-image .note-book-content, .note-book-card.type-file .note-book-content, .note-book-card.type-protected-session .note-book-content {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.note-book-card.type-image .note-book-content img {
max-width: 100%;
max-height: 100%;
}
.note-book-title {
flex-grow: 0;
}
.note-book-content {
flex-grow: 1;
}
.note-book-auto-message {
background-color: var(--accented-background-color);
text-align: center;
width: 100%;
border-radius: 10px;
padding: 5px;
margin-top: 5px;
}
#toast-container {
position: absolute;
width: 100%;

View File

@@ -1,9 +1,9 @@
"use strict";
const app_info = require('../../services/app_info');
const appInfo = require('../../services/app_info');
async function getAppInfo() {
return app_info;
return appInfo;
}
module.exports = {

View File

@@ -101,7 +101,8 @@ async function deleteBranch(req) {
const branch = await repository.getBranch(req.params.branchId);
const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes');
const noteDeleted = await notes.deleteBranch(branch, taskContext);
const deleteId = utils.randomString(10);
const noteDeleted = await notes.deleteBranch(branch, deleteId, taskContext);
if (last) {
taskContext.taskSucceeded();

View File

@@ -1,5 +1,6 @@
"use strict";
const attributeService = require("../../services/attributes");
const noteService = require('../../services/notes');
const dateNoteService = require('../../services/date_notes');
const dateUtils = require('../../services/date_utils');
@@ -23,16 +24,26 @@ async function findClippingNote(todayNote, pageUrl) {
return null;
}
async function getClipperInboxNote() {
let clipperInbox = await attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
clipperInbox = await dateNoteService.getDateNote(dateUtils.localNowDate());
}
return clipperInbox;
}
async function addClipping(req) {
const {title, content, pageUrl, images} = req.body;
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
const clipperInbox = await getClipperInboxNote();
let clippingNote = await findClippingNote(todayNote, pageUrl);
let clippingNote = await findClippingNote(clipperInbox, pageUrl);
if (!clippingNote) {
clippingNote = (await noteService.createNewNote({
parentNoteId: todayNote.noteId,
parentNoteId: clipperInbox.noteId,
title: title,
content: '',
type: 'text'
@@ -54,10 +65,10 @@ async function addClipping(req) {
async function createNote(req) {
const {title, content, pageUrl, images, clipType} = req.body;
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
const clipperInbox = await getClipperInboxNote();
const {note} = await noteService.createNewNote({
parentNoteId: todayNote.noteId,
parentNoteId: clipperInbox.noteId,
title,
content,
type: 'text'

View File

@@ -49,7 +49,7 @@ async function loginSync(req) {
return {
sourceId: sourceIdService.getCurrentSourceId(),
maxSyncId: await sql.getValue("SELECT MAX(id) FROM sync")
maxSyncId: await sql.getValue("SELECT MAX(id) FROM sync WHERE isSynced = 1")
};
}

View File

@@ -55,12 +55,15 @@ async function deleteNote(req) {
const taskId = req.query.taskId;
const last = req.query.last === 'true';
// note how deleteId is separate from taskId - single taskId produces separate deleteId for each "top level" deleted note
const deleteId = utils.randomString(10);
const note = await repository.getNote(noteId);
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
for (const branch of await note.getBranches()) {
await noteService.deleteBranch(branch, taskContext);
await noteService.deleteBranch(branch, deleteId, taskContext);
}
if (last) {
@@ -68,6 +71,16 @@ async function deleteNote(req) {
}
}
async function undeleteNote(req) {
const note = await repository.getNote(req.params.noteId);
const taskContext = TaskContext.getInstance(utils.randomString(10), 'undelete-notes');
await noteService.undeleteNote(note, note.deleteId, taskContext);
await taskContext.taskSucceeded();
}
async function sortNotes(req) {
const noteId = req.params.noteId;
@@ -169,6 +182,7 @@ module.exports = {
getNote,
updateNote,
deleteNote,
undeleteNote,
createNote,
sortNotes,
protectSubtree,

View File

@@ -6,6 +6,7 @@ const attributes = require('../../services/attributes');
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = new Set([
'eraseNotesAfterTimeInSeconds',
'protectedSessionTimeout',
'noteRevisionSnapshotTimeInterval',
'zoomFactor',

View File

@@ -2,6 +2,7 @@
const sql = require('../../services/sql');
const protectedSessionService = require('../../services/protected_session');
const noteService = require('../../services/notes');
async function getRecentChanges() {
const recentChanges = await sql.getRows(
@@ -10,6 +11,8 @@ async function getRecentChanges() {
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
note_revisions.title,
@@ -19,21 +22,23 @@ async function getRecentChanges() {
JOIN notes USING(noteId)
ORDER BY
note_revisions.utcDateCreated DESC
LIMIT 1000
LIMIT 200
)
UNION ALL SELECT * FROM (
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS date
notes.utcDateModified AS date
FROM
notes
ORDER BY
utcDateCreated DESC
LIMIT 1000
utcDateModified DESC
LIMIT 200
)
ORDER BY date DESC
LIMIT 200`);
@@ -48,6 +53,20 @@ async function getRecentChanges() {
change.title = change.current_title = "[Protected]";
}
}
if (change.current_isDeleted) {
if (change.current_isErased) {
change.canBeUndeleted = false;
}
else {
const deleteId = change.current_deleteId;
const undeletedParentBranches = await noteService.getUndeletedParentBranches(change.noteId, deleteId);
// note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op)
change.canBeUndeleted = undeletedParentBranches.length > 0;
}
}
}
return recentChanges;

View File

@@ -29,8 +29,12 @@ async function searchFromNote(req) {
return [404, `Note ${req.params.noteId} has not been found.`];
}
if (note.isDeleted) {
return [400, `Note ${req.params.noteId} is deleted.`];
}
if (note.type !== 'search') {
return [400, '`Note ${req.params.noteId} is not search note.`']
return [400, `Note ${req.params.noteId} is not search note.`]
}
const json = await note.getJsonContent();
@@ -41,18 +45,28 @@ async function searchFromNote(req) {
let noteIds;
if (json.searchString.startsWith('=')) {
const relationName = json.searchString.substr(1).trim();
try {
if (json.searchString.startsWith('=')) {
const relationName = json.searchString.substr(1).trim();
noteIds = await searchFromRelation(note, relationName);
noteIds = await searchFromRelation(note, relationName);
} else {
noteIds = await searchService.searchForNoteIds(json.searchString);
}
}
else {
noteIds = await searchService.searchForNoteIds(json.searchString);
catch (e) {
log.error(`Search failed for note ${note.noteId}: ` + e.message + ": " + e.stack);
throw new Error("Search failed, see logs for details.");
}
// we won't return search note's own noteId
noteIds = noteIds.filter(noteId => noteId !== note.noteId);
if (noteIds.length > 200) {
noteIds = noteIds.slice(0, 200);
}
return noteIds.map(noteCacheService.getNotePath).filter(res => !!res);
}

View File

@@ -50,7 +50,7 @@ async function getStats() {
async function checkSync() {
return {
entityHashes: await contentHashService.getEntityHashes(),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1')
};
}
@@ -116,11 +116,11 @@ async function forceNoteSync(req) {
async function getChanged(req) {
const lastSyncId = parseInt(req.query.lastSyncId);
const syncs = await sql.getRows("SELECT * FROM sync WHERE id > ? LIMIT 1000", [lastSyncId]);
const syncs = await sql.getRows("SELECT * FROM sync WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastSyncId]);
return {
syncs: await syncService.getSyncRecords(syncs),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1')
};
}

View File

@@ -131,6 +131,7 @@ function register(app) {
apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote);
apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes);
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectSubtree);

View File

@@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 155;
const SYNC_VERSION = 13;
const APP_DB_VERSION = 158;
const SYNC_VERSION = 14;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-01-08T21:01:24+01:00", buildRevision: "2b69abf8ab2241f01cd38b31308e54b9faaa74d5" };
module.exports = { buildDate:"2020-03-28T20:58:20+01:00", buildRevision: "b4b22d9353bdc6ad4d3bab7cdb33bd0b844cc36d" };

View File

@@ -7,6 +7,7 @@ const noteService = require('./notes');
const repository = require('./repository');
const Branch = require('../entities/branch');
const TaskContext = require("./task_context.js");
const utils = require('./utils');
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
@@ -54,7 +55,8 @@ async function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
const branch = await repository.getEntity(`SELECT * FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]);
if (branch) {
await noteService.deleteBranch(branch, new TaskContext());
const deleteId = utils.randomString(10);
await noteService.deleteBranch(branch, deleteId, new TaskContext());
}
}

View File

@@ -51,7 +51,7 @@ class ConsistencyChecks {
childToParents[childNoteId].push(parentNoteId);
}
function checkTreeCycle(noteId, path) {
const checkTreeCycle = (noteId, path) => {
if (noteId === 'root') {
return;
}
@@ -75,7 +75,7 @@ class ConsistencyChecks {
checkTreeCycle(parentNoteId, newPath);
}
}
}
};
const noteIds = Object.keys(childToParents);
@@ -323,14 +323,25 @@ class ConsistencyChecks {
WHERE isErased = 1
AND content IS NOT NULL`,
async ({noteId}) => {
if (this.autoFix) {
await sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]);
logFix(`Note ${noteId} content has been set to null since the note is erased`);
}
else {
logError(`Note ${noteId} content is not null even though the note is erased`);
}
// we always fix this issue because there does not seem to be a good way to prevent it.
// Scenario in which this can happen:
// 1. user on instance A deletes the note (sync for notes is created, but not for note_contents) and is later erased
// 2. instance B gets synced from instance A, note is updated because of sync row for notes,
// but note_contents is not because erasion does not create sync rows
// 3. therefore note.isErased = true, but note_contents.content remains not updated and not erased.
//
// Considered solutions:
// - don't sync erased notes - this might prevent syncing also of the isDeleted flag and note would continue
// to exist on the other instance
// - create sync rows for erased event - this would be a problem for undeletion since erasion might happen
// on one instance after undelete and thus would win even though there's no user action behind it
//
// So instead we just fix such cases afterwards here.
await sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]);
logFix(`Note ${noteId} content has been set to null since the note is erased`);
});
await this.findAndFixIssues(`
@@ -535,7 +546,7 @@ class ConsistencyChecks {
${entityName}
LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key}
WHERE
sync.id IS NULL AND ` + (entityName === 'options' ? 'isSynced = 1' : '1'),
sync.id IS NULL AND ` + (entityName === 'options' ? 'options.isSynced = 1' : '1'),
async ({entityId}) => {
if (this.autoFix) {
await syncTableService.addEntitySync(entityName, entityId);
@@ -547,23 +558,23 @@ class ConsistencyChecks {
});
await this.findAndFixIssues(`
SELECT
id, entityId
FROM
sync
LEFT JOIN ${entityName} ON entityId = ${key}
WHERE
sync.entityName = '${entityName}'
AND ${key} IS NULL`,
async ({id, entityId}) => {
if (this.autoFix) {
await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
SELECT
id, entityId
FROM
sync
LEFT JOIN ${entityName} ON entityId = ${key}
WHERE
sync.entityName = '${entityName}'
AND ${key} IS NULL`,
async ({id, entityId}) => {
if (this.autoFix) {
await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
} else {
logError(`Unrecognized sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
}
});
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
} else {
logError(`Unrecognized sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
}
});
}
async findSyncRowsIssues() {

View File

@@ -40,7 +40,9 @@ async function getRootCalendarNote() {
parentNoteId: 'root',
title: 'Calendar',
target: 'into',
isProtected: false
isProtected: false,
type: 'text',
content: ''
})).note;
await attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);

View File

@@ -16,7 +16,7 @@ async function exportToOpml(taskContext, branch, version, res) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
if (!note.isStringNote() || await note.hasOwnedLabel('excludeFromExport')) {
if (await note.hasOwnedLabel('excludeFromExport')) {
return;
}
@@ -24,13 +24,13 @@ async function exportToOpml(taskContext, branch, version, res) {
if (opmlVersion === 1) {
const preparedTitle = escapeXmlAttribute(title);
const preparedContent = prepareText(await note.getContent());
const preparedContent = note.isStringNote() ? prepareText(await note.getContent()) : '';
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
}
else if (opmlVersion === 2) {
const preparedTitle = escapeXmlAttribute(title);
const preparedContent = escapeXmlAttribute(await note.getContent());
const preparedContent = note.isStringNote() ? escapeXmlAttribute(await note.getContent()) : '';
res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
}

View File

@@ -1,5 +1,5 @@
const sax = require("sax");
const fileType = require('file-type');
const FileType = require('file-type');
const stream = require('stream');
const log = require("../log");
const utils = require("../utils");
@@ -235,14 +235,12 @@ async function importEnex(taskContext, file, parentNote) {
taskContext.increaseProgressCount();
let noteContent = await noteEntity.getContent();
for (const resource of resources) {
const hash = utils.md5(resource.content);
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
const fileTypeFromBuffer = fileType(resource.content);
const fileTypeFromBuffer = await FileType.fromBuffer(resource.content);
if (fileTypeFromBuffer) {
// If fileType returns something for buffer, then set the mime given
resource.mime = fileTypeFromBuffer.mime;
@@ -268,7 +266,7 @@ async function importEnex(taskContext, file, parentNote) {
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
noteContent = noteContent.replace(mediaRegex, resourceLink);
content = content.replace(mediaRegex, resourceLink);
};
if (["image/jpeg", "image/png", "image/gif", "image/webp"].includes(resource.mime)) {
@@ -281,12 +279,12 @@ async function importEnex(taskContext, file, parentNote) {
const imageLink = `<img src="${url}">`;
noteContent = noteContent.replace(mediaRegex, imageLink);
content = content.replace(mediaRegex, imageLink);
if (!noteContent.includes(imageLink)) {
if (!content.includes(imageLink)) {
// if there wasn't any match for the reference, we'll add the image anyway
// otherwise image would be removed since no note would include it
noteContent += imageLink;
content += imageLink;
}
} catch (e) {
log.error("error when saving image from ENEX file: " + e);
@@ -298,7 +296,7 @@ async function importEnex(taskContext, file, parentNote) {
}
// save updated content with links to files/images
await noteEntity.setContent(noteContent);
await noteEntity.setContent(content);
await noteService.scanForLinks(noteEntity.noteId);

View File

@@ -147,11 +147,16 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
continue;
}
if (attr.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink'].includes(attr.name)) {
if (attr.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(attr.name)) {
// these relations are created automatically and as such don't need to be duplicated in the import
continue;
}
if (attr.type === 'label' && attr.name === 'externalLink') {
// also created automatically
continue;
}
if (attr.type === 'relation') {
attr.value = getNewNoteId(attr.value);
}

View File

@@ -306,6 +306,10 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "ZoomIn",
defaultShortcuts: ["CommandOrControl+="]
},
{
actionName: "CopyWithoutFormatting",
defaultShortcuts: ["CommandOrControl+Alt+C"]
}
];

View File

@@ -242,6 +242,34 @@ function findInternalLinks(content, foundLinks) {
return content.replace(/href="[^"]*#root/g, 'href="#root');
}
function findExternalLinks(content, foundLinks) {
const re = /href="([a-zA-Z]+:\/\/[^"]*)"/g;
let match;
while (match = re.exec(content)) {
foundLinks.push({
name: 'externalLink',
value: match[1]
});
}
return content;
}
function findIncludeNoteLinks(content, foundLinks) {
const re = /<section class="include-note" data-note-id="([a-zA-Z0-9]+)">/g;
let match;
while (match = re.exec(content)) {
foundLinks.push({
name: 'includeNoteLink',
value: match[1]
});
}
return content;
}
function findRelationMapLinks(content, foundLinks) {
const obj = JSON.parse(content);
@@ -267,6 +295,8 @@ async function saveLinks(note, content) {
if (note.type === 'text') {
content = findImageLinks(content, foundLinks);
content = findInternalLinks(content, foundLinks);
content = findExternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
}
else if (note.type === 'relation-map') {
findRelationMapLinks(content, foundLinks);
@@ -278,9 +308,11 @@ async function saveLinks(note, content) {
const existingLinks = await note.getLinks();
for (const foundLink of foundLinks) {
const targetNote = await repository.getNote(foundLink.value);
if (!targetNote || targetNote.isDeleted) {
continue;
if (foundLink.name !== 'externalLink') {
const targetNote = await repository.getNote(foundLink.value);
if (!targetNote || targetNote.isDeleted) {
continue;
}
}
const existingLink = existingLinks.find(existingLink =>
@@ -290,7 +322,7 @@ async function saveLinks(note, content) {
if (!existingLink) {
await new Attribute({
noteId: note.noteId,
type: 'relation',
type: foundLink.name === 'externalLink' ? 'label' : 'relation',
name: foundLink.name,
value: foundLink.value,
}).save();
@@ -389,8 +421,14 @@ async function updateNote(noteId, noteUpdates) {
};
}
/** @return {boolean} - true if note has been deleted, false otherwise */
async function deleteBranch(branch, taskContext) {
/**
* @param {Branch} branch
* @param {string} deleteId
* @param {TaskContext} taskContext
*
* @return {boolean} - true if note has been deleted, false otherwise
*/
async function deleteBranch(branch, deleteId, taskContext) {
taskContext.increaseProgressCount();
if (!branch || branch.isDeleted) {
@@ -405,26 +443,32 @@ async function deleteBranch(branch, taskContext) {
}
branch.isDeleted = true;
branch.deleteId = deleteId;
await branch.save();
const note = await branch.getNote();
const notDeletedBranches = await note.getBranches();
if (notDeletedBranches.length === 0) {
note.isDeleted = true;
await note.save();
for (const childBranch of await note.getChildBranches()) {
await deleteBranch(childBranch, taskContext);
await deleteBranch(childBranch, deleteId, taskContext);
}
// first delete children and then parent - this will show up better in recent changes
note.isDeleted = true;
note.deleteId = deleteId;
await note.save();
for (const attribute of await note.getOwnedAttributes()) {
attribute.isDeleted = true;
attribute.deleteId = deleteId;
await attribute.save();
}
for (const relation of await note.getTargetRelations()) {
relation.isDeleted = true;
relation.deleteId = deleteId;
await relation.save();
}
@@ -435,6 +479,88 @@ async function deleteBranch(branch, taskContext) {
}
}
/**
* @param {Note} note
* @param {string} deleteId
* @param {TaskContext} taskContext
*/
async function undeleteNote(note, deleteId, taskContext) {
const undeletedParentBranches = await getUndeletedParentBranches(note.noteId, deleteId);
if (undeletedParentBranches.length === 0) {
// cannot undelete if there's no undeleted parent
return;
}
for (const parentBranch of undeletedParentBranches) {
await undeleteBranch(parentBranch, deleteId, taskContext);
}
}
/**
* @param {Branch} branch
* @param {string} deleteId
* @param {TaskContext} taskContext
*/
async function undeleteBranch(branch, deleteId, taskContext) {
if (!branch.isDeleted) {
return;
}
const note = await branch.getNote();
if (note.isDeleted && note.deleteId !== deleteId) {
return;
}
branch.isDeleted = false;
await branch.save();
taskContext.increaseProgressCount();
if (note.isDeleted && note.deleteId === deleteId) {
note.isDeleted = false;
await note.save();
const attrs = await repository.getEntities(`
SELECT * FROM attributes
WHERE isDeleted = 1
AND deleteId = ?
AND (noteId = ?
OR (type = 'relation' AND value = ?))`, [deleteId, note.noteId, note.noteId]);
for (const attr of attrs) {
attr.isDeleted = false;
await attr.save();
}
const childBranches = await repository.getEntities(`
SELECT branches.*
FROM branches
WHERE branches.isDeleted = 1
AND branches.deleteId = ?
AND branches.parentNoteId = ?`, [deleteId, note.noteId]);
for (const childBranch of childBranches) {
await undeleteBranch(childBranch, deleteId, taskContext);
}
}
}
/**
* @return return deleted branches of an undeleted parent note
*/
async function getUndeletedParentBranches(noteId, deleteId) {
return await repository.getEntities(`
SELECT branches.*
FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
WHERE branches.noteId = ?
AND branches.isDeleted = 1
AND branches.deleteId = ?
AND parentNote.isDeleted = 0`, [noteId, deleteId]);
}
async function scanForLinks(noteId) {
const note = await repository.getNote(noteId);
if (!note || !['text', 'relation-map'].includes(note.type)) {
@@ -453,7 +579,9 @@ async function scanForLinks(noteId) {
}
async function eraseDeletedNotes() {
const cutoffDate = new Date(Date.now() - 48 * 3600 * 1000);
const eraseNotesAfterTimeInSeconds = await optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
const noteIdsToErase = await sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND isErased = 0 AND notes.utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]);
@@ -467,10 +595,12 @@ async function eraseDeletedNotes() {
// - we don't want change the hash since this erasing happens on each instance separately
// and changing the hash would fire up the sync errors temporarily
// setting contentLength to zero would serve no benefit and it leaves potentially useful trail
await sql.executeMany(`
UPDATE notes
SET isErased = 1
SET title = '[deleted]',
contentLength = 0,
isProtected = 0,
isErased = 1
WHERE noteId IN (???)`, noteIdsToErase);
await sql.executeMany(`
@@ -488,9 +618,16 @@ async function eraseDeletedNotes() {
await sql.executeMany(`
UPDATE note_revisions
SET isErased = 1,
title = NULL
title = NULL,
contentLength = 0
WHERE isErased = 0 AND noteId IN (???)`, noteIdsToErase);
await sql.executeMany(`
UPDATE attributes
SET name = 'deleted',
value = ''
WHERE noteId IN (???)`, noteIdsToErase);
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
}
@@ -544,7 +681,9 @@ module.exports = {
createNewNoteWithTarget,
updateNote,
deleteBranch,
undeleteNote,
protectNoteRecursively,
scanForLinks,
duplicateNote
duplicateNote,
getUndeletedParentBranches
};

View File

@@ -80,7 +80,8 @@ const defaultOptions = [
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false }
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false },
{ name: 'eraseNotesAfterTimeInSeconds', value: '604800', isSynced: true } // default is 7 days
];
async function initStartupOptions() {

View File

@@ -2,7 +2,6 @@ const sql = require('./sql');
const ScriptContext = require('./script_context');
const repository = require('./repository');
const cls = require('./cls');
const sourceIdService = require('./source_id');
const log = require('./log');
async function executeNote(note, apiParams) {

View File

@@ -209,6 +209,8 @@ async function transactional(func) {
transactionActive = false;
resolve();
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
catch (e) {
if (transactionActive) {

View File

@@ -176,7 +176,7 @@ async function pushSync(syncContext) {
let lastSyncedPush = await getLastSyncedPush();
while (true) {
const syncs = await sql.getRows('SELECT * FROM sync WHERE id > ? LIMIT 1000', [lastSyncedPush]);
const syncs = await sql.getRows('SELECT * FROM sync WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]);
if (syncs.length === 0) {
log.info("Nothing to push");
@@ -228,14 +228,15 @@ async function syncFinished(syncContext) {
async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
const lastSyncedPullId = await getLastSyncedPull();
if (await getLastSyncedPull() < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check.");
if (lastSyncedPullId < resp.maxSyncId) {
log.info(`There are some outstanding pulls (${lastSyncedPullId} vs. ${resp.maxSyncId}), skipping content check.`);
return true;
}
const notPushedSyncs = await sql.getValue("SELECT EXISTS(SELECT 1 FROM sync WHERE id > ?)", [await getLastSyncedPush()]);
const notPushedSyncs = await sql.getValue("SELECT EXISTS(SELECT 1 FROM sync WHERE isSynced = 1 AND id > ?)", [await getLastSyncedPush()]);
if (notPushedSyncs) {
log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
@@ -352,7 +353,7 @@ async function updatePushStats() {
if (await syncOptions.isSyncSetup()) {
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
stats.outstandingPushes = await sql.getValue("SELECT COUNT(1) FROM sync WHERE id > ?", [lastSyncedPush]);
stats.outstandingPushes = await sql.getValue("SELECT COUNT(1) FROM sync WHERE isSynced = 1 AND id > ?", [lastSyncedPush]);
}
}

View File

@@ -11,7 +11,8 @@ async function insertEntitySync(entityName, entityId, sourceId) {
entityName: entityName,
entityId: entityId,
utcSyncDate: dateUtils.utcNowDateTime(),
sourceId: sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId()
sourceId: sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId(),
isSynced: 1
};
sync.id = await sql.replace("sync", sync);
@@ -23,8 +24,6 @@ async function addEntitySync(entityName, entityId, sourceId) {
const sync = await insertEntitySync(entityName, entityId, sourceId);
syncs.push(sync);
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
async function addEntitySyncsForSector(entityName, entityPrimaryKey, sector) {

View File

@@ -71,22 +71,36 @@ function sendMessageToAllClients(message) {
}
}
async function fillInAdditionalProperties(sync) {
// fill in some extra data needed by the frontend
if (sync.entityName === 'attributes') {
sync.noteId = await sql.getValue(`SELECT noteId
FROM attributes
WHERE attributeId = ?`, [sync.entityId]);
} else if (sync.entityName === 'note_revisions') {
sync.noteId = await sql.getValue(`SELECT noteId
FROM note_revisions
WHERE noteRevisionId = ?`, [sync.entityId]);
} else if (sync.entityName === 'branches') {
const {noteId, parentNoteId} = await sql.getRow(`SELECT noteId, parentNoteId
FROM branches
WHERE branchId = ?`, [sync.entityId]);
sync.noteId = noteId;
sync.parentNoteId = parentNoteId;
}
}
async function sendPing(client) {
const syncData = require('./sync_table').getEntitySyncsNewerThan(lastAcceptedSyncIds[client.id]);
for (const sync of syncData) {
// fill in some extra data needed by the frontend
if (sync.entityName === 'attributes') {
sync.noteId = await sql.getValue(`SELECT noteId FROM attributes WHERE attributeId = ?`, [sync.entityId]);
try {
await fillInAdditionalProperties(sync);
}
else if (sync.entityName === 'note_revisions') {
sync.noteId = await sql.getValue(`SELECT noteId FROM note_revisions WHERE noteRevisionId = ?`, [sync.entityId]);
}
else if (sync.entityName === 'branches') {
const {noteId, parentNoteId} = await sql.getRow(`SELECT noteId, parentNoteId FROM branches WHERE branchId = ?`, [sync.entityId]);
sync.noteId = noteId;
sync.parentNoteId = parentNoteId;
catch (e) {
log.error("Could not fill additional properties for sync " + JSON.stringify(sync)
+ " because of error: " + e.message + ": " + e.stack);
}
}

View File

@@ -250,6 +250,7 @@
<link href="stylesheets/themes.css" rel="stylesheet">
<link href="stylesheets/style.css" rel="stylesheet">
<link href="stylesheets/detail.css" rel="stylesheet">
<link href="stylesheets/desktop.css" rel="stylesheet">
<script src="javascripts/desktop.js" crossorigin type="module"></script>

View File

@@ -1,5 +1,5 @@
<div class="note-detail-image note-detail-component">
<div style="display: flex; justify-content: space-evenly; margin: 10px;">
<div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;">
<button class="image-download btn btn-sm btn-primary" type="button">Download</button>
<button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button>

View File

@@ -1,5 +1,5 @@
<div class="note-detail-relation-map note-detail-component">
<button class="relation-map-create-child-note btn btn-sm floating-button" type="button"
<button class="relation-map-create-child-note btn btn-sm floating-button no-print" type="button"
title="Create new child note and add it into this relation map">
<span class="bx bx-folder-plus"></span>
@@ -7,11 +7,11 @@
</button>
<button type="button"
class="relation-map-reset-pan-zoom btn icon-button floating-button bx bx-crop"
class="relation-map-reset-pan-zoom btn icon-button floating-button bx bx-crop no-print"
title="Reset pan & zoom to initial coordinates and magnification"
style="right: 70px;"></button>
<div class="btn-group floating-button" style="right: 10px;">
<div class="btn-group floating-button no-print" style="right: 10px;">
<button type="button"
class="relation-map-zoom-in btn icon-button bx bx-zoom-in"
title="Zoom In"></button>

View File

@@ -9,7 +9,7 @@
</button>
</div>
<div class="modal-body">
<table class="table">
<table class="table table-borderless">
<tr>
<th>Homepage:</th>
<td><a href="https://github.com/zadam/trilium" class="external">https://github.com/zadam/trilium</a></td>

View File

@@ -73,6 +73,8 @@
<% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %>
<% include details/book.ejs %>
</div>
</div>
</div>
@@ -116,6 +118,7 @@
<link href="stylesheets/themes.css" rel="stylesheet">
<link href="stylesheets/style.css" rel="stylesheet">
<link href="stylesheets/detail.css" rel="stylesheet">
<link href="stylesheets/mobile.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css">

View File

@@ -19,13 +19,13 @@
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
I have server instance up and I want to setup sync with it</label>
I have server instance already and I want to setup sync with it</label>
</div>
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>