mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1bc46c04 | ||
|
|
4bc44605fb | ||
|
|
b868990fba | ||
|
|
26c06c9826 | ||
|
|
f5b89432a6 | ||
|
|
0e7372adbf | ||
|
|
d4fbe28517 | ||
|
|
668528d5eb | ||
|
|
17348a9cfe | ||
|
|
09b610701d | ||
|
|
71e687ad8e | ||
|
|
171877ce08 | ||
|
|
4f1e6ec70f | ||
|
|
1938c317c3 | ||
|
|
99d81059d0 | ||
|
|
59d5a86110 | ||
|
|
a5e56ea839 | ||
|
|
44f85224e7 | ||
|
|
0aa08b1c1e | ||
|
|
406d74c4d7 | ||
|
|
7f9a8a55ca | ||
|
|
a42bbba0e5 | ||
|
|
145efe67c3 | ||
|
|
513748836e | ||
|
|
427ce3972e | ||
|
|
02c0f9a6cd | ||
|
|
208771216e | ||
|
|
385d97a9b3 | ||
|
|
e39d1d08ac | ||
|
|
0f106fb96f | ||
|
|
df9acd0504 | ||
|
|
dbe0eb3f3a | ||
|
|
4513651e12 | ||
|
|
3204291463 | ||
|
|
510704a074 | ||
|
|
f440493e45 | ||
|
|
b897c6de13 | ||
|
|
acbd18e8fc | ||
|
|
ff5b84db10 | ||
|
|
16535f6a73 | ||
|
|
5b657ad961 | ||
|
|
bbbc3e9dc4 | ||
|
|
f43f0e10a1 | ||
|
|
6d842a65a2 | ||
|
|
50c4de021c | ||
|
|
936d8449f6 | ||
|
|
462bc0edd5 | ||
|
|
35ef3c8470 | ||
|
|
5117d43e29 | ||
|
|
7c9ac488e8 | ||
|
|
fec1574447 | ||
|
|
f7587de452 | ||
|
|
41a6e777ea | ||
|
|
8fb0de900b | ||
|
|
a40bf71fd4 | ||
|
|
2a53bb03ae | ||
|
|
a684879b91 | ||
|
|
ddbd4f73c8 | ||
|
|
b0ed790edf | ||
|
|
3424406ff1 | ||
|
|
ce5c385c15 | ||
|
|
cd9eef32b0 | ||
|
|
12d82e3b33 | ||
|
|
f071d3f651 | ||
|
|
297b536ebc | ||
|
|
7cca2d9247 | ||
|
|
36dc802d16 | ||
|
|
c78ddb70cb | ||
|
|
9fb0599c45 | ||
|
|
13f524fb39 | ||
|
|
27be3b4c90 | ||
|
|
af4ea66742 | ||
|
|
0f42c396f3 | ||
|
|
9e96272eb3 | ||
|
|
965dbcbc9a | ||
|
|
7ac109e7f7 | ||
|
|
ac25770c0e | ||
|
|
5b15424498 | ||
|
|
f1240c26bf | ||
|
|
1c0fd243d1 | ||
|
|
3491235533 | ||
|
|
5f36856571 | ||
|
|
d3e44b37e9 | ||
|
|
90e9297ec5 | ||
|
|
c568ef2f8a | ||
|
|
fcf6141cde | ||
|
|
21551d7b77 | ||
|
|
12031d369f | ||
|
|
b44c523845 | ||
|
|
49989695ff | ||
|
|
a55d3530e9 | ||
|
|
2aab3ad281 | ||
|
|
194ce4f10f | ||
|
|
2089c32839 | ||
|
|
f437be7af0 | ||
|
|
96dc56098d | ||
|
|
61987e46f7 | ||
|
|
509093b755 | ||
|
|
097114c0f2 | ||
|
|
040f9185f8 | ||
|
|
6dc934abbe | ||
|
|
2d24bf81dd | ||
|
|
9452fc236b | ||
|
|
365c37604b | ||
|
|
01c7e58d47 | ||
|
|
d3d49923b1 | ||
|
|
263ac299d0 | ||
|
|
3d185a5178 | ||
|
|
2ff7a890bc | ||
|
|
2eb1a9705f | ||
|
|
ed1381103a | ||
|
|
170d317589 | ||
|
|
ededc063df | ||
|
|
986eace1be | ||
|
|
29086d8dfe | ||
|
|
9b3f3fde05 | ||
|
|
6a50afd952 | ||
|
|
697eee2706 | ||
|
|
8a95afd756 | ||
|
|
4d6eda8fe6 | ||
|
|
e4f459fa2b | ||
|
|
f578e001b0 | ||
|
|
2a08aef885 | ||
|
|
7564bf388c | ||
|
|
7e4d70259f | ||
|
|
5b98c1c0f3 | ||
|
|
02dc7b199b | ||
|
|
d39cdbfada | ||
|
|
50bb4a47ee | ||
|
|
a4627f2ddb | ||
|
|
c8253caae9 | ||
|
|
0ece9bd1be | ||
|
|
b6935abcc9 | ||
|
|
37ab7b4641 | ||
|
|
013714cb5c | ||
|
|
1fe7c62f5a | ||
|
|
a06618d851 | ||
|
|
e7460ca3a9 | ||
|
|
073300bbcd | ||
|
|
a201661ce5 | ||
|
|
6235a3c886 | ||
|
|
3972c27e7a | ||
|
|
14cffbbe62 | ||
|
|
599c3c04af | ||
|
|
f1412b631d | ||
|
|
41908050bb | ||
|
|
f07033423c | ||
|
|
daf96fcbf2 | ||
|
|
2bca94529e | ||
|
|
b2c9a0da21 | ||
|
|
7a9542b4fc | ||
|
|
3a95c9e1bc | ||
|
|
3d2ef6be01 | ||
|
|
d67246699a | ||
|
|
14c704d6db | ||
|
|
4c8eeb2e6f | ||
|
|
c1b245c8b1 | ||
|
|
74202d67bb | ||
|
|
26066f39b1 | ||
|
|
b255cf190c | ||
|
|
bc77b143b0 | ||
|
|
9f0ff6ae7a | ||
|
|
736704c7d6 | ||
|
|
654c116c58 | ||
|
|
89a5cab98f | ||
|
|
c39d0be8cd | ||
|
|
e75b4cd848 |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
.idea
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,5 @@ yarn-error.log
|
||||
*.db
|
||||
config.ini
|
||||
cert.key
|
||||
cert.crt
|
||||
cert.crt
|
||||
docs/
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<dataSource name="document.db">
|
||||
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.9">
|
||||
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.11">
|
||||
<root id="1">
|
||||
<ServerVersion>3.16.1</ServerVersion>
|
||||
</root>
|
||||
@@ -12,10 +12,10 @@
|
||||
<collation id="4" parent="1" name="NOCASE"/>
|
||||
<collation id="5" parent="1" name="RTRIM"/>
|
||||
<table id="6" parent="2" name="api_tokens"/>
|
||||
<table id="7" parent="2" name="branches"/>
|
||||
<table id="8" parent="2" name="event_log"/>
|
||||
<table id="9" parent="2" name="images"/>
|
||||
<table id="10" parent="2" name="labels"/>
|
||||
<table id="7" parent="2" name="attributes"/>
|
||||
<table id="8" parent="2" name="branches"/>
|
||||
<table id="9" parent="2" name="event_log"/>
|
||||
<table id="10" parent="2" name="images"/>
|
||||
<table id="11" parent="2" name="note_images"/>
|
||||
<table id="12" parent="2" name="note_revisions"/>
|
||||
<table id="13" parent="2" name="notes"/>
|
||||
@@ -59,7 +59,6 @@
|
||||
<index id="25" parent="6" name="sqlite_autoindex_api_tokens_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>apiTokenId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="26" parent="6">
|
||||
@@ -67,7 +66,7 @@
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="27" parent="7" name="branchId">
|
||||
<column id="27" parent="7" name="attributeId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
@@ -77,589 +76,574 @@
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="29" parent="7" name="parentNoteId">
|
||||
<column id="29" parent="7" name="type">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="30" parent="7" name="notePosition">
|
||||
<column id="30" parent="7" name="name">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="31" parent="7" name="prefix">
|
||||
<column id="31" parent="7" name="value">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="32" parent="7" name="isExpanded">
|
||||
<column id="32" parent="7" name="position">
|
||||
<Position>6</Position>
|
||||
<DataType>BOOLEAN|0s</DataType>
|
||||
</column>
|
||||
<column id="33" parent="7" name="isDeleted">
|
||||
<Position>7</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="33" parent="7" name="dateCreated">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="34" parent="7" name="dateModified">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="35" parent="7" name="hash">
|
||||
<column id="35" parent="7" name="isDeleted">
|
||||
<Position>9</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="36" parent="7" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="37" parent="7" name="isInheritable">
|
||||
<Position>11</Position>
|
||||
<DataType>int|0s</DataType>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<index id="38" parent="7" name="sqlite_autoindex_attributes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>attributeId</ColNames>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="39" parent="7">
|
||||
<ColNames>attributeId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="40" parent="8" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="41" parent="8" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="42" parent="8" name="parentNoteId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="43" parent="8" name="notePosition">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="44" parent="8" name="prefix">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="45" parent="8" name="isExpanded">
|
||||
<Position>6</Position>
|
||||
<DataType>BOOLEAN|0s</DataType>
|
||||
</column>
|
||||
<column id="46" parent="8" name="isDeleted">
|
||||
<Position>7</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="47" parent="8" name="dateModified">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="48" parent="8" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="36" parent="7" name="dateCreated">
|
||||
<column id="49" parent="8" name="dateCreated">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="37" parent="7" name="sqlite_autoindex_branches_1">
|
||||
<index id="50" parent="8" name="sqlite_autoindex_branches_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="38" parent="7" name="IDX_branches_noteId_parentNoteId">
|
||||
<index id="51" parent="8" name="IDX_branches_noteId_parentNoteId">
|
||||
<ColNames>noteId
|
||||
parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="39" parent="7" name="IDX_branches_noteId">
|
||||
<index id="52" parent="8" name="IDX_branches_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="40" parent="7" name="IDX_branches_parentNoteId">
|
||||
<index id="53" parent="8" name="IDX_branches_parentNoteId">
|
||||
<ColNames>parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="41" parent="7">
|
||||
<key id="54" parent="8">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="42" parent="8" name="id">
|
||||
<column id="55" parent="9" name="eventId">
|
||||
<Position>1</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<SequenceIdentity>1</SequenceIdentity>
|
||||
</column>
|
||||
<column id="43" parent="8" name="noteId">
|
||||
<column id="56" parent="9" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="44" parent="8" name="comment">
|
||||
<column id="57" parent="9" name="comment">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="45" parent="8" name="dateCreated">
|
||||
<column id="58" parent="9" name="dateCreated">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<key id="46" parent="8">
|
||||
<ColNames>id</ColNames>
|
||||
<index id="59" parent="9" name="sqlite_autoindex_event_log_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>eventId</ColNames>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="60" parent="9">
|
||||
<ColNames>eventId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_event_log_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="47" parent="9" name="imageId">
|
||||
<column id="61" parent="10" name="imageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="48" parent="9" name="format">
|
||||
<column id="62" parent="10" name="format">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="49" parent="9" name="checksum">
|
||||
<column id="63" parent="10" name="checksum">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="50" parent="9" name="name">
|
||||
<column id="64" parent="10" name="name">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="51" parent="9" name="data">
|
||||
<column id="65" parent="10" name="data">
|
||||
<Position>5</Position>
|
||||
<DataType>BLOB|0s</DataType>
|
||||
</column>
|
||||
<column id="52" parent="9" name="isDeleted">
|
||||
<column id="66" parent="10" name="isDeleted">
|
||||
<Position>6</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="53" parent="9" name="dateModified">
|
||||
<column id="67" parent="10" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="54" parent="9" name="dateCreated">
|
||||
<column id="68" parent="10" name="dateCreated">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="55" parent="9" name="hash">
|
||||
<column id="69" parent="10" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="56" parent="9" name="sqlite_autoindex_images_1">
|
||||
<index id="70" parent="10" name="sqlite_autoindex_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="57" parent="9">
|
||||
<key id="71" parent="10">
|
||||
<ColNames>imageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="58" parent="10" name="labelId">
|
||||
<column id="72" parent="11" name="noteImageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="59" parent="10" name="noteId">
|
||||
<column id="73" parent="11" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="60" parent="10" name="name">
|
||||
<column id="74" parent="11" name="imageId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="61" parent="10" name="value">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="62" parent="10" name="position">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="63" parent="10" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="64" parent="10" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="65" parent="10" name="isDeleted">
|
||||
<Position>8</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="66" parent="10" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="67" parent="10" name="sqlite_autoindex_labels_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>labelId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="68" parent="10" name="IDX_labels_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="69" parent="10" name="IDX_labels_name_value">
|
||||
<ColNames>name
|
||||
value</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="70" parent="10">
|
||||
<ColNames>labelId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="71" parent="11" name="noteImageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="72" parent="11" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="73" parent="11" name="imageId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="74" parent="11" name="isDeleted">
|
||||
<column id="75" parent="11" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="75" parent="11" name="dateModified">
|
||||
<column id="76" parent="11" name="dateModified">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="76" parent="11" name="dateCreated">
|
||||
<column id="77" parent="11" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="77" parent="11" name="hash">
|
||||
<column id="78" parent="11" name="hash">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="78" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<index id="79" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="79" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<index id="80" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<ColNames>noteId
|
||||
imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="80" parent="11" name="IDX_note_images_noteId">
|
||||
<index id="81" parent="11" name="IDX_note_images_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="81" parent="11" name="IDX_note_images_imageId">
|
||||
<index id="82" parent="11" name="IDX_note_images_imageId">
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="82" parent="11">
|
||||
<key id="83" parent="11">
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="83" parent="12" name="noteRevisionId">
|
||||
<column id="84" parent="12" name="noteRevisionId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="84" parent="12" name="noteId">
|
||||
<column id="85" parent="12" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="85" parent="12" name="title">
|
||||
<column id="86" parent="12" name="title">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="86" parent="12" name="content">
|
||||
<column id="87" parent="12" name="content">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="87" parent="12" name="isProtected">
|
||||
<column id="88" parent="12" name="isProtected">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="88" parent="12" name="dateModifiedFrom">
|
||||
<column id="89" parent="12" name="dateModifiedFrom">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="89" parent="12" name="dateModifiedTo">
|
||||
<column id="90" parent="12" name="dateModifiedTo">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="90" parent="12" name="type">
|
||||
<column id="91" parent="12" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="91" parent="12" name="mime">
|
||||
<column id="92" parent="12" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="92" parent="12" name="hash">
|
||||
<column id="93" parent="12" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="93" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<index id="94" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="94" parent="12" name="IDX_note_revisions_noteId">
|
||||
<index id="95" parent="12" name="IDX_note_revisions_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="95" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<ColNames>dateModifiedFrom</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<ColNames>dateModifiedTo</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="97" parent="12">
|
||||
<key id="98" parent="12">
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="98" parent="13" name="noteId">
|
||||
<column id="99" parent="13" name="noteId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="99" parent="13" name="title">
|
||||
<column id="100" parent="13" name="title">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>"unnamed"</DefaultExpression>
|
||||
</column>
|
||||
<column id="100" parent="13" name="content">
|
||||
<column id="101" parent="13" name="content">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="101" parent="13" name="isProtected">
|
||||
<column id="102" parent="13" name="isProtected">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="102" parent="13" name="isDeleted">
|
||||
<column id="103" parent="13" name="isDeleted">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="103" parent="13" name="dateCreated">
|
||||
<column id="104" parent="13" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="104" parent="13" name="dateModified">
|
||||
<column id="105" parent="13" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="105" parent="13" name="type">
|
||||
<column id="106" parent="13" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text'</DefaultExpression>
|
||||
</column>
|
||||
<column id="106" parent="13" name="mime">
|
||||
<column id="107" parent="13" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text/html'</DefaultExpression>
|
||||
</column>
|
||||
<column id="107" parent="13" name="hash">
|
||||
<column id="108" parent="13" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="108" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<index id="109" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="109" parent="13" name="IDX_notes_type">
|
||||
<index id="110" parent="13" name="IDX_notes_type">
|
||||
<ColNames>type</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="110" parent="13">
|
||||
<key id="111" parent="13">
|
||||
<ColNames>noteId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="111" parent="14" name="name">
|
||||
<column id="112" parent="14" name="name">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="112" parent="14" name="value">
|
||||
<column id="113" parent="14" name="value">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="113" parent="14" name="dateModified">
|
||||
<column id="114" parent="14" name="dateModified">
|
||||
<Position>3</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<column id="114" parent="14" name="isSynced">
|
||||
<column id="115" parent="14" name="isSynced">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="115" parent="14" name="hash">
|
||||
<column id="116" parent="14" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="116" parent="14" name="dateCreated">
|
||||
<column id="117" parent="14" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="117" parent="14" name="sqlite_autoindex_options_1">
|
||||
<index id="118" parent="14" name="sqlite_autoindex_options_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>name</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="118" parent="14">
|
||||
<key id="119" parent="14">
|
||||
<ColNames>name</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="119" parent="15" name="branchId">
|
||||
<column id="120" parent="15" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="120" parent="15" name="notePath">
|
||||
<column id="121" parent="15" name="notePath">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="121" parent="15" name="dateCreated">
|
||||
<column id="122" parent="15" name="hash">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="122" parent="15" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<column id="123" parent="15" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="124" parent="15" name="sqlite_autoindex_recent_notes_1">
|
||||
<column id="123" parent="15" name="dateCreated">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="124" parent="15" name="isDeleted">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<index id="125" parent="15" name="sqlite_autoindex_recent_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="125" parent="15">
|
||||
<key id="126" parent="15">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="126" parent="16" name="sourceId">
|
||||
<column id="127" parent="16" name="sourceId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="127" parent="16" name="dateCreated">
|
||||
<column id="128" parent="16" name="dateCreated">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="128" parent="16" name="sqlite_autoindex_source_ids_1">
|
||||
<index id="129" parent="16" name="sqlite_autoindex_source_ids_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>sourceId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="129" parent="16">
|
||||
<key id="130" parent="16">
|
||||
<ColNames>sourceId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="130" parent="17" name="type">
|
||||
<column id="131" parent="17" name="type">
|
||||
<Position>1</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="131" parent="17" name="name">
|
||||
<column id="132" parent="17" name="name">
|
||||
<Position>2</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="132" parent="17" name="tbl_name">
|
||||
<column id="133" parent="17" name="tbl_name">
|
||||
<Position>3</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="133" parent="17" name="rootpage">
|
||||
<column id="134" parent="17" name="rootpage">
|
||||
<Position>4</Position>
|
||||
<DataType>integer|0s</DataType>
|
||||
</column>
|
||||
<column id="134" parent="17" name="sql">
|
||||
<column id="135" parent="17" name="sql">
|
||||
<Position>5</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="135" parent="18" name="name">
|
||||
<column id="136" parent="18" name="name">
|
||||
<Position>1</Position>
|
||||
</column>
|
||||
<column id="136" parent="18" name="seq">
|
||||
<column id="137" parent="18" name="seq">
|
||||
<Position>2</Position>
|
||||
</column>
|
||||
<column id="137" parent="19" name="id">
|
||||
<column id="138" parent="19" name="id">
|
||||
<Position>1</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<SequenceIdentity>1</SequenceIdentity>
|
||||
</column>
|
||||
<column id="138" parent="19" name="entityName">
|
||||
<column id="139" parent="19" name="entityName">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="139" parent="19" name="entityId">
|
||||
<column id="140" parent="19" name="entityId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="140" parent="19" name="sourceId">
|
||||
<column id="141" parent="19" name="sourceId">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="141" parent="19" name="syncDate">
|
||||
<column id="142" parent="19" name="syncDate">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="142" parent="19" name="IDX_sync_entityName_entityId">
|
||||
<index id="143" parent="19" name="IDX_sync_entityName_entityId">
|
||||
<ColNames>entityName
|
||||
entityId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="143" parent="19" name="IDX_sync_syncDate">
|
||||
<index id="144" parent="19" name="IDX_sync_syncDate">
|
||||
<ColNames>syncDate</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="144" parent="19">
|
||||
<key id="145" parent="19">
|
||||
<ColNames>id</ColNames>
|
||||
<Primary>1</Primary>
|
||||
</key>
|
||||
|
||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM node:8.11.2
|
||||
|
||||
RUN apt-get update && apt-get install -y nasm
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||
# where available (npm@5+)
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install --production
|
||||
# If you are building your code for production
|
||||
# RUN npm install --only=production
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
CMD [ "node", "src/www" ]
|
||||
8
bin/build-docker.sh
Executable file
8
bin/build-docker.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo docker build -t zadam/trilium:latest -t zadam/trilium:$1 .
|
||||
23
bin/build-pkg.sh
Executable file
23
bin/build-pkg.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
|
||||
mkdir $PKG_DIR
|
||||
|
||||
pkg . --targets node8-linux-x64 --output ${PKG_DIR}/trilium
|
||||
|
||||
chmod +x ${PKG_DIR}/trilium
|
||||
|
||||
cp node_modules/sqlite3/lib/binding/node-v57-linux-x64/node_sqlite3.node ${PKG_DIR}/
|
||||
cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
|
||||
|
||||
cd dist
|
||||
|
||||
7z a trilium-linux-x64-${VERSION}-server.7z trilium-linux-x64-server
|
||||
9
bin/push-docker-image.sh
Executable file
9
bin/push-docker-image.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo docker push zadam/trilium:latest
|
||||
sudo docker push zadam/trilium:$1
|
||||
@@ -47,6 +47,7 @@ bin/package.sh
|
||||
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
|
||||
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
|
||||
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
|
||||
SERVER_BUILD=trilium-linux-x64-server-$VERSION.7z
|
||||
|
||||
echo "Creating release in GitHub"
|
||||
|
||||
@@ -75,4 +76,21 @@ github-release upload \
|
||||
--name "$WINDOWS_X64_BUILD" \
|
||||
--file "dist/$WINDOWS_X64_BUILD"
|
||||
|
||||
echo "Packaging server version"
|
||||
|
||||
bin/build-pkg.sh $VERSION
|
||||
|
||||
github-release upload \
|
||||
--tag $TAG \
|
||||
--name "$SERVER_BUILD" \
|
||||
--file "dist/$SERVER_BUILD"
|
||||
|
||||
echo "Building docker image"
|
||||
|
||||
bin/build-docker.sh $VERSION
|
||||
|
||||
echo "Pushing docker image to dockerhub"
|
||||
|
||||
bin/push-docker-image.sh $VERSION
|
||||
|
||||
echo "Release finished!"
|
||||
@@ -3,15 +3,10 @@
|
||||
instanceName=
|
||||
|
||||
[Network]
|
||||
# port setting is relevant only for web deployments, desktop builds run on random free port
|
||||
port=8080
|
||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
||||
https=false
|
||||
# path to certificate (run "bash generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
|
||||
# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
|
||||
certPath=
|
||||
keyPath=
|
||||
|
||||
[Sync]
|
||||
syncServerHost=
|
||||
syncServerTimeout=10000
|
||||
syncProxy=
|
||||
syncServerCertificate=
|
||||
2
db/migrations/0099__add_theme_option.sql
Normal file
2
db/migrations/0099__add_theme_option.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('theme_key', 'theme', 'white', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
15
db/migrations/0100__remove_optionId.sql
Normal file
15
db/migrations/0100__remove_optionId.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
create table options_mig
|
||||
(
|
||||
name TEXT not null PRIMARY KEY,
|
||||
value TEXT,
|
||||
dateModified INT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
hash TEXT default "" not null,
|
||||
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
|
||||
);
|
||||
|
||||
INSERT INTO options_mig (name, value, dateModified, isSynced, hash, dateCreated)
|
||||
SELECT name, value, dateModified, isSynced, hash, dateCreated FROM options;
|
||||
|
||||
DROP TABLE options;
|
||||
ALTER TABLE options_mig RENAME TO options;
|
||||
8
db/migrations/0101__add_sync_options.sql
Normal file
8
db/migrations/0101__add_sync_options.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('syncServerHost', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('syncServerTimeout', '5000', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('syncProxy', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
2
db/migrations/0102__fix_sync_entityIds.sql
Normal file
2
db/migrations/0102__fix_sync_entityIds.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DELETE FROM sync WHERE entityName = 'note_tree';
|
||||
DELETE FROM sync WHERE entityName = 'attributes';
|
||||
2
db/migrations/0103__add_initialized_option.sql
Normal file
2
db/migrations/0103__add_initialized_option.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('initialized', 'true', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
4
db/migrations/0104__fill_sync_rows_for_options.js
Normal file
4
db/migrations/0104__fill_sync_rows_for_options.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const syncTableService = require('../../src/services/sync_table');
|
||||
|
||||
// options has not been filled so far which caused problems with clean-slate sync.
|
||||
module.exports = async () => await syncTableService.fillAllSyncRows();
|
||||
@@ -0,0 +1,2 @@
|
||||
UPDATE notes SET content = '' WHERE isDeleted = 1;
|
||||
UPDATE note_revisions SET content = '' WHERE (SELECT isDeleted FROM notes WHERE noteId = note_revisions.noteId) = 1;
|
||||
15
db/migrations/0106__add_relations_table.sql
Normal file
15
db/migrations/0106__add_relations_table.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE relations
|
||||
(
|
||||
relationId TEXT not null primary key,
|
||||
sourceNoteId TEXT not null,
|
||||
name TEXT not null,
|
||||
targetNoteId TEXT not null,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_relation_sourceNoteId
|
||||
on relations (sourceNoteId);
|
||||
CREATE INDEX IDX_relation_targetNoteId
|
||||
on relations (targetNoteId);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE relations ADD isInheritable int DEFAULT 0 NULL;
|
||||
9
db/migrations/0108__new_backup_options.sql
Normal file
9
db/migrations/0108__new_backup_options.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
UPDATE options SET name = 'lastDailyBackupDate' WHERE name = 'lastBackupDate';
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('lastWeeklyBackupDate', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('lastMonthlyBackupDate', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', 0);
|
||||
|
||||
-- these options are not synced so no need to fix sync rows
|
||||
27
db/migrations/0109__create_attributes.sql
Normal file
27
db/migrations/0109__create_attributes.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
create table 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,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
hash TEXT default "" not null);
|
||||
|
||||
create index IDX_attributes_name_value
|
||||
on attributes (name, value);
|
||||
|
||||
create index IDX_attributes_value
|
||||
on attributes (value);
|
||||
|
||||
create index IDX_attributes_noteId
|
||||
on attributes (noteId);
|
||||
|
||||
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
|
||||
SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels;
|
||||
|
||||
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
|
||||
SELECT relationId, sourceNoteId, 'relation', name, targetNoteId, position, dateCreated, dateModified, isDeleted, hash FROM relations;
|
||||
1
db/migrations/0110__add_isInheritable_to_attributes.sql
Normal file
1
db/migrations/0110__add_isInheritable_to_attributes.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE attributes ADD isInheritable int DEFAULT 0 NULL;
|
||||
4
db/migrations/0111__cleanup_labels_and_relations.sql
Normal file
4
db/migrations/0111__cleanup_labels_and_relations.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
DROP TABLE relations;
|
||||
DROP TABLE labels;
|
||||
|
||||
DELETE FROM sync WHERE entityName = 'relations' OR entityName = 'labels';
|
||||
1
db/migrations/0112__rename_inheritAttributes.sql
Normal file
1
db/migrations/0112__rename_inheritAttributes.sql
Normal file
@@ -0,0 +1 @@
|
||||
UPDATE attributes SET name = 'template' WHERE name = 'inheritAttributes';
|
||||
@@ -82,21 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
|
||||
`noteId`,
|
||||
`parentNoteId`
|
||||
);
|
||||
CREATE TABLE labels
|
||||
(
|
||||
labelId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_labels_name_value
|
||||
on labels (name, value);
|
||||
CREATE INDEX IDX_labels_noteId
|
||||
on labels (noteId);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "unnamed",
|
||||
@@ -115,9 +100,10 @@ CREATE INDEX IDX_notes_type
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
|
||||
`branchId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
hash TEXT DEFAULT "" NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "event_log" (
|
||||
`eventId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT,
|
||||
@@ -126,11 +112,22 @@ CREATE TABLE IF NOT EXISTS "event_log" (
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "options"
|
||||
(
|
||||
optionId TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT not null,
|
||||
name TEXT not null PRIMARY KEY,
|
||||
value TEXT,
|
||||
dateModified INT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
hash TEXT default "" not null,
|
||||
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
|
||||
);
|
||||
CREATE TABLE 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,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
hash TEXT default "" not null, isInheritable int DEFAULT 0 NULL);
|
||||
|
||||
10
electron.js
10
electron.js
@@ -2,9 +2,9 @@
|
||||
|
||||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
const config = require('./src/services/config');
|
||||
const log = require('./src/services/log');
|
||||
const url = require("url");
|
||||
const port = require('./src/services/port');
|
||||
|
||||
const app = electron.app;
|
||||
const globalShortcut = electron.globalShortcut;
|
||||
@@ -23,7 +23,7 @@ function onClosed() {
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
async function createMainWindow() {
|
||||
const win = new electron.BrowserWindow({
|
||||
width: 1200,
|
||||
height: 900,
|
||||
@@ -31,14 +31,12 @@ function createMainWindow() {
|
||||
icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
|
||||
});
|
||||
|
||||
const port = config['Network']['port'] || '3000';
|
||||
|
||||
win.setMenu(null);
|
||||
win.loadURL('http://localhost:' + port);
|
||||
win.loadURL('http://localhost:' + await port);
|
||||
win.on('closed', onClosed);
|
||||
|
||||
win.webContents.on('new-window', (e, url) => {
|
||||
if (url !== mainWindow.webContents.getURL()) {
|
||||
if (url !== win.webContents.getURL()) {
|
||||
e.preventDefault();
|
||||
require('electron').shell.openExternal(url);
|
||||
}
|
||||
|
||||
8422
package-lock.json
generated
8422
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
118
package.json
118
package.json
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.15.0",
|
||||
"version": "0.20.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
"trilium": "./src/www"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zadam/trilium.git"
|
||||
@@ -17,60 +20,65 @@
|
||||
"start-forge": "electron-forge start",
|
||||
"package-forge": "electron-forge package",
|
||||
"make-forge": "electron-forge make",
|
||||
"publish-forge": "electron-forge publish"
|
||||
"publish-forge": "electron-forge publish",
|
||||
"build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
|
||||
"build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
|
||||
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.3",
|
||||
"axios": "^0.18",
|
||||
"body-parser": "^1.18.3",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"debug": "~3.1.0",
|
||||
"devtron": "^1.4.0",
|
||||
"ejs": "~2.6.1",
|
||||
"electron-debug": "^1.5.0",
|
||||
"electron-dl": "^1.12.0",
|
||||
"electron-in-page-search": "^1.3.2",
|
||||
"express": "~4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"fs-extra": "^6.0.1",
|
||||
"helmet": "^3.12.1",
|
||||
"html": "^1.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"imagemin": "^5.3.1",
|
||||
"imagemin-giflossy": "^5.1.10",
|
||||
"imagemin-mozjpeg": "^7.0.0",
|
||||
"imagemin-pngquant": "^5.1.0",
|
||||
"ini": "^1.3.5",
|
||||
"jimp": "^0.2.28",
|
||||
"moment": "^2.22.1",
|
||||
"multer": "^1.3.0",
|
||||
"async-mutex": "0.1.3",
|
||||
"axios": "0.18",
|
||||
"body-parser": "1.18.3",
|
||||
"cls-hooked": "4.2.2",
|
||||
"cookie-parser": "1.4.3",
|
||||
"debug": "3.1.0",
|
||||
"devtron": "1.4.0",
|
||||
"ejs": "2.6.1",
|
||||
"electron-debug": "2.0.0",
|
||||
"electron-dl": "1.12.0",
|
||||
"electron-in-page-search": "1.3.2",
|
||||
"express": "4.16.3",
|
||||
"express-session": "1.15.6",
|
||||
"fs-extra": "7.0.0",
|
||||
"get-port": "4.0.0",
|
||||
"helmet": "3.13.0",
|
||||
"html": "1.0.0",
|
||||
"image-type": "3.0.0",
|
||||
"imagemin": "6.0.0",
|
||||
"imagemin-giflossy": "5.1.10",
|
||||
"imagemin-mozjpeg": "7.0.0",
|
||||
"imagemin-pngquant": "6.0.0",
|
||||
"ini": "1.3.5",
|
||||
"jimp": "0.3.5",
|
||||
"moment": "2.22.2",
|
||||
"multer": "1.3.1",
|
||||
"open": "0.0.5",
|
||||
"rand-token": "^0.4.0",
|
||||
"rcedit": "^1.1.0",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"sanitize-filename": "^1.6.1",
|
||||
"scrypt": "^6.0.3",
|
||||
"serve-favicon": "~2.5.0",
|
||||
"session-file-store": "^1.2.0",
|
||||
"simple-node-logger": "^0.93.37",
|
||||
"sqlite": "^2.9.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"unescape": "^1.0.1",
|
||||
"ws": "^5.2.0",
|
||||
"xml2js": "^0.4.19"
|
||||
"rand-token": "0.4.0",
|
||||
"rcedit": "1.1.0",
|
||||
"request": "2.88.0",
|
||||
"request-promise": "4.2.2",
|
||||
"rimraf": "2.6.2",
|
||||
"sanitize-filename": "1.6.1",
|
||||
"scrypt": "6.0.3",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.2.0",
|
||||
"simple-node-logger": "0.93.37",
|
||||
"sqlite": "3.0.0",
|
||||
"tar-stream": "1.6.1",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "6.0.0",
|
||||
"xml2js": "0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^2.0.1",
|
||||
"electron-compile": "^6.4.2",
|
||||
"electron-packager": "^12.1.0",
|
||||
"electron-prebuilt-compile": "2.0.0",
|
||||
"electron-rebuild": "^1.7.3",
|
||||
"lorem-ipsum": "^1.0.4",
|
||||
"tape": "^4.9.0",
|
||||
"xo": "^0.21.1"
|
||||
"electron": "2.0.7",
|
||||
"electron-compile": "6.4.3",
|
||||
"electron-packager": "12.1.1",
|
||||
"electron-prebuilt-compile": "2.0.7",
|
||||
"electron-rebuild": "1.8.2",
|
||||
"lorem-ipsum": "1.0.5",
|
||||
"tape": "4.9.1",
|
||||
"xo": "0.22.0",
|
||||
"pkg": "4.3.4"
|
||||
},
|
||||
"config": {
|
||||
"forge": {
|
||||
@@ -109,5 +117,15 @@
|
||||
"node",
|
||||
"browser"
|
||||
]
|
||||
},
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"./db/**/*",
|
||||
"./src/public/**/*",
|
||||
"./src/views/**/*",
|
||||
"./node_modules/mozjpeg/vendor/*",
|
||||
"./node_modules/pngquant-bin/vendor/*",
|
||||
"./node_modules/giflossy/vendor/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const os = require('os');
|
||||
const sessionSecret = require('./services/session_secret');
|
||||
const cls = require('./services/cls');
|
||||
require('./entities/entity_constructor');
|
||||
require('./services/handlers');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -47,7 +48,7 @@ const sessionParser = session({
|
||||
cookie: {
|
||||
// path: "/",
|
||||
httpOnly: true,
|
||||
maxAge: 1800000
|
||||
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
|
||||
},
|
||||
store: new FileStore({
|
||||
ttl: 30 * 24 * 3600,
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
const Entity = require('./entity');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
/**
|
||||
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
|
||||
*
|
||||
* @param {string} apiTokenId - primary key
|
||||
* @param {string} token
|
||||
* @param {boolean} isDeleted - true if API token is deleted
|
||||
* @param {string} dateCreated
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class ApiToken extends Entity {
|
||||
static get tableName() { return "api_tokens"; }
|
||||
static get entityName() { return "api_tokens"; }
|
||||
static get primaryKeyName() { return "apiTokenId"; }
|
||||
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -18,6 +26,8 @@ class ApiToken extends Entity {
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
93
src/entities/attribute.js
Normal file
93
src/entities/attribute.js
Normal file
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
/**
|
||||
* Attribute is key value pair owned by a note.
|
||||
*
|
||||
* @param {string} attributeId
|
||||
* @param {string} noteId
|
||||
* @param {string} type
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {int} position
|
||||
* @param {boolean} isInheritable
|
||||
* @param {boolean} isDeleted
|
||||
* @param {string} dateCreated
|
||||
* @param {string} dateModified
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class Attribute extends Entity {
|
||||
static get entityName() { return "attributes"; }
|
||||
static get primaryKeyName() { return "attributeId"; }
|
||||
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "dateCreated"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isInheritable = !!this.isInheritable;
|
||||
|
||||
if (this.isDefinition()) {
|
||||
try {
|
||||
this.value = JSON.parse(this.value);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async getTargetNote() {
|
||||
if (this.type !== 'relation') {
|
||||
throw new Error(`Attribute ${this.attributeId} is not relation`);
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.value]);
|
||||
}
|
||||
|
||||
isDefinition() {
|
||||
return this.type === 'label-definition' || this.type === 'relation-definition';
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
if (!this.value) {
|
||||
// null value isn't allowed
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
if (!this.isInheritable) {
|
||||
this.isInheritable = false;
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Attribute;
|
||||
@@ -5,19 +5,33 @@ const dateUtils = require('../services/date_utils');
|
||||
const repository = require('../services/repository');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
/**
|
||||
* Branch represents note's placement in the tree - it's essentially pair of noteId and parentNoteId.
|
||||
* Each note can have multiple (at least one) branches, meaning it can be placed into multiple places in the tree.
|
||||
*
|
||||
* @param {string} branchId - primary key
|
||||
* @param {string} noteId
|
||||
* @param {string} parentNoteId
|
||||
* @param {int} notePosition
|
||||
* @param {string} prefix
|
||||
* @param {boolean} isExpanded
|
||||
* @param {boolean} isDeleted
|
||||
* @param {string} dateModified
|
||||
* @param {string} dateCreated
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class Branch extends Entity {
|
||||
static get tableName() { return "branches"; }
|
||||
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", "dateModified", "isDeleted", "prefix"]; }
|
||||
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.notePosition === undefined) {
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
|
||||
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
@@ -31,7 +45,11 @@ class Branch extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const utils = require('../services/utils');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
class Entity {
|
||||
/**
|
||||
* @param {object} [row] - database row representing given entity
|
||||
*/
|
||||
constructor(row = {}) {
|
||||
for (const key in row) {
|
||||
this[key] = row[key];
|
||||
}
|
||||
|
||||
if ('isDeleted' in this) {
|
||||
this.isDeleted = !!this.isDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
@@ -15,20 +21,25 @@ class Entity {
|
||||
this[this.constructor.primaryKeyName] = utils.newEntityId();
|
||||
}
|
||||
|
||||
const origHash = this.hash;
|
||||
|
||||
this.hash = this.generateHash();
|
||||
|
||||
this.isChanged = origHash !== this.hash;
|
||||
}
|
||||
|
||||
generateHash() {
|
||||
let contentToHash = "";
|
||||
|
||||
for (const propertyName of this.constructor.hashedProperties) {
|
||||
contentToHash += "|" + this[propertyName];
|
||||
}
|
||||
|
||||
// this IF is to ease the migration from before hashed options, can be later removed
|
||||
if (this.constructor.tableName !== 'options' || this.isSynced) {
|
||||
this["hash"] = utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
return utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
|
||||
async save() {
|
||||
await repository.updateEntity(this);
|
||||
await require('../services/repository').updateEntity(this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,37 @@ const NoteRevision = require('../entities/note_revision');
|
||||
const Image = require('../entities/image');
|
||||
const NoteImage = require('../entities/note_image');
|
||||
const Branch = require('../entities/branch');
|
||||
const Label = require('../entities/label');
|
||||
const Attribute = require('../entities/attribute');
|
||||
const RecentNote = require('../entities/recent_note');
|
||||
const ApiToken = require('../entities/api_token');
|
||||
const Option = require('../entities/option');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
const ENTITY_NAME_TO_ENTITY = {
|
||||
"attributes": Attribute,
|
||||
"images": Image,
|
||||
"note_images": NoteImage,
|
||||
"branches": Branch,
|
||||
"notes": Note,
|
||||
"note_revisions": NoteRevision,
|
||||
"recent_notes": RecentNote,
|
||||
"options": Option,
|
||||
"api_tokens": ApiToken
|
||||
};
|
||||
|
||||
function getEntityFromEntityName(entityName) {
|
||||
if (!(entityName in ENTITY_NAME_TO_ENTITY)) {
|
||||
throw new Error(`Entity for table ${entityName} not found!`);
|
||||
}
|
||||
|
||||
return ENTITY_NAME_TO_ENTITY[entityName];
|
||||
}
|
||||
|
||||
function createEntityFromRow(row) {
|
||||
let entity;
|
||||
|
||||
if (row.labelId) {
|
||||
entity = new Label(row);
|
||||
if (row.attributeId) {
|
||||
entity = new Attribute(row);
|
||||
}
|
||||
else if (row.noteRevisionId) {
|
||||
entity = new NoteRevision(row);
|
||||
@@ -46,8 +66,9 @@ function createEntityFromRow(row) {
|
||||
return entity;
|
||||
}
|
||||
|
||||
repository.setEntityConstructor(createEntityFromRow);
|
||||
|
||||
module.exports = {
|
||||
createEntityFromRow
|
||||
};
|
||||
createEntityFromRow,
|
||||
getEntityFromEntityName
|
||||
};
|
||||
|
||||
repository.setEntityConstructor(module.exports);
|
||||
|
||||
@@ -3,14 +3,26 @@
|
||||
const Entity = require('./entity');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
/**
|
||||
* This class represents image data.
|
||||
*
|
||||
* @param {string} imageId
|
||||
* @param {string} format
|
||||
* @param {string} checksum
|
||||
* @param {string} name
|
||||
* @param {blob} data
|
||||
* @param {boolean} isDeleted
|
||||
* @param {string} dateModified
|
||||
* @param {string} dateCreated
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class Image extends Entity {
|
||||
static get tableName() { return "images"; }
|
||||
static get entityName() { return "images"; }
|
||||
static get primaryKeyName() { return "imageId"; }
|
||||
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
|
||||
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateCreated"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -19,7 +31,11 @@ class Image extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
class Label extends Entity {
|
||||
static get tableName() { return "labels"; }
|
||||
static get primaryKeyName() { return "labelId"; }
|
||||
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.value) {
|
||||
// null value isn't allowed
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM labels WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Label;
|
||||
@@ -1,18 +1,42 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const Attribute = require('./attribute');
|
||||
const protectedSessionService = require('../services/protected_session');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
class Note extends Entity {
|
||||
static get tableName() { return "notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
|
||||
/**
|
||||
* This represents a Note which is a central object in the Trilium Notes project.
|
||||
*
|
||||
* @property {string} noteId - primary key
|
||||
* @property {string} type - one of "text", "code", "file" or "render"
|
||||
* @property {string} mime - MIME type, e.g. "text/html"
|
||||
* @property {string} title - note title
|
||||
* @property {string} content - note content - e.g. HTML text for text notes, file payload for files
|
||||
* @property {boolean} isProtected - true if note is protected
|
||||
* @property {boolean} isDeleted - true if note is deleted
|
||||
* @property {string} dateCreated
|
||||
* @property {string} dateModified
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class Note extends Entity {
|
||||
static get entityName() { return "notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "title", "content", "type", "isProtected", "isDeleted"]; }
|
||||
|
||||
/**
|
||||
* @param row - object containing database row from "notes" table
|
||||
*/
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isProtected = !!this.isProtected;
|
||||
|
||||
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
|
||||
if (this.isProtected && this.noteId) {
|
||||
protectedSessionService.decryptNote(this);
|
||||
@@ -30,19 +54,28 @@ class Note extends Entity {
|
||||
catch(e) {}
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
|
||||
isRoot() {
|
||||
return this.noteId === 'root';
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is of application/json content type */
|
||||
isJson() {
|
||||
return this.mime === "application/json";
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
|
||||
isJavaScript() {
|
||||
return (this.type === "code" || this.type === "file")
|
||||
&& (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is HTML */
|
||||
isHtml() {
|
||||
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
|
||||
}
|
||||
|
||||
/** @returns {string} JS script environment - either "frontend" or "backend" */
|
||||
getScriptEnv() {
|
||||
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
|
||||
return "frontend";
|
||||
@@ -59,55 +92,385 @@ class Note extends Entity {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getLabels() {
|
||||
return await repository.getEntities("SELECT * FROM labels WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
|
||||
/**
|
||||
* @returns {Promise<Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
|
||||
*/
|
||||
async getOwnedAttributes() {
|
||||
return await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
|
||||
async getLabelMap() {
|
||||
const map = {};
|
||||
|
||||
for (const label of await this.getLabels()) {
|
||||
map[label.name] = label.value;
|
||||
/** @returns {Promise<Attribute[]>} all note's attributes, including inherited ones */
|
||||
async getAttributes() {
|
||||
if (!this.__attributeCache) {
|
||||
await this.loadAttributesToCache();
|
||||
}
|
||||
|
||||
return map;
|
||||
return this.__attributeCache;
|
||||
}
|
||||
|
||||
async hasLabel(name) {
|
||||
const map = await this.getLabelMap();
|
||||
|
||||
return map.hasOwnProperty(name);
|
||||
/** @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones */
|
||||
async getLabels() {
|
||||
return (await this.getAttributes()).filter(attr => attr.type === LABEL);
|
||||
}
|
||||
|
||||
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
|
||||
async getLabel(name) {
|
||||
return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]);
|
||||
/** @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones */
|
||||
async getRelations() {
|
||||
return (await this.getAttributes()).filter(attr => attr.type === RELATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear note's attributes cache to force fresh reload for next attribute request.
|
||||
* Cache is note instance scoped.
|
||||
*/
|
||||
invalidateAttributeCache() {
|
||||
this.__attributeCache = null;
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
async loadAttributesToCache() {
|
||||
const attributes = await repository.getEntities(`
|
||||
WITH RECURSIVE
|
||||
tree(noteId, level) AS (
|
||||
SELECT ?, 0
|
||||
UNION
|
||||
SELECT branches.parentNoteId, tree.level + 1 FROM branches
|
||||
JOIN tree ON branches.noteId = tree.noteId
|
||||
JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
),
|
||||
treeWithAttrs(noteId, level) AS (
|
||||
SELECT * FROM tree
|
||||
UNION
|
||||
SELECT attributes.value, treeWithAttrs.level + 1 FROM attributes
|
||||
JOIN treeWithAttrs ON treeWithAttrs.noteId = attributes.noteId
|
||||
WHERE attributes.isDeleted = 0
|
||||
AND attributes.type = 'relation'
|
||||
AND attributes.name = 'template'
|
||||
AND (attributes.noteId = ? OR attributes.isInheritable = 1)
|
||||
)
|
||||
SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId
|
||||
WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?)
|
||||
ORDER BY level, noteId, position`, [this.noteId, this.noteId, this.noteId]);
|
||||
// attributes are ordered so that "closest" attributes are first
|
||||
// we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
|
||||
|
||||
const filteredAttributes = attributes.filter((attr, index) => {
|
||||
if (attr.isDefinition()) {
|
||||
const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
|
||||
|
||||
// keep only if this element is the first definition for this type & name
|
||||
return firstDefinitionIndex === index;
|
||||
}
|
||||
else {
|
||||
const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name);
|
||||
|
||||
if (!definitionAttr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const definition = definitionAttr.value;
|
||||
|
||||
if (definition.multiplicityType === 'multivalue') {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
|
||||
|
||||
// in case of single-valued attribute we'll keep it only if it's first (closest)
|
||||
return firstAttrIndex === index;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const attr of filteredAttributes) {
|
||||
attr.isOwned = attr.noteId === this.noteId;
|
||||
}
|
||||
|
||||
this.__attributeCache = filteredAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {Promise<boolean>} true if note has an attribute with given type and name (including inherited)
|
||||
*/
|
||||
async hasAttribute(type, name) {
|
||||
return !!await this.getAttribute(type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {Promise<Attribute>} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
|
||||
*/
|
||||
async getAttribute(type, name) {
|
||||
const attributes = await this.getAttributes();
|
||||
|
||||
return attributes.find(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {Promise<string>} attribute value of given type and name or null if no such attribute exists.
|
||||
*/
|
||||
async getAttributeValue(type, name) {
|
||||
const attr = await this.getAttribute(type, name);
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on enabled, attribute is either set or removed.
|
||||
*
|
||||
* @param {string} type - attribute type ('relation', 'label' etc.)
|
||||
* @param {boolean} enabled - toggle On or Off
|
||||
* @param {string} name - attribute name
|
||||
* @param {string} [value] - attribute value (optional)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async toggleAttribute(type, enabled, name, value) {
|
||||
if (enabled) {
|
||||
await this.setAttribute(type, name, value);
|
||||
}
|
||||
else {
|
||||
await this.removeAttribute(type, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates given attribute name-value pair if it doesn't exist.
|
||||
*
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @param {string} [value] - attribute value (optional)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setAttribute(type, name, value) {
|
||||
const attributes = await this.getOwnedAttributes();
|
||||
let attr = attributes.find(attr => attr.type === type && (value === undefined || attr.value === value));
|
||||
|
||||
if (!attr) {
|
||||
attr = new Attribute({
|
||||
noteId: this.noteId,
|
||||
type: type,
|
||||
name: name,
|
||||
value: value !== undefined ? value : ""
|
||||
});
|
||||
|
||||
await attr.save();
|
||||
|
||||
this.invalidateAttributeCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes given attribute name-value pair if it exists.
|
||||
*
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @param {string} [value] - attribute value (optional)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async removeAttribute(type, name, value) {
|
||||
const attributes = await this.getOwnedAttributes();
|
||||
|
||||
for (const attribute of attributes) {
|
||||
if (attribute.type === type && (value === undefined || value === attribute.value)) {
|
||||
attribute.isDeleted = true;
|
||||
await attribute.save();
|
||||
|
||||
this.invalidateAttributeCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {Promise<boolean>} true if label exists (including inherited)
|
||||
*/
|
||||
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {Promise<boolean>} true if relation exists (including inherited)
|
||||
*/
|
||||
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {Promise<Attribute>} label if it exists, null otherwise
|
||||
*/
|
||||
async getLabel(name) { return await this.getAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {Promise<Attribute>} relation if it exists, null otherwise
|
||||
*/
|
||||
async getRelation(name) { return await this.getAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {Promise<string>} label value if label exists, null otherwise
|
||||
*/
|
||||
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {Promise<string>} relation value if relation exists, null otherwise
|
||||
*/
|
||||
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
|
||||
|
||||
/**
|
||||
* Based on enabled, label is either set or removed.
|
||||
*
|
||||
* @param {boolean} enabled - toggle On or Off
|
||||
* @param {string} name - label name
|
||||
* @param {string} [value] - label value (optional)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async toggleLabel(enabled, name, value) { return await this.toggleAttribute(LABEL, enabled, name, value); }
|
||||
|
||||
/**
|
||||
* Based on enabled, relation is either set or removed.
|
||||
*
|
||||
* @param {boolean} enabled - toggle On or Off
|
||||
* @param {string} name - relation name
|
||||
* @param {string} [value] - relation value (noteId)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async toggleRelation(enabled, name, value) { return await this.toggleAttribute(RELATION, enabled, name, value); }
|
||||
|
||||
/**
|
||||
* Create label name-value pair if it doesn't exist yet.
|
||||
*
|
||||
* @param {string} name - label name
|
||||
* @param {string} [value] - label value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); }
|
||||
|
||||
/**
|
||||
* Create relation name-value pair if it doesn't exist yet.
|
||||
*
|
||||
* @param {string} name - relation name
|
||||
* @param {string} [value] - relation value (noteId)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setRelation(name, value) { return await this.setAttribute(RELATION, name, value); }
|
||||
|
||||
/**
|
||||
* Remove label name-value pair, if it exists.
|
||||
*
|
||||
* @param {string} name - label name
|
||||
* @param {string} [value] - label value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async removeLabel(name, value) { return await this.removeAttribute(LABEL, name, value); }
|
||||
|
||||
/**
|
||||
* Remove relation name-value pair, if it exists.
|
||||
*
|
||||
* @param {string} name - relation name
|
||||
* @param {string} [value] - relation value (noteId)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async removeRelation(name, value) { return await this.removeAttribute(RELATION, name, value); }
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
|
||||
*/
|
||||
async getRelationTarget(name) {
|
||||
const relation = await this.getRelation(name);
|
||||
|
||||
return relation ? await repository.getNote(relation.value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds notes with given attribute name and value. Only own attributes are considered, not inherited ones
|
||||
*
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @param {string} [value] - attribute value
|
||||
* @returns {Promise<Note[]>}
|
||||
*/
|
||||
async findNotesWithAttribute(type, name, value) {
|
||||
const params = [this.noteId, name];
|
||||
let valueCondition = "";
|
||||
|
||||
if (value !== undefined) {
|
||||
params.push(value);
|
||||
valueCondition = " AND attributes.value = ?";
|
||||
}
|
||||
|
||||
const notes = await repository.getEntities(`
|
||||
WITH RECURSIVE
|
||||
tree(noteId) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT branches.noteId FROM branches
|
||||
JOIN tree ON branches.parentNoteId = tree.noteId
|
||||
JOIN notes ON notes.noteId = branches.noteId
|
||||
WHERE notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
)
|
||||
SELECT notes.* FROM notes
|
||||
JOIN tree ON tree.noteId = notes.noteId
|
||||
JOIN attributes ON attributes.noteId = notes.noteId
|
||||
WHERE attributes.isDeleted = 0
|
||||
AND attributes.name = ?
|
||||
${valueCondition}
|
||||
ORDER BY noteId, position`, params);
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds notes with given label name and value. Only own labels are considered, not inherited ones
|
||||
*
|
||||
* @param {string} name - label name
|
||||
* @param {string} [value] - label value
|
||||
* @returns {Promise<Note[]>}
|
||||
*/
|
||||
async findNotesWithLabel(name, value) { return await this.findNotesWithAttribute(LABEL, name, value); }
|
||||
|
||||
/**
|
||||
* Finds notes with given relation name and value. Only own relations are considered, not inherited ones
|
||||
*
|
||||
* @param {string} name - relation name
|
||||
* @param {string} [value] - relation value
|
||||
* @returns {Promise<Note[]>}
|
||||
*/
|
||||
async findNotesWithRelation(name, value) { return await this.findNotesWithAttribute(RELATION, name, value); }
|
||||
|
||||
/**
|
||||
* Returns note revisions of this note.
|
||||
*
|
||||
* @returns {Promise<NoteRevision[]>}
|
||||
*/
|
||||
async getRevisions() {
|
||||
return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<NoteImage[]>}
|
||||
*/
|
||||
async getNoteImages() {
|
||||
return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Branch[]>}
|
||||
*/
|
||||
async getBranches() {
|
||||
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async getChildNote(name) {
|
||||
return await repository.getEntity(`
|
||||
SELECT notes.*
|
||||
FROM branches
|
||||
JOIN notes USING(noteId)
|
||||
WHERE notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
AND branches.parentNoteId = ?
|
||||
AND notes.title = ?`, [this.noteId, name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Note[]>} child notes of this note
|
||||
*/
|
||||
async getChildNotes() {
|
||||
return await repository.getEntities(`
|
||||
SELECT notes.*
|
||||
@@ -119,6 +482,9 @@ class Note extends Entity {
|
||||
ORDER BY branches.notePosition`, [this.noteId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Branch[]>} child branches of this note
|
||||
*/
|
||||
async getChildBranches() {
|
||||
return await repository.getEntities(`
|
||||
SELECT branches.*
|
||||
@@ -128,6 +494,9 @@ class Note extends Entity {
|
||||
ORDER BY branches.notePosition`, [this.noteId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning)
|
||||
*/
|
||||
async getParentNotes() {
|
||||
return await repository.getEntities(`
|
||||
SELECT parent_notes.*
|
||||
@@ -140,8 +509,6 @@ class Note extends Entity {
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isJson() && this.jsonContent) {
|
||||
this.content = JSON.stringify(this.jsonContent, null, '\t');
|
||||
}
|
||||
@@ -158,7 +525,11 @@ class Note extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,22 @@ const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
/**
|
||||
* This class represents image's placement in the note(s). One image may be placed into several notes.
|
||||
*
|
||||
* @param {string} noteImageId
|
||||
* @param {string} noteId
|
||||
* @param {string} imageId
|
||||
* @param {boolean} isDeleted
|
||||
* @param {string} dateModified
|
||||
* @param {string} dateCreated
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class NoteImage extends Entity {
|
||||
static get tableName() { return "note_images"; }
|
||||
static get entityName() { return "note_images"; }
|
||||
static get primaryKeyName() { return "noteImageId"; }
|
||||
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
|
||||
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateCreated"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
@@ -18,8 +30,6 @@ class NoteImage extends Entity {
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -28,7 +38,11 @@ class NoteImage extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,31 @@ const Entity = require('./entity');
|
||||
const protectedSessionService = require('../services/protected_session');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
/**
|
||||
* NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
|
||||
*
|
||||
* @param {string} noteRevisionId
|
||||
* @param {string} noteId
|
||||
* @param {string} type
|
||||
* @param {string} mime
|
||||
* @param {string} title
|
||||
* @param {string} content
|
||||
* @param {string} isProtected
|
||||
* @param {string} dateModifiedFrom
|
||||
* @param {string} dateModifiedTo
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class NoteRevision extends Entity {
|
||||
static get tableName() { return "note_revisions"; }
|
||||
static get entityName() { return "note_revisions"; }
|
||||
static get primaryKeyName() { return "noteRevisionId"; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isProtected = !!this.isProtected;
|
||||
|
||||
if (this.isProtected) {
|
||||
protectedSessionService.decryptNoteRevision(this);
|
||||
}
|
||||
@@ -22,11 +39,11 @@ class NoteRevision extends Entity {
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isProtected) {
|
||||
protectedSessionService.encryptNoteRevision(this);
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,34 @@
|
||||
const Entity = require('./entity');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
/**
|
||||
* Option represents name-value pair, either directly configurable by the user or some system property.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {boolean} isSynced
|
||||
* @param {string} dateModified
|
||||
* @param {string} dateCreated
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class Option extends Entity {
|
||||
static get tableName() { return "options"; }
|
||||
static get primaryKeyName() { return "optionId"; }
|
||||
static get hashedProperties() { return ["optionId", "name", "value"]; }
|
||||
static get entityName() { return "options"; }
|
||||
static get primaryKeyName() { return "name"; }
|
||||
static get hashedProperties() { return ["name", "value"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isSynced = !!this.isSynced;
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
const Entity = require('./entity');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
/**
|
||||
* RecentNote represents recently visited note.
|
||||
*
|
||||
* @param {string} branchId
|
||||
* @param {string} notePath
|
||||
* @param {boolean} isDeleted
|
||||
* @param {string} dateModified
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class RecentNote extends Entity {
|
||||
static get tableName() { return "recent_notes"; }
|
||||
static get entityName() { return "recent_notes"; }
|
||||
static get primaryKeyName() { return "branchId"; }
|
||||
static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -18,6 +26,8 @@ class RecentNote extends Entity {
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
src/public/images/icons/clock-16.png
Normal file
BIN
src/public/images/icons/clock-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 B |
1
src/public/images/shield.svg
Normal file
1
src/public/images/shield.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fafafa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
|
After Width: | Height: | Size: 274 B |
@@ -2,8 +2,8 @@ import cloningService from '../services/cloning.js';
|
||||
import linkService from '../services/link.js';
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import treeUtils from '../services/tree_utils.js';
|
||||
import server from "../services/server.js";
|
||||
import noteDetailText from "../services/note_detail_text.js";
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
|
||||
const $dialog = $("#add-link-dialog");
|
||||
const $form = $("#add-link-form");
|
||||
@@ -15,6 +15,7 @@ const $prefixFormGroup = $("#add-link-prefix-form-group");
|
||||
const $linkTypeDiv = $("#add-link-type-div");
|
||||
const $linkTypes = $("input[name='add-link-type']");
|
||||
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
|
||||
const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
|
||||
|
||||
function setLinkType(linkType) {
|
||||
$linkTypes.each(function () {
|
||||
@@ -53,24 +54,16 @@ async function showDialog() {
|
||||
$linkTitle.val(noteTitle);
|
||||
}
|
||||
|
||||
$autoComplete.autocomplete({
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
await $autoComplete.autocomplete({
|
||||
source: noteAutocompleteService.autocompleteSource,
|
||||
minLength: 0,
|
||||
change: async (event, ui) => {
|
||||
if (!ui.item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notePath = linkService.getNotePathFromLabel(ui.item.value);
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 2,
|
||||
change: async () => {
|
||||
const val = $autoComplete.val();
|
||||
const notePath = linkService.getNodePathFromLabel(val);
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
@@ -81,21 +74,30 @@ async function showDialog() {
|
||||
await setDefaultLinkTitle(noteId);
|
||||
}
|
||||
},
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// this is called when user goes through autocomplete list with keyboard
|
||||
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
|
||||
focus: async (event, ui) => {
|
||||
const notePath = linkService.getNodePathFromLabel(ui.item.value);
|
||||
const notePath = linkService.getNotePathFromLabel(ui.item.value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
await setDefaultLinkTitle(noteId);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
showRecentNotes();
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
const value = $autoComplete.val();
|
||||
|
||||
const notePath = linkService.getNodePathFromLabel(value);
|
||||
const notePath = linkService.getNotePathFromLabel(value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
if (notePath) {
|
||||
@@ -153,8 +155,14 @@ function linkTypeChanged() {
|
||||
$linkTypeDiv.toggle(!hasSelection());
|
||||
}
|
||||
|
||||
function showRecentNotes() {
|
||||
$autoComplete.autocomplete("search", "");
|
||||
}
|
||||
|
||||
$linkTypes.change(linkTypeChanged);
|
||||
|
||||
$showRecentNotesButton.click(showRecentNotes);
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
302
src/public/javascripts/dialogs/attributes.js
Normal file
302
src/public/javascripts/dialogs/attributes.js
Normal file
@@ -0,0 +1,302 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
import linkService from "../services/link.js";
|
||||
|
||||
const $dialog = $("#attributes-dialog");
|
||||
const $saveAttributesButton = $("#save-attributes-button");
|
||||
const $ownedAttributesBody = $('#owned-attributes-table tbody');
|
||||
|
||||
const attributesModel = new AttributesModel();
|
||||
|
||||
function AttributesModel() {
|
||||
const self = this;
|
||||
|
||||
this.ownedAttributes = ko.observableArray();
|
||||
this.inheritedAttributes = ko.observableArray();
|
||||
|
||||
this.availableTypes = [
|
||||
{ text: "Label", value: "label" },
|
||||
{ text: "Label definition", value: "label-definition" },
|
||||
{ text: "Relation", value: "relation" },
|
||||
{ text: "Relation definition", value: "relation-definition" }
|
||||
];
|
||||
|
||||
this.availableLabelTypes = [
|
||||
{ text: "Text", value: "text" },
|
||||
{ text: "Number", value: "number" },
|
||||
{ text: "Boolean", value: "boolean" },
|
||||
{ text: "Date", value: "date" },
|
||||
{ text: "URL", value: "url"}
|
||||
];
|
||||
|
||||
this.multiplicityTypes = [
|
||||
{ text: "Single value", value: "singlevalue" },
|
||||
{ text: "Multi value", value: "multivalue" }
|
||||
];
|
||||
|
||||
this.typeChanged = function(data, event) {
|
||||
self.getTargetAttribute(event.target).valueHasMutated();
|
||||
};
|
||||
|
||||
this.labelTypeChanged = function(data, event) {
|
||||
self.getTargetAttribute(event.target).valueHasMutated();
|
||||
};
|
||||
|
||||
this.updateAttributePositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// attributes in the viewmodel (self.ownedAttributes()) stays the same
|
||||
$ownedAttributesBody.find('input[name="position"]').each(function() {
|
||||
const attribute = self.getTargetAttribute(this);
|
||||
|
||||
attribute().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
async function showAttributes(attributes) {
|
||||
const ownedAttributes = attributes.filter(attr => attr.isOwned);
|
||||
|
||||
for (const attr of ownedAttributes) {
|
||||
attr.labelValue = attr.type === 'label' ? attr.value : '';
|
||||
attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value) + " (" + attr.value + ")") : '';
|
||||
attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : {
|
||||
labelType: "text",
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
};
|
||||
|
||||
attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
};
|
||||
|
||||
delete attr.value;
|
||||
}
|
||||
|
||||
self.ownedAttributes(ownedAttributes.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
const inheritedAttributes = attributes.filter(attr => !attr.isOwned);
|
||||
|
||||
self.inheritedAttributes(inheritedAttributes);
|
||||
}
|
||||
|
||||
this.loadAttributes = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
await showAttributes(attributes);
|
||||
|
||||
// attribute might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".attribute-type-select:last").focus(), 100);
|
||||
|
||||
$ownedAttributesBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $ownedAttributesBody,
|
||||
update: this.updateAttributePositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteAttribute = function(data, event) {
|
||||
const attribute = self.getTargetAttribute(event.target);
|
||||
const attributeData = attribute();
|
||||
|
||||
if (attributeData) {
|
||||
attributeData.isDeleted = true;
|
||||
|
||||
attribute(attributeData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveAttributesButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateAttributePositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const attributesToSave = self.ownedAttributes()
|
||||
.map(attribute => attribute())
|
||||
.filter(attribute => attribute.attributeId !== "" || attribute.name !== "");
|
||||
|
||||
for (const attr of attributesToSave) {
|
||||
if (attr.type === 'label') {
|
||||
attr.value = attr.labelValue;
|
||||
}
|
||||
else if (attr.type === 'relation') {
|
||||
attr.value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(attr.relationValue));
|
||||
}
|
||||
else if (attr.type === 'label-definition') {
|
||||
attr.value = attr.labelDefinition;
|
||||
}
|
||||
else if (attr.type === 'relation-definition') {
|
||||
attr.value = attr.relationDefinition;
|
||||
}
|
||||
|
||||
delete attr.labelValue;
|
||||
delete attr.relationValue;
|
||||
delete attr.labelDefinition;
|
||||
delete attr.relationDefinition;
|
||||
}
|
||||
|
||||
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
|
||||
|
||||
await showAttributes(attributes);
|
||||
|
||||
infoService.showMessage("Attributes have been saved.");
|
||||
|
||||
noteDetailService.loadAttributes();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const attributes = self.ownedAttributes().filter(attr => !attr().isDeleted);
|
||||
const last = attributes.length === 0 ? null : attributes[attributes.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "") {
|
||||
self.ownedAttributes.push(ko.observable({
|
||||
attributeId: '',
|
||||
type: 'label',
|
||||
name: '',
|
||||
labelValue: '',
|
||||
relationValue: '',
|
||||
isInheritable: false,
|
||||
isDeleted: false,
|
||||
position: 0,
|
||||
labelDefinition: {
|
||||
labelType: "text",
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
},
|
||||
relationDefinition: {
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.attributeChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const attribute = self.getTargetAttribute(event.target);
|
||||
|
||||
attribute.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.ownedAttributes()[index]();
|
||||
|
||||
return cur.name.trim() === "" && !cur.isDeleted && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue);
|
||||
};
|
||||
|
||||
this.getTargetAttribute = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.ownedAttributes()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await attributesModel.loadAttributes();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 950,
|
||||
height: 700
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(attributesModel, $dialog[0]);
|
||||
|
||||
$dialog.on('focus', '.attribute-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
source: async (request, response) => {
|
||||
const attribute = attributesModel.getTargetAttribute(this);
|
||||
const type = (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label';
|
||||
const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(request.term));
|
||||
const result = names.map(name => {
|
||||
return {
|
||||
label: name,
|
||||
value: name
|
||||
}
|
||||
});
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$dialog.on('focus', '.label-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const attributeName = $(this).parent().parent().find('.attribute-name').val();
|
||||
|
||||
if (attributeName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
|
||||
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: attributeValues.map(attribute => {
|
||||
return {
|
||||
attribute: attribute,
|
||||
value: attribute
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -15,7 +15,7 @@ async function showDialog() {
|
||||
|
||||
await $dialog.dialog({
|
||||
modal: true,
|
||||
width: 500
|
||||
width: 600
|
||||
});
|
||||
|
||||
const currentNode = treeService.getCurrentNode();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import linkService from '../services/link.js';
|
||||
import server from '../services/server.js';
|
||||
import searchNotesService from '../services/search_notes.js';
|
||||
import noteautocompleteService from '../services/note_autocomplete.js';
|
||||
import linkService from "../services/link.js";
|
||||
|
||||
const $dialog = $("#jump-to-note-dialog");
|
||||
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||
const $form = $("#jump-to-note-form");
|
||||
const $jumpToNoteButton = $("#jump-to-note-button");
|
||||
const $showInFullTextButton = $("#show-in-full-text-button");
|
||||
const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
@@ -16,43 +15,29 @@ async function showDialog() {
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800
|
||||
width: 800,
|
||||
position: { my: "center top+100", at: "top", of: window }
|
||||
});
|
||||
|
||||
await $autoComplete.autocomplete({
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
source: noteautocompleteService.autocompleteSource,
|
||||
focus: event => event.preventDefault(),
|
||||
minLength: 0,
|
||||
autoFocus: true,
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
focus: function(event, ui) {
|
||||
return $(ui.item).val() !== 'No results';
|
||||
},
|
||||
minLength: 2
|
||||
const notePath = linkService.getNotePathFromLabel(ui.item.value);
|
||||
|
||||
treeService.activateNote(notePath);
|
||||
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedNotePath() {
|
||||
const val = $autoComplete.val();
|
||||
return linkService.getNodePathFromLabel(val);
|
||||
}
|
||||
|
||||
function goToNote() {
|
||||
const notePath = getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
treeService.activateNode(notePath);
|
||||
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
showRecentNotes();
|
||||
}
|
||||
|
||||
function showInFullText(e) {
|
||||
@@ -69,16 +54,14 @@ function showInFullText(e) {
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
goToNote();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$jumpToNoteButton.click(goToNote);
|
||||
function showRecentNotes() {
|
||||
$autoComplete.autocomplete("search", "");
|
||||
}
|
||||
|
||||
$showInFullTextButton.click(showInFullText);
|
||||
|
||||
$showRecentNotesButton.click(showRecentNotes);
|
||||
|
||||
$dialog.bind('keydown', 'ctrl+return', showInFullText);
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import utils from '../services/utils.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
|
||||
const $dialog = $("#labels-dialog");
|
||||
const $saveLabelsButton = $("#save-labels-button");
|
||||
const $labelsBody = $('#labels-table tbody');
|
||||
|
||||
const labelsModel = new LabelsModel();
|
||||
let labelNames = [];
|
||||
|
||||
function LabelsModel() {
|
||||
const self = this;
|
||||
|
||||
this.labels = ko.observableArray();
|
||||
|
||||
this.updateLabelPositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// labels in the viewmodel (self.labels()) stays the same
|
||||
$labelsBody.find('input[name="position"]').each(function() {
|
||||
const label = self.getTargetLabel(this);
|
||||
|
||||
label().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
this.loadLabels = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const labels = await server.get('notes/' + noteId + '/labels');
|
||||
|
||||
self.labels(labels.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
labelNames = await server.get('labels/names');
|
||||
|
||||
// label might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".label-name:last").focus(), 100);
|
||||
|
||||
$labelsBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $labelsBody,
|
||||
update: this.updateLabelPositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteLabel = function(data, event) {
|
||||
const label = self.getTargetLabel(event.target);
|
||||
const labelData = label();
|
||||
|
||||
if (labelData) {
|
||||
labelData.isDeleted = 1;
|
||||
|
||||
label(labelData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveLabelsButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateLabelPositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const labelsToSave = self.labels()
|
||||
.map(label => label())
|
||||
.filter(label => label.labelId !== "" || label.name !== "");
|
||||
|
||||
const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
|
||||
|
||||
self.labels(labels.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
infoService.showMessage("Labels have been saved.");
|
||||
|
||||
noteDetailService.loadLabelList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const labels = self.labels().filter(attr => attr().isDeleted === 0);
|
||||
const last = labels.length === 0 ? null : labels[labels.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "" || last.value !== "") {
|
||||
self.labels.push(ko.observable({
|
||||
labelId: '',
|
||||
name: '',
|
||||
value: '',
|
||||
isDeleted: 0,
|
||||
position: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.labelChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const label = self.getTargetLabel(event.target);
|
||||
|
||||
label.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.labels()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
|
||||
const label = labels[i]();
|
||||
|
||||
if (index !== i && cur.name === label.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.labels()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
|
||||
};
|
||||
|
||||
this.getTargetLabel = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.labels()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await labelsModel.loadLabels();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
|
||||
|
||||
$(document).on('focus', '.label-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: labelNames.map(label => {
|
||||
return {
|
||||
label: label,
|
||||
value: label
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$(document).on('focus', '.label-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const labelName = $(this).parent().parent().find('.label-name').val();
|
||||
|
||||
if (labelName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
|
||||
|
||||
if (labelValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: labelValues.map(label => {
|
||||
return {
|
||||
label: label,
|
||||
value: label
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -63,10 +63,10 @@ $list.on('change', () => {
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', "a[action='note-revision']", event => {
|
||||
$(document).on('click', "a[data-action='note-revision']", event => {
|
||||
const linkEl = $(event.target);
|
||||
const noteId = linkEl.attr('note-path');
|
||||
const noteRevisionId = linkEl.attr('note-revision-id');
|
||||
const noteId = linkEl.attr('data-note-path');
|
||||
const noteRevisionId = linkEl.attr('data-note-revision-id');
|
||||
|
||||
showNoteRevisionsDialog(noteId, noteRevisionId);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function formatNode(node, level) {
|
||||
const indentAfter = new Array(level - 1).join(' ');
|
||||
let textNode;
|
||||
|
||||
for (const i = 0; i < node.children.length; i++) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
textNode = document.createTextNode('\n' + indentBefore);
|
||||
node.insertBefore(textNode, node.children[i]);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
import protectedSessionHolder from '../services/protected_session_holder.js';
|
||||
import utils from '../services/utils.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import zoomService from "../services/zoom.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#options-dialog");
|
||||
const $tabs = $("#options-tabs");
|
||||
@@ -33,8 +34,8 @@ async function showDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOptions(optionName, optionValue) {
|
||||
await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
|
||||
async function saveOptions(options) {
|
||||
await server.put('options', options);
|
||||
|
||||
infoService.showMessage("Options change have been saved.");
|
||||
}
|
||||
@@ -44,6 +45,41 @@ export default {
|
||||
saveOptions
|
||||
};
|
||||
|
||||
addTabHandler((function() {
|
||||
const $themeSelect = $("#theme-select");
|
||||
const $zoomFactorSelect = $("#zoom-factor-select");
|
||||
const $html = $("html");
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$themeSelect.val(options.theme);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
$zoomFactorSelect.val(options.zoomFactor);
|
||||
}
|
||||
else {
|
||||
$zoomFactorSelect.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
$themeSelect.change(function() {
|
||||
const newTheme = $(this).val();
|
||||
|
||||
$html.attr("class", "theme-" + newTheme);
|
||||
|
||||
server.put('options/theme/' + newTheme);
|
||||
});
|
||||
|
||||
$zoomFactorSelect.change(function() {
|
||||
const newZoomFactor = $(this).val();
|
||||
|
||||
zoomService.setZoomFactorAndSave(newZoomFactor);
|
||||
});
|
||||
|
||||
return {
|
||||
optionsLoaded
|
||||
};
|
||||
})());
|
||||
|
||||
addTabHandler((function() {
|
||||
const $form = $("#change-password-form");
|
||||
const $oldPassword = $("#old-password");
|
||||
@@ -93,16 +129,15 @@ addTabHandler((function() {
|
||||
addTabHandler((function() {
|
||||
const $form = $("#protected-session-timeout-form");
|
||||
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
|
||||
const optionName = 'protectedSessionTimeout';
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$protectedSessionTimeout.val(options[optionName]);
|
||||
$protectedSessionTimeout.val(options['protectedSessionTimeout']);
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
const protectedSessionTimeout = $protectedSessionTimeout.val();
|
||||
|
||||
saveOptions(optionName, protectedSessionTimeout).then(() => {
|
||||
saveOptions({ 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
|
||||
protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
|
||||
});
|
||||
|
||||
@@ -117,14 +152,13 @@ addTabHandler((function() {
|
||||
addTabHandler((function () {
|
||||
const $form = $("#note-revision-snapshot-time-interval-form");
|
||||
const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
|
||||
const optionName = 'noteRevisionSnapshotTimeInterval';
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$timeInterval.val(options[optionName]);
|
||||
$timeInterval.val(options['noteRevisionSnapshotTimeInterval']);
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
saveOptions(optionName, $timeInterval.val());
|
||||
saveOptions({ 'noteRevisionSnapshotTimeInterval': $timeInterval.val() });
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -137,6 +171,7 @@ addTabHandler((function () {
|
||||
addTabHandler((async function () {
|
||||
const $appVersion = $("#app-version");
|
||||
const $dbVersion = $("#db-version");
|
||||
const $syncVersion = $("#sync-version");
|
||||
const $buildDate = $("#build-date");
|
||||
const $buildRevision = $("#build-revision");
|
||||
|
||||
@@ -144,6 +179,7 @@ addTabHandler((async function () {
|
||||
|
||||
$appVersion.html(appInfo.appVersion);
|
||||
$dbVersion.html(appInfo.dbVersion);
|
||||
$syncVersion.html(appInfo.syncVersion);
|
||||
$buildDate.html(appInfo.buildDate);
|
||||
$buildRevision.html(appInfo.buildRevision);
|
||||
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
|
||||
@@ -151,6 +187,57 @@ addTabHandler((async function () {
|
||||
return {};
|
||||
})());
|
||||
|
||||
addTabHandler((function() {
|
||||
const $form = $("#sync-setup-form");
|
||||
const $syncServerHost = $("#sync-server-host");
|
||||
const $syncServerTimeout = $("#sync-server-timeout");
|
||||
const $syncProxy = $("#sync-proxy");
|
||||
const $testSyncButton = $("#test-sync-button");
|
||||
const $syncToServerButton = $("#sync-to-server-button");
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$syncServerHost.val(options['syncServerHost']);
|
||||
$syncServerTimeout.val(options['syncServerTimeout']);
|
||||
$syncProxy.val(options['syncProxy']);
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
saveOptions({
|
||||
'syncServerHost': $syncServerHost.val(),
|
||||
'syncServerTimeout': $syncServerTimeout.val(),
|
||||
'syncProxy': $syncProxy.val()
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$testSyncButton.click(async () => {
|
||||
const result = await server.post('sync/test');
|
||||
|
||||
if (result.connection === "Success") {
|
||||
infoService.showMessage("Sync server handshake has been successful");
|
||||
}
|
||||
else {
|
||||
infoService.showError("Sync server handshake failed, error: " + result.error);
|
||||
}
|
||||
});
|
||||
|
||||
$syncToServerButton.click(async () => {
|
||||
const resp = await server.post("setup/sync-to-server");
|
||||
|
||||
if (resp.success) {
|
||||
infoService.showMessage("Sync has been established to the server instance. It will take some time to finish.");
|
||||
}
|
||||
else {
|
||||
infoService.showError('Sync setup failed: ' + resp.error);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
optionsLoaded
|
||||
};
|
||||
})());
|
||||
|
||||
addTabHandler((async function () {
|
||||
const $forceFullSyncButton = $("#force-full-sync-button");
|
||||
const $fillSyncRowsButton = $("#fill-sync-rows-button");
|
||||
|
||||
@@ -15,7 +15,11 @@ async function showDialog() {
|
||||
|
||||
const result = await server.get('recent-changes/');
|
||||
|
||||
$dialog.html('');
|
||||
$dialog.empty();
|
||||
|
||||
if (result.length === 0) {
|
||||
$dialog.append("No changes yet ...");
|
||||
}
|
||||
|
||||
const groupedByDate = groupByDate(result);
|
||||
|
||||
@@ -30,9 +34,9 @@ async function showDialog() {
|
||||
const revLink = $("<a>", {
|
||||
href: 'javascript:',
|
||||
text: 'rev'
|
||||
}).attr('action', 'note-revision')
|
||||
.attr('note-path', change.noteId)
|
||||
.attr('note-revision-id', change.noteRevisionId);
|
||||
}).attr('data-action', 'note-revision')
|
||||
.attr('data-note-path', change.noteId)
|
||||
.attr('data-note-revision-id', change.noteRevisionId);
|
||||
|
||||
let noteLink;
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import server from '../services/server.js';
|
||||
|
||||
const $dialog = $("#recent-notes-dialog");
|
||||
const $searchInput = $('#recent-notes-search-input');
|
||||
|
||||
function addRecentNote(branchId, notePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === treeService.getCurrentNotePath()) {
|
||||
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 100,
|
||||
position: { my: "center top+100", at: "top", of: window }
|
||||
});
|
||||
|
||||
$searchInput.val('');
|
||||
|
||||
const result = await server.get('recent-notes');
|
||||
|
||||
// remove the current note
|
||||
const recNotes = result.filter(note => note.notePath !== treeService.getCurrentNotePath());
|
||||
|
||||
const items = recNotes.map(rn => {
|
||||
return {
|
||||
label: rn.title,
|
||||
value: rn.notePath
|
||||
};
|
||||
});
|
||||
|
||||
$searchInput.autocomplete({
|
||||
source: items,
|
||||
minLength: 0,
|
||||
autoFocus: true,
|
||||
select: function (event, ui) {
|
||||
treeService.activateNode(ui.item.value);
|
||||
|
||||
$searchInput.autocomplete('destroy');
|
||||
$dialog.dialog('close');
|
||||
},
|
||||
focus: function (event, ui) {
|
||||
event.preventDefault();
|
||||
},
|
||||
close: function (event, ui) {
|
||||
if (event.keyCode === 27) { // escape closes dialog
|
||||
$searchInput.autocomplete('destroy');
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
else {
|
||||
// keep autocomplete open
|
||||
// we're kind of abusing autocomplete to work in a way which it's not designed for
|
||||
$searchInput.autocomplete("search", "");
|
||||
}
|
||||
},
|
||||
create: () => $searchInput.autocomplete("search", ""),
|
||||
classes: {
|
||||
"ui-autocomplete": "recent-notes-autocomplete"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
showDialog,
|
||||
addRecentNote
|
||||
};
|
||||
@@ -1,19 +1,28 @@
|
||||
/** Represents mapping between note and parent note */
|
||||
class Branch {
|
||||
constructor(treeCache, row) {
|
||||
this.treeCache = treeCache;
|
||||
/** @param {string} primary key */
|
||||
this.branchId = row.branchId;
|
||||
/** @param {string} */
|
||||
this.noteId = row.noteId;
|
||||
this.note = null;
|
||||
/** @param {string} */
|
||||
this.parentNoteId = row.parentNoteId;
|
||||
/** @param {int} */
|
||||
this.notePosition = row.notePosition;
|
||||
/** @param {string} */
|
||||
this.prefix = row.prefix;
|
||||
/** @param {boolean} */
|
||||
this.isExpanded = row.isExpanded;
|
||||
}
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
async getNote() {
|
||||
return await this.treeCache.getNote(this.noteId);
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if it's top level, meaning its parent is root note */
|
||||
isTopLevel() {
|
||||
return this.parentNoteId === 'root';
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import NoteShort from './note_short.js';
|
||||
|
||||
/**
|
||||
* Represents full note, specifically including note's content.
|
||||
*/
|
||||
class NoteFull extends NoteShort {
|
||||
constructor(treeCache, row) {
|
||||
super(treeCache, row);
|
||||
|
||||
/** @param {string} */
|
||||
this.content = row.content;
|
||||
|
||||
if (this.content !== "" && this.isJson()) {
|
||||
try {
|
||||
/** @param {object} */
|
||||
this.jsonContent = JSON.parse(this.content);
|
||||
}
|
||||
catch(e) {}
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
/**
|
||||
* This note's representation is used in note tree and is kept in TreeCache.
|
||||
* Its notable omission is the note content.
|
||||
*/
|
||||
class NoteShort {
|
||||
constructor(treeCache, row) {
|
||||
this.treeCache = treeCache;
|
||||
/** @param {string} */
|
||||
this.noteId = row.noteId;
|
||||
/** @param {string} */
|
||||
this.title = row.title;
|
||||
/** @param {boolean} */
|
||||
this.isProtected = row.isProtected;
|
||||
/** @param {string} one of 'text', 'code', 'file' or 'render' */
|
||||
this.type = row.type;
|
||||
/** @param {string} content-type, e.g. "application/json" */
|
||||
this.mime = row.mime;
|
||||
/** @param {boolean} */
|
||||
this.archived = row.archived;
|
||||
this.cssClass = row.cssClass;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
isJson() {
|
||||
return this.mime === "application/json";
|
||||
}
|
||||
|
||||
/** @returns {Promise<Branch[]>} */
|
||||
async getBranches() {
|
||||
const branchIds = this.treeCache.parents[this.noteId].map(
|
||||
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
|
||||
@@ -20,11 +33,13 @@ class NoteShort {
|
||||
return this.treeCache.getBranches(branchIds);
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasChildren() {
|
||||
return this.treeCache.children[this.noteId]
|
||||
&& this.treeCache.children[this.noteId].length > 0;
|
||||
}
|
||||
|
||||
/** @returns {Promise<Branch[]>} */
|
||||
async getChildBranches() {
|
||||
if (!this.treeCache.children[this.noteId]) {
|
||||
return [];
|
||||
@@ -36,18 +51,22 @@ class NoteShort {
|
||||
return await this.treeCache.getBranches(branchIds);
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
getParentNoteIds() {
|
||||
return this.treeCache.parents[this.noteId] || [];
|
||||
}
|
||||
|
||||
/** @returns {Promise<NoteShort[]>} */
|
||||
async getParentNotes() {
|
||||
return await this.treeCache.getNotes(this.getParentNoteIds());
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
getChildNoteIds() {
|
||||
return this.treeCache.children[this.noteId] || [];
|
||||
}
|
||||
|
||||
/** @returns {Promise<NoteShort[]>} */
|
||||
async getChildNotes() {
|
||||
return await this.treeCache.getNotes(this.getChildNoteIds());
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import server from './services/server.js';
|
||||
|
||||
$(document).ready(async () => {
|
||||
const {appDbVersion, dbVersion} = await server.get('migration');
|
||||
|
||||
console.log("HI", {appDbVersion, dbVersion});
|
||||
|
||||
if (appDbVersion === dbVersion) {
|
||||
$("#up-to-date").show();
|
||||
}
|
||||
else {
|
||||
$("#need-to-migrate").show();
|
||||
|
||||
$("#app-db-version").html(appDbVersion);
|
||||
$("#db-version").html(dbVersion);
|
||||
}
|
||||
});
|
||||
|
||||
$("#run-migration").click(async () => {
|
||||
$("#run-migration").prop("disabled", true);
|
||||
|
||||
$("#migration-result").show();
|
||||
|
||||
const result = await server.post('migration');
|
||||
|
||||
for (const migration of result.migrations) {
|
||||
const row = $('<tr>')
|
||||
.append($('<td>').html(migration.dbVersion))
|
||||
.append($('<td>').html(migration.name))
|
||||
.append($('<td>').html(migration.success ? 'Yes' : 'No'))
|
||||
.append($('<td>').html(migration.success ? 'N/A' : migration.error));
|
||||
|
||||
if (!migration.success) {
|
||||
row.addClass("danger");
|
||||
}
|
||||
|
||||
$("#migration-table").append(row);
|
||||
}
|
||||
});
|
||||
|
||||
// copy of this shortcut to be able to debug migration problems
|
||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
|
||||
require('electron').remote.getCurrentWindow().toggleDevTools();
|
||||
|
||||
return false;
|
||||
});
|
||||
24
src/public/javascripts/services/bootstrap.js
vendored
24
src/public/javascripts/services/bootstrap.js
vendored
@@ -1,10 +1,9 @@
|
||||
import addLinkDialog from '../dialogs/add_link.js';
|
||||
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
|
||||
import labelsDialog from '../dialogs/labels.js';
|
||||
import attributesDialog from '../dialogs/attributes.js';
|
||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
|
||||
import noteSourceDialog from '../dialogs/note_source.js';
|
||||
import recentChangesDialog from '../dialogs/recent_changes.js';
|
||||
import recentNotesDialog from '../dialogs/recent_notes.js';
|
||||
import optionsDialog from '../dialogs/options.js';
|
||||
import sqlConsoleDialog from '../dialogs/sql_console.js';
|
||||
|
||||
@@ -18,7 +17,7 @@ import noteDetailService from './note_detail.js';
|
||||
import noteType from './note_type.js';
|
||||
import protected_session from './protected_session.js';
|
||||
import searchNotesService from './search_notes.js';
|
||||
import ScriptApi from './script_api.js';
|
||||
import FrontendScriptApi from './frontend_script_api.js';
|
||||
import ScriptContext from './script_context.js';
|
||||
import sync from './sync.js';
|
||||
import treeService from './tree.js';
|
||||
@@ -36,6 +35,8 @@ import libraryLoader from "./library_loader.js";
|
||||
window.glob.getCurrentNode = treeService.getCurrentNode;
|
||||
window.glob.getHeaders = server.getHeaders;
|
||||
window.glob.showAddLinkDialog = addLinkDialog.showDialog;
|
||||
// this is required by CKEditor when uploading images
|
||||
window.glob.noteChanged = noteDetailService.noteChanged;
|
||||
|
||||
// required for ESLint plugin
|
||||
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
|
||||
@@ -47,7 +48,12 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
|
||||
let message = "Uncaught error: ";
|
||||
|
||||
if (string.indexOf("script error") > -1){
|
||||
if (string.includes("Cannot read property 'defaultView' of undefined")) {
|
||||
// ignore this specific error which is very common but we don't know where it comes from
|
||||
// and it seems to be harmless
|
||||
return true;
|
||||
}
|
||||
else if (string.includes("script error")) {
|
||||
message += 'No details available';
|
||||
}
|
||||
else {
|
||||
@@ -65,6 +71,14 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
return false;
|
||||
};
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
|
||||
|
||||
$(document).on("click", "button[data-help-page]", e => {
|
||||
const $button = $(e.target);
|
||||
|
||||
window.open(wikiBaseUrl + $button.attr("data-help-page"), '_blank');
|
||||
});
|
||||
|
||||
$("#logout-button").toggle(!utils.isElectron());
|
||||
|
||||
if (utils.isElectron()) {
|
||||
@@ -74,7 +88,7 @@ if (utils.isElectron()) {
|
||||
await treeService.reload();
|
||||
}
|
||||
|
||||
await treeService.activateNode(parentNoteId);
|
||||
await treeService.activateNote(parentNoteId);
|
||||
|
||||
setTimeout(() => {
|
||||
const node = treeService.getCurrentNode();
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import ScriptContext from "./script_context.js";
|
||||
import server from "./server.js";
|
||||
import infoService from "./info.js";
|
||||
|
||||
async function executeBundle(bundle) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
|
||||
async function getAndExecuteBundle(noteId, originEntity = null) {
|
||||
const bundle = await server.get('script/bundle/' + noteId);
|
||||
|
||||
return await (function () {
|
||||
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
||||
}.call(apiContext));
|
||||
await executeBundle(bundle, originEntity);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, originEntity) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
|
||||
|
||||
try {
|
||||
return await (function () {
|
||||
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
||||
}.call(apiContext));
|
||||
}
|
||||
catch (e) {
|
||||
infoService.showAndLogError(`Execution of script "${bundle.note.title}" (${bundle.note.noteId}) failed with error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStartupBundles() {
|
||||
@@ -17,7 +29,17 @@ async function executeStartupBundles() {
|
||||
}
|
||||
}
|
||||
|
||||
async function executeRelationBundles(note, relationName) {
|
||||
const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
|
||||
|
||||
for (const bundle of bundlesToRun) {
|
||||
await executeBundle(bundle, note);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
executeBundle,
|
||||
executeStartupBundles
|
||||
getAndExecuteBundle,
|
||||
executeStartupBundles,
|
||||
executeRelationBundles
|
||||
}
|
||||
@@ -121,7 +121,7 @@ const contextMenuOptions = {
|
||||
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "editBranchPrefix", parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search');
|
||||
|
||||
// Activate node on right-click
|
||||
node.setActive();
|
||||
|
||||
@@ -6,13 +6,12 @@ import zoomService from "./zoom.js";
|
||||
import noteRevisionsDialog from "../dialogs/note_revisions.js";
|
||||
import optionsDialog from "../dialogs/options.js";
|
||||
import addLinkDialog from "../dialogs/add_link.js";
|
||||
import recentNotesDialog from "../dialogs/recent_notes.js";
|
||||
import jumpToNoteDialog from "../dialogs/jump_to_note.js";
|
||||
import noteSourceDialog from "../dialogs/note_source.js";
|
||||
import recentChangesDialog from "../dialogs/recent_changes.js";
|
||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
|
||||
import searchNotesService from "./search_notes.js";
|
||||
import labelsDialog from "../dialogs/labels.js";
|
||||
import attributesDialog from "../dialogs/attributes.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
|
||||
function registerEntrypoints() {
|
||||
@@ -29,21 +28,17 @@ function registerEntrypoints() {
|
||||
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
|
||||
|
||||
$("#show-source-button").click(noteSourceDialog.showDialog);
|
||||
utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
|
||||
|
||||
$("#recent-changes-button").click(recentChangesDialog.showDialog);
|
||||
|
||||
$("#protected-session-on").click(protectedSessionService.enterProtectedSession);
|
||||
$("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
|
||||
|
||||
$("#recent-notes-button").click(recentNotesDialog.showDialog);
|
||||
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
|
||||
|
||||
$("#toggle-search-button").click(searchNotesService.toggleSearch);
|
||||
utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
|
||||
|
||||
$(".show-labels-button").click(labelsDialog.showDialog);
|
||||
utils.bindShortcut('alt+l', labelsDialog.showDialog);
|
||||
$(".show-attributes-button").click(attributesDialog.showDialog);
|
||||
utils.bindShortcut('alt+a', attributesDialog.showDialog);
|
||||
|
||||
$("#options-button").click(optionsDialog.showDialog);
|
||||
|
||||
@@ -58,7 +53,13 @@ function registerEntrypoints() {
|
||||
utils.bindShortcut('alt+right', window.history.forward);
|
||||
}
|
||||
|
||||
utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed"));
|
||||
utils.bindShortcut('alt+m', e => {
|
||||
$(".hide-toggle").toggle();
|
||||
|
||||
// when hiding switch display to block, otherwise grid still tries to display columns which shows
|
||||
// left empty column
|
||||
$("#container").css("display", $("#container").css("display") === "grid" ? "block" : "grid");
|
||||
});
|
||||
|
||||
// hide (toggle) everything except for the note content for distraction free writing
|
||||
utils.bindShortcut('alt+t', e => {
|
||||
|
||||
@@ -21,7 +21,7 @@ $("#file-upload").change(async function() {
|
||||
|
||||
await treeService.reload();
|
||||
|
||||
await treeService.activateNode(resp.noteId);
|
||||
await treeService.activateNote(resp.noteId);
|
||||
});
|
||||
|
||||
export default {
|
||||
|
||||
196
src/public/javascripts/services/frontend_script_api.js
Normal file
196
src/public/javascripts/services/frontend_script_api.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import treeService from './tree.js';
|
||||
import server from './server.js';
|
||||
import utils from './utils.js';
|
||||
import infoService from './info.js';
|
||||
import linkService from './link.js';
|
||||
import treeCache from './tree_cache.js';
|
||||
|
||||
/**
|
||||
* This is the main frontend API interface for scripts. It's published in the local "api" object.
|
||||
*
|
||||
* @constructor
|
||||
* @hideconstructor
|
||||
*/
|
||||
function FrontendScriptApi(startNote, currentNote, originEntity = null) {
|
||||
const $pluginButtons = $("#plugin-buttons");
|
||||
|
||||
/** @property {object} note where script started executing */
|
||||
this.startNote = startNote;
|
||||
/** @property {object} note where script is currently executing */
|
||||
this.currentNote = currentNote;
|
||||
/** @property {object|null} entity whose event triggered this execution */
|
||||
this.originEntity = originEntity;
|
||||
|
||||
/**
|
||||
* Activates note in the tree and in the note detail.
|
||||
*
|
||||
* @method
|
||||
* @param {string} notePath (or noteId)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
this.activateNote = treeService.activateNote;
|
||||
|
||||
/**
|
||||
* Activates newly created note. Compared to this.activateNote() also refreshes tree.
|
||||
*
|
||||
* @param {string} notePath (or noteId)
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
this.activateNewNote = async notePath => {
|
||||
await treeService.reload();
|
||||
|
||||
await treeService.activateNote(notePath, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} ToolbarButtonOptions
|
||||
* @property {string} title
|
||||
* @property {string} [icon] - name of the jQuery UI icon to be used (e.g. "clock" for "ui-icon-clock" icon)
|
||||
* @property {function} action - callback handling the click on the button
|
||||
* @property {string} [shortcut] - keyboard shortcut for the button, e.g. "alt+t"
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds new button the the plugin area.
|
||||
*
|
||||
* @param {ToolbarButtonOptions} opts
|
||||
*/
|
||||
this.addButtonToToolbar = opts => {
|
||||
const buttonId = "toolbar-button-" + opts.title.replace(/[^a-zA-Z0-9]/g, "-");
|
||||
|
||||
$("#" + buttonId).remove();
|
||||
|
||||
const icon = $("<span>")
|
||||
.addClass("ui-icon ui-icon-" + opts.icon);
|
||||
|
||||
const button = $('<button>')
|
||||
.addClass("btn btn-xs")
|
||||
.click(opts.action)
|
||||
.append(icon)
|
||||
.append($("<span>").text(opts.title));
|
||||
|
||||
button.attr('id', buttonId);
|
||||
|
||||
$pluginButtons.append(button);
|
||||
|
||||
if (opts.shortcut) {
|
||||
$(document).bind('keydown', opts.shortcut, opts.action);
|
||||
|
||||
button.attr("title", "Shortcut " + opts.shortcut);
|
||||
}
|
||||
};
|
||||
|
||||
function prepareParams(params) {
|
||||
if (!params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return params.map(p => {
|
||||
if (typeof p === "function") {
|
||||
return "!@#Function: " + p.toString();
|
||||
}
|
||||
else {
|
||||
return p;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes given anonymous function on the server.
|
||||
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
|
||||
*
|
||||
* @param {string} script - script to be executed on the backend
|
||||
* @param {Array.<?>} params - list of parameters to the anonymous function to be send to backend
|
||||
* @return {Promise<*>} return value of the executed function on the backend
|
||||
*/
|
||||
this.runOnServer = async (script, params = []) => {
|
||||
if (typeof script === "function") {
|
||||
script = script.toString();
|
||||
}
|
||||
|
||||
const ret = await server.post('script/exec', {
|
||||
script: script,
|
||||
params: prepareParams(params),
|
||||
startNoteId: startNote.noteId,
|
||||
currentNoteId: currentNote.noteId,
|
||||
originEntityName: "notes", // currently there's no other entity on frontend which can trigger event
|
||||
originEntityId: originEntity ? originEntity.noteId : null
|
||||
});
|
||||
|
||||
if (ret.success) {
|
||||
return ret.executionResult;
|
||||
}
|
||||
else {
|
||||
throw new Error("server error: " + ret.error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns list of notes. If note is missing from cache, it's loaded.
|
||||
*
|
||||
* This is often used to bulk-fill the cache with notes which would have to be picked one by one
|
||||
* otherwise (by e.g. createNoteLink())
|
||||
*
|
||||
* @param {string[]} noteIds
|
||||
* @param {boolean} [silentNotFoundError] - don't report error if the note is not found
|
||||
* @return {Promise<NoteShort[]>}
|
||||
*/
|
||||
this.getNotes = async (noteIds, silentNotFoundError = false) => await treeCache.getNotes(noteIds, silentNotFoundError);
|
||||
|
||||
/**
|
||||
* Instance name identifies particular Trilium instance. It can be useful for scripts
|
||||
* if some action needs to happen on only one specific instance.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
this.getInstanceName = () => window.glob.instanceName;
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @param {Date} date
|
||||
* @returns {string} date in YYYY-MM-DD format
|
||||
*/
|
||||
this.formatDateISO = utils.formatDateISO;
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @param {string} str
|
||||
* @returns {Date} parsed object
|
||||
*/
|
||||
this.parseDate = utils.parseDate;
|
||||
|
||||
/**
|
||||
* Show info message to the user.
|
||||
*
|
||||
* @method
|
||||
* @param {string} message
|
||||
*/
|
||||
this.showMessage = infoService.showMessage;
|
||||
|
||||
/**
|
||||
* Show error message to the user.
|
||||
*
|
||||
* @method
|
||||
* @param {string} message
|
||||
*/
|
||||
this.showError = infoService.showError;
|
||||
|
||||
/**
|
||||
* Refresh tree
|
||||
*
|
||||
* @method
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
this.refreshTree = treeService.reload;
|
||||
|
||||
/**
|
||||
* Create note link (jQuery object) for given note.
|
||||
*
|
||||
* @method
|
||||
* @param {string} notePath (or noteId)
|
||||
* @param {string} [noteTitle] - if not present we'll use note title
|
||||
*/
|
||||
this.createNoteLink = linkService.createNoteLink;
|
||||
}
|
||||
|
||||
export default FrontendScriptApi;
|
||||
@@ -14,6 +14,12 @@ function showMessage(message) {
|
||||
});
|
||||
}
|
||||
|
||||
function showAndLogError(message, delay = 10000) {
|
||||
showError(message, delay);
|
||||
|
||||
messagingService.logError(message);
|
||||
}
|
||||
|
||||
function showError(message, delay = 10000) {
|
||||
console.log(utils.now(), "error: ", message);
|
||||
|
||||
@@ -36,5 +42,6 @@ function throwError(message) {
|
||||
export default {
|
||||
showMessage,
|
||||
showError,
|
||||
showAndLogError,
|
||||
throwError
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import noteDetailText from './note_detail_text.js';
|
||||
import treeUtils from './tree_utils.js';
|
||||
|
||||
function getNotePathFromLink(url) {
|
||||
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
|
||||
const notePathMatch = /#root([A-Za-z0-9/]*)$/.exec(url);
|
||||
|
||||
if (notePathMatch === null) {
|
||||
return null;
|
||||
@@ -13,8 +13,8 @@ function getNotePathFromLink(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function getNodePathFromLabel(label) {
|
||||
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
|
||||
function getNotePathFromLabel(label) {
|
||||
const notePathMatch = / \(([#A-Za-z0-9/]+)\)/.exec(label);
|
||||
|
||||
if (notePathMatch !== null) {
|
||||
return notePathMatch[1];
|
||||
@@ -23,7 +23,7 @@ function getNodePathFromLabel(label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createNoteLink(notePath, noteTitle) {
|
||||
async function createNoteLink(notePath, noteTitle = null) {
|
||||
if (!noteTitle) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
@@ -33,8 +33,8 @@ async function createNoteLink(notePath, noteTitle) {
|
||||
const noteLink = $("<a>", {
|
||||
href: 'javascript:',
|
||||
text: noteTitle
|
||||
}).attr('action', 'note')
|
||||
.attr('note-path', notePath);
|
||||
}).attr('data-action', 'note')
|
||||
.attr('data-note-path', notePath);
|
||||
|
||||
return noteLink;
|
||||
}
|
||||
@@ -43,10 +43,10 @@ function goToLink(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $link = $(e.target);
|
||||
let notePath = $link.attr("note-path");
|
||||
let notePath = $link.attr("data-note-path");
|
||||
|
||||
if (!notePath) {
|
||||
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
|
||||
const address = $link.attr("data-note-path") ? $link.attr("data-note-path") : $link.attr('href');
|
||||
|
||||
if (!address) {
|
||||
return;
|
||||
@@ -61,7 +61,7 @@ function goToLink(e) {
|
||||
notePath = getNotePathFromLink(address);
|
||||
}
|
||||
|
||||
treeService.activateNode(notePath);
|
||||
treeService.activateNote(notePath);
|
||||
|
||||
// this is quite ugly hack, but it seems like we can't close the tooltip otherwise
|
||||
$("[role='tooltip']").remove();
|
||||
@@ -90,14 +90,26 @@ function addTextToEditor(text) {
|
||||
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
|
||||
}
|
||||
|
||||
ko.bindingHandlers.noteLink = {
|
||||
init: async function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
||||
const noteId = ko.unwrap(valueAccessor());
|
||||
|
||||
if (noteId) {
|
||||
const link = await createNoteLink(noteId);
|
||||
|
||||
$(element).append(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
|
||||
// of opening the link in new window/tab
|
||||
$(document).on('click', "a[action='note']", goToLink);
|
||||
$(document).on('click', "a[data-action='note']", goToLink);
|
||||
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
|
||||
$(document).on('dblclick', '#note-detail-text a', goToLink);
|
||||
|
||||
export default {
|
||||
getNodePathFromLabel,
|
||||
getNotePathFromLabel,
|
||||
getNotePathFromLink,
|
||||
createNoteLink,
|
||||
addLinkToEditor,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import utils from './utils.js';
|
||||
import infoService from "./info.js";
|
||||
|
||||
const $changesToPushCount = $("#changes-to-push-count");
|
||||
const $outstandingSyncsCount = $("#outstanding-syncs-count");
|
||||
|
||||
const syncMessageHandlers = [];
|
||||
const messageHandlers = [];
|
||||
|
||||
let ws;
|
||||
@@ -25,9 +26,17 @@ function subscribeToMessages(messageHandler) {
|
||||
messageHandlers.push(messageHandler);
|
||||
}
|
||||
|
||||
function subscribeToSyncMessages(messageHandler) {
|
||||
syncMessageHandlers.push(messageHandler);
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
for (const messageHandler of messageHandlers) {
|
||||
messageHandler(message);
|
||||
}
|
||||
|
||||
if (message.type === 'sync') {
|
||||
lastPingTs = new Date().getTime();
|
||||
|
||||
@@ -39,11 +48,11 @@ function handleMessage(event) {
|
||||
|
||||
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
|
||||
|
||||
for (const messageHandler of messageHandlers) {
|
||||
messageHandler(syncData);
|
||||
for (const syncMessageHandler of syncMessageHandlers) {
|
||||
syncMessageHandler(syncData);
|
||||
}
|
||||
|
||||
$changesToPushCount.html(message.changesToPushCount);
|
||||
$outstandingSyncsCount.html(message.outstandingSyncs);
|
||||
}
|
||||
else if (message.type === 'sync-hash-check-failed') {
|
||||
infoService.showError("Sync check failed!", 60000);
|
||||
@@ -73,26 +82,10 @@ setTimeout(() => {
|
||||
|
||||
lastSyncId = glob.maxSyncIdAtLoad;
|
||||
lastPingTs = new Date().getTime();
|
||||
let connectionBrokenNotification = null;
|
||||
|
||||
setInterval(async () => {
|
||||
if (new Date().getTime() - lastPingTs > 30000) {
|
||||
if (!connectionBrokenNotification) {
|
||||
connectionBrokenNotification = $.notify({
|
||||
// options
|
||||
message: "Lost connection to server"
|
||||
},{
|
||||
// options
|
||||
type: 'danger',
|
||||
delay: 100000000 // keep it until we explicitly close it
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (connectionBrokenNotification) {
|
||||
await connectionBrokenNotification.close();
|
||||
connectionBrokenNotification = null;
|
||||
|
||||
infoService.showMessage("Re-connected to server");
|
||||
console.log("Lost connection to server");
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
@@ -104,5 +97,6 @@ setTimeout(() => {
|
||||
|
||||
export default {
|
||||
logError,
|
||||
subscribeToMessages
|
||||
subscribeToMessages,
|
||||
subscribeToSyncMessages
|
||||
};
|
||||
60
src/public/javascripts/services/note_autocomplete.js
Normal file
60
src/public/javascripts/services/note_autocomplete.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import server from "./server.js";
|
||||
import noteDetailService from "./note_detail.js";
|
||||
|
||||
async function autocompleteSource(request, response) {
|
||||
const result = await server.get('autocomplete'
|
||||
+ '?query=' + encodeURIComponent(request.term)
|
||||
+ '¤tNoteId=' + noteDetailService.getCurrentNoteId());
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
value: row.label + ' (' + row.value + ')'
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
async function initNoteAutocomplete($el) {
|
||||
if (!$el.hasClass("ui-autocomplete-input")) {
|
||||
const $showRecentNotesButton = $("<span>")
|
||||
.addClass("input-group-addon show-recent-notes-button")
|
||||
.prop("title", "Show recent notes");
|
||||
|
||||
$el.after($showRecentNotesButton);
|
||||
|
||||
$showRecentNotesButton.click(() => $el.autocomplete("search", ""));
|
||||
|
||||
await $el.autocomplete({
|
||||
appendTo: $el.parent().parent(),
|
||||
source: autocompleteSource,
|
||||
minLength: 0,
|
||||
change: function (event, ui) {
|
||||
$el.trigger("change");
|
||||
},
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ko.bindingHandlers.noteAutocomplete = {
|
||||
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
||||
initNoteAutocomplete($(element));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
initNoteAutocomplete,
|
||||
autocompleteSource
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
import messagingService from "./messaging.js";
|
||||
import infoService from "./info.js";
|
||||
import linkService from "./link.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import NoteFull from "../entities/note_full.js";
|
||||
import noteDetailCode from './note_detail_code.js';
|
||||
@@ -14,6 +15,8 @@ import noteDetailText from './note_detail_text.js';
|
||||
import noteDetailFile from './note_detail_file.js';
|
||||
import noteDetailSearch from './note_detail_search.js';
|
||||
import noteDetailRender from './note_detail_render.js';
|
||||
import bundleService from "./bundle.js";
|
||||
import noteAutocompleteService from "./note_autocomplete.js";
|
||||
|
||||
const $noteTitle = $("#note-title");
|
||||
|
||||
@@ -22,11 +25,12 @@ const $noteDetailComponents = $(".note-detail-component");
|
||||
const $protectButton = $("#protect-button");
|
||||
const $unprotectButton = $("#unprotect-button");
|
||||
const $noteDetailWrapper = $("#note-detail-wrapper");
|
||||
const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
|
||||
const $noteIdDisplay = $("#note-id-display");
|
||||
const $labelList = $("#label-list");
|
||||
const $labelListInner = $("#label-list-inner");
|
||||
const $attributeList = $("#attribute-list");
|
||||
const $attributeListInner = $("#attribute-list-inner");
|
||||
const $childrenOverview = $("#children-overview");
|
||||
const $scriptArea = $("#note-detail-script-area");
|
||||
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
|
||||
|
||||
let currentNote = null;
|
||||
|
||||
@@ -115,11 +119,12 @@ async function saveNoteIfChanged() {
|
||||
}
|
||||
|
||||
function setNoteBackgroundIfProtected(note) {
|
||||
const isProtected = !!note.isProtected;
|
||||
const isProtected = note.isProtected;
|
||||
|
||||
$noteDetailComponentWrapper.toggleClass("protected", isProtected);
|
||||
$noteDetailWrapper.toggleClass("protected", isProtected);
|
||||
$protectButton.toggleClass("active", isProtected);
|
||||
$unprotectButton.toggleClass("active", !isProtected);
|
||||
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
|
||||
}
|
||||
|
||||
let isNewNoteCreated = false;
|
||||
@@ -153,8 +158,6 @@ async function loadNoteDetail(noteId) {
|
||||
|
||||
setNoteBackgroundIfProtected(currentNote);
|
||||
|
||||
await handleProtectedSession();
|
||||
|
||||
$noteDetailWrapper.show();
|
||||
|
||||
noteChangeDisabled = true;
|
||||
@@ -167,6 +170,8 @@ async function loadNoteDetail(noteId) {
|
||||
|
||||
$noteDetailComponents.hide();
|
||||
|
||||
await handleProtectedSession();
|
||||
|
||||
await getComponent(currentNote.type).show();
|
||||
}
|
||||
finally {
|
||||
@@ -178,9 +183,13 @@ async function loadNoteDetail(noteId) {
|
||||
// after loading new note make sure editor is scrolled to the top
|
||||
$noteDetailWrapper.scrollTop(0);
|
||||
|
||||
const labels = await loadLabelList();
|
||||
$scriptArea.html('');
|
||||
|
||||
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
|
||||
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
|
||||
|
||||
const attributes = await loadAttributes();
|
||||
|
||||
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview');
|
||||
await showChildrenOverview(hideChildrenOverview);
|
||||
}
|
||||
|
||||
@@ -200,7 +209,7 @@ async function showChildrenOverview(hideChildrenOverview) {
|
||||
const link = $('<a>', {
|
||||
href: 'javascript:',
|
||||
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
|
||||
}).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
|
||||
}).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
|
||||
|
||||
const childEl = $('<div class="child-overview">').html(link);
|
||||
$childrenOverview.append(childEl);
|
||||
@@ -209,25 +218,230 @@ async function showChildrenOverview(hideChildrenOverview) {
|
||||
$childrenOverview.show();
|
||||
}
|
||||
|
||||
async function loadLabelList() {
|
||||
async function loadAttributes() {
|
||||
$promotedAttributesContainer.empty();
|
||||
$attributeList.hide();
|
||||
|
||||
const noteId = getCurrentNoteId();
|
||||
|
||||
const labels = await server.get('notes/' + noteId + '/labels');
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
$labelListInner.html('');
|
||||
const promoted = attributes.filter(attr =>
|
||||
(attr.type === 'label-definition' || attr.type === 'relation-definition')
|
||||
&& !attr.name.startsWith("child:")
|
||||
&& attr.value.isPromoted);
|
||||
|
||||
if (labels.length > 0) {
|
||||
for (const label of labels) {
|
||||
$labelListInner.append(utils.formatLabel(label) + " ");
|
||||
let idx = 1;
|
||||
|
||||
async function createRow(definitionAttr, valueAttr) {
|
||||
const definition = definitionAttr.value;
|
||||
const inputId = "promoted-input-" + idx;
|
||||
const $tr = $("<tr>");
|
||||
const $labelCell = $("<th>").append(valueAttr.name);
|
||||
const $input = $("<input>")
|
||||
.prop("id", inputId)
|
||||
.prop("tabindex", definitionAttr.position)
|
||||
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||
.prop("attribute-type", valueAttr.type)
|
||||
.prop("attribute-name", valueAttr.name)
|
||||
.prop("value", valueAttr.value)
|
||||
.addClass("form-control")
|
||||
.addClass("promoted-attribute-input");
|
||||
|
||||
idx++;
|
||||
|
||||
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
|
||||
|
||||
const $actionCell = $("<td>");
|
||||
const $multiplicityCell = $("<td>");
|
||||
|
||||
$tr
|
||||
.append($labelCell)
|
||||
.append($inputCell)
|
||||
.append($actionCell)
|
||||
.append($multiplicityCell);
|
||||
|
||||
if (valueAttr.type === 'label') {
|
||||
if (definition.labelType === 'text') {
|
||||
$input.prop("type", "text");
|
||||
|
||||
// no need to await for this, can be done asynchronously
|
||||
server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => {
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$input.autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: attributeValues.map(attribute => {
|
||||
return {
|
||||
attribute: attribute,
|
||||
value: attribute
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
|
||||
$input.focus(() => $input.autocomplete("search", ""));
|
||||
});
|
||||
}
|
||||
else if (definition.labelType === 'number') {
|
||||
$input.prop("type", "number");
|
||||
}
|
||||
else if (definition.labelType === 'boolean') {
|
||||
$input.prop("type", "checkbox");
|
||||
|
||||
if (valueAttr.value === "true") {
|
||||
$input.prop("checked", "checked");
|
||||
}
|
||||
}
|
||||
else if (definition.labelType === 'date') {
|
||||
$input.prop("type", "text");
|
||||
|
||||
$input.datepicker({
|
||||
changeMonth: true,
|
||||
changeYear: true,
|
||||
yearRange: "c-200:c+10",
|
||||
dateFormat: "yy-mm-dd"
|
||||
});
|
||||
|
||||
const $todayButton = $("<button>").addClass("btn btn-small").text("Today").click(() => {
|
||||
$input.val(utils.formatDateISO(new Date()));
|
||||
$input.trigger("change");
|
||||
});
|
||||
|
||||
$actionCell.append($todayButton);
|
||||
}
|
||||
else if (definition.labelType === 'url') {
|
||||
$input.prop("placeholder", "http://website...");
|
||||
|
||||
const $openButton = $("<button>").addClass("btn btn-small").text("Open").click(() => {
|
||||
window.open($input.val(), '_blank');
|
||||
});
|
||||
|
||||
$actionCell.append($openButton);
|
||||
}
|
||||
else {
|
||||
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
|
||||
}
|
||||
}
|
||||
else if (valueAttr.type === 'relation') {
|
||||
if (valueAttr.value) {
|
||||
$input.val((await treeUtils.getNoteTitle(valueAttr.value) + " (" + valueAttr.value + ")"));
|
||||
}
|
||||
|
||||
// no need to wait for this
|
||||
noteAutocompleteService.initNoteAutocomplete($input);
|
||||
|
||||
// ideally we'd use link instead of button which would allow tooltip preview, but
|
||||
// we can't guarantee updating the link in the a element
|
||||
const $openButton = $("<button>").addClass("btn btn-small").text("Open").click(() => {
|
||||
const notePath = linkService.getNotePathFromLabel($input.val());
|
||||
|
||||
treeService.activateNote(notePath);
|
||||
});
|
||||
|
||||
$actionCell.append($openButton);
|
||||
}
|
||||
else {
|
||||
messagingService.logError("Unknown attribute type=" + valueAttr.type);
|
||||
return;
|
||||
}
|
||||
|
||||
$labelList.show();
|
||||
}
|
||||
else {
|
||||
$labelList.hide();
|
||||
if (definition.multiplicityType === "multivalue") {
|
||||
const addButton = $("<span>")
|
||||
.addClass("glyphicon glyphicon-plus pointer")
|
||||
.prop("title", "Add new attribute")
|
||||
.click(async () => {
|
||||
const $new = await createRow(definitionAttr, {
|
||||
attributeId: "",
|
||||
type: valueAttr.type,
|
||||
name: definitionAttr.name,
|
||||
value: ""
|
||||
});
|
||||
|
||||
$tr.after($new);
|
||||
|
||||
$new.find('input').focus();
|
||||
});
|
||||
|
||||
const removeButton = $("<span>")
|
||||
.addClass("glyphicon glyphicon-trash pointer")
|
||||
.prop("title", "Remove this attribute")
|
||||
.click(async () => {
|
||||
if (valueAttr.attributeId) {
|
||||
await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId);
|
||||
}
|
||||
|
||||
$tr.remove();
|
||||
});
|
||||
|
||||
$multiplicityCell.append(addButton).append(" ").append(removeButton);
|
||||
}
|
||||
|
||||
return $tr;
|
||||
}
|
||||
|
||||
return labels;
|
||||
if (promoted.length > 0) {
|
||||
const $tbody = $("<tbody>");
|
||||
|
||||
for (const definitionAttr of promoted) {
|
||||
const definitionType = definitionAttr.type;
|
||||
const valueType = definitionType.substr(0, definitionType.length - 11);
|
||||
|
||||
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
|
||||
|
||||
if (valueAttrs.length === 0) {
|
||||
valueAttrs.push({
|
||||
attributeId: "",
|
||||
type: valueType,
|
||||
name: definitionAttr.name,
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
if (definitionAttr.value.multiplicityType === 'singlevalue') {
|
||||
valueAttrs = valueAttrs.slice(0, 1);
|
||||
}
|
||||
|
||||
for (const valueAttr of valueAttrs) {
|
||||
const $tr = await createRow(definitionAttr, valueAttr);
|
||||
|
||||
$tbody.append($tr);
|
||||
}
|
||||
}
|
||||
|
||||
// we replace the whole content in one step so there can't be any race conditions
|
||||
// (previously we saw promoted attributes doubling)
|
||||
$promotedAttributesContainer.empty().append($tbody);
|
||||
}
|
||||
else {
|
||||
$attributeListInner.html('');
|
||||
|
||||
if (attributes.length > 0) {
|
||||
for (const attribute of attributes) {
|
||||
if (attribute.type === 'label') {
|
||||
$attributeListInner.append(utils.formatLabel(attribute) + " ");
|
||||
}
|
||||
else if (attribute.type === 'relation') {
|
||||
$attributeListInner.append(attribute.name + "=");
|
||||
$attributeListInner.append(await linkService.createNoteLink(attribute.value));
|
||||
$attributeListInner.append(" ");
|
||||
}
|
||||
else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') {
|
||||
$attributeListInner.append(attribute.name + " definition ");
|
||||
}
|
||||
else {
|
||||
messagingService.logError("Unknown attr type: " + attribute.type);
|
||||
}
|
||||
}
|
||||
|
||||
$attributeList.show();
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async function loadNote(noteId) {
|
||||
@@ -242,7 +456,7 @@ function focus() {
|
||||
getComponent(note.type).focus();
|
||||
}
|
||||
|
||||
messagingService.subscribeToMessages(syncData => {
|
||||
messagingService.subscribeToSyncMessages(syncData => {
|
||||
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
|
||||
infoService.showMessage('Reloading note because of background changes');
|
||||
|
||||
@@ -250,6 +464,35 @@ messagingService.subscribeToMessages(syncData => {
|
||||
}
|
||||
});
|
||||
|
||||
$promotedAttributesContainer.on('change', '.promoted-attribute-input', async event => {
|
||||
const $attr = $(event.target);
|
||||
|
||||
let value;
|
||||
|
||||
if ($attr.prop("type") === "checkbox") {
|
||||
value = $attr.is(':checked') ? "true" : "false";
|
||||
}
|
||||
else if ($attr.prop("attribute-type") === "relation") {
|
||||
if ($attr.val()) {
|
||||
value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel($attr.val()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = $attr.val();
|
||||
}
|
||||
|
||||
const result = await server.put("notes/" + getCurrentNoteId() + "/attribute", {
|
||||
attributeId: $attr.prop("attribute-id"),
|
||||
type: $attr.prop("attribute-type"),
|
||||
name: $attr.prop("attribute-name"),
|
||||
value: value
|
||||
});
|
||||
|
||||
$attr.prop("attribute-id", result.attributeId);
|
||||
|
||||
infoService.showMessage("Attribute has been saved.");
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
$noteTitle.on('input', () => {
|
||||
noteChanged();
|
||||
@@ -278,7 +521,7 @@ export default {
|
||||
getCurrentNoteId,
|
||||
newNoteCreated,
|
||||
focus,
|
||||
loadLabelList,
|
||||
loadAttributes,
|
||||
saveNote,
|
||||
saveNoteIfChanged,
|
||||
noteChanged
|
||||
|
||||
@@ -32,7 +32,7 @@ async function show() {
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lineNumbers: true,
|
||||
tabindex: 2 // so that tab from title will lead to code editor focus
|
||||
tabindex: 100
|
||||
});
|
||||
|
||||
codeEditor.on('change', noteDetailService.noteChanged);
|
||||
@@ -75,9 +75,7 @@ async function executeCurrentNote() {
|
||||
const currentNote = noteDetailService.getCurrentNote();
|
||||
|
||||
if (currentNote.mime.endsWith("env=frontend")) {
|
||||
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
|
||||
|
||||
bundleService.executeBundle(bundle);
|
||||
await bundleService.getAndExecuteBundle(noteDetailService.getCurrentNoteId());
|
||||
}
|
||||
|
||||
if (currentNote.mime.endsWith("env=backend")) {
|
||||
|
||||
@@ -14,13 +14,13 @@ const $fileOpen = $("#file-open");
|
||||
async function show() {
|
||||
const currentNote = noteDetailService.getCurrentNote();
|
||||
|
||||
const labels = await server.get('notes/' + currentNote.noteId + '/labels');
|
||||
const labelMap = utils.toObject(labels, l => [l.name, l.value]);
|
||||
const attributes = await server.get('notes/' + currentNote.noteId + '/attributes');
|
||||
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
|
||||
|
||||
$noteDetailFile.show();
|
||||
|
||||
$fileFileName.text(labelMap.original_file_name);
|
||||
$fileFileSize.text(labelMap.file_size + " bytes");
|
||||
$fileFileName.text(attributeMap.originalFileName);
|
||||
$fileFileSize.text(attributeMap.fileSize + " bytes");
|
||||
$fileFileType.text(currentNote.mime);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ let codeEditorInitialized;
|
||||
async function show() {
|
||||
codeEditorInitialized = false;
|
||||
|
||||
$noteDetailRender.show();
|
||||
$noteDetailRender.empty().show();
|
||||
|
||||
await render();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,13 @@ async function show() {
|
||||
|
||||
textEditor = await BalloonEditor.create($noteDetailText[0], {});
|
||||
|
||||
textEditor.model.document.on('change', noteDetailService.noteChanged);
|
||||
textEditor.model.document.on('change', () => {
|
||||
// change is triggered on just marker/selection changes which is not interesting for us
|
||||
if (textEditor.model.document.differ.getChanges().length > 0) {
|
||||
noteDetailService.noteChanged();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
textEditor.setData(noteDetailService.getCurrentNote().content);
|
||||
|
||||
@@ -32,6 +32,7 @@ function ensureProtectedSession(requireProtectedSession, modal) {
|
||||
const dfd = $.Deferred();
|
||||
|
||||
if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
// using deferred instead of promise because it allows resolving from outside
|
||||
protectedSessionDeferred = dfd;
|
||||
|
||||
if (treeService.getCurrentNode().data.isProtected) {
|
||||
@@ -39,7 +40,9 @@ function ensureProtectedSession(requireProtectedSession, modal) {
|
||||
}
|
||||
|
||||
$dialog.dialog({
|
||||
modal: modal,
|
||||
// everything is now non-modal, because modal dialog caused weird high CPU usage on opening
|
||||
// and tearing of text input
|
||||
modal: false,
|
||||
width: 400,
|
||||
open: () => {
|
||||
if (!modal) {
|
||||
@@ -125,7 +128,14 @@ async function unprotectNoteAndSendToServer() {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureProtectedSession(true, true);
|
||||
if (!protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
console.log("Unprotecting notes outside of protected session is not allowed.");
|
||||
// the reason is that it's not easy to handle even with ensureProtectedSession,
|
||||
// because we would first have to make sure the note is loaded and only then unprotect
|
||||
// we used to have a bug where we would overwrite the previous note with unprotected content.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const note = noteDetailService.getCurrentNote();
|
||||
note.isProtected = false;
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import treeService from './tree.js';
|
||||
import server from './server.js';
|
||||
import utils from './utils.js';
|
||||
import infoService from './info.js';
|
||||
|
||||
function ScriptApi(startNote, currentNote) {
|
||||
const $pluginButtons = $("#plugin-buttons");
|
||||
|
||||
async function activateNote(notePath) {
|
||||
await treeService.activateNode(notePath);
|
||||
}
|
||||
|
||||
function addButtonToToolbar(buttonId, button) {
|
||||
$("#" + buttonId).remove();
|
||||
|
||||
button.attr('id', buttonId);
|
||||
|
||||
$pluginButtons.append(button);
|
||||
}
|
||||
|
||||
function prepareParams(params) {
|
||||
if (!params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return params.map(p => {
|
||||
if (typeof p === "function") {
|
||||
return "!@#Function: " + p.toString();
|
||||
}
|
||||
else {
|
||||
return p;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function runOnServer(script, params = []) {
|
||||
if (typeof script === "function") {
|
||||
script = script.toString();
|
||||
}
|
||||
|
||||
const ret = await server.post('script/exec', {
|
||||
script: script,
|
||||
params: prepareParams(params),
|
||||
startNoteId: startNote.noteId,
|
||||
currentNoteId: currentNote.noteId
|
||||
});
|
||||
|
||||
return ret.executionResult;
|
||||
}
|
||||
|
||||
return {
|
||||
startNote: startNote,
|
||||
currentNote: currentNote,
|
||||
addButtonToToolbar,
|
||||
activateNote,
|
||||
getInstanceName: () => window.glob.instanceName,
|
||||
runOnServer,
|
||||
formatDateISO: utils.formatDateISO,
|
||||
parseDate: utils.parseDate,
|
||||
showMessage: infoService.showMessage,
|
||||
showError: infoService.showError,
|
||||
reloadTree: treeService.reload
|
||||
}
|
||||
}
|
||||
|
||||
export default ScriptApi;
|
||||
@@ -1,13 +1,13 @@
|
||||
import ScriptApi from './script_api.js';
|
||||
import FrontendScriptApi from './frontend_script_api.js';
|
||||
import utils from './utils.js';
|
||||
|
||||
function ScriptContext(startNote, allNotes) {
|
||||
function ScriptContext(startNote, allNotes, originEntity = null) {
|
||||
const modules = {};
|
||||
|
||||
return {
|
||||
modules: modules,
|
||||
notes: utils.toObject(allNotes, note => [note.noteId, note]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, new FrontendScriptApi(startNote, note, originEntity)]),
|
||||
require: moduleNoteIds => {
|
||||
return moduleName => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
|
||||
@@ -58,7 +58,7 @@ async function doSearch(searchText) {
|
||||
const link = $('<a>', {
|
||||
href: 'javascript:',
|
||||
text: result.title
|
||||
}).attr('action', 'note').attr('note-path', result.path);
|
||||
}).attr('data-action', 'note').attr('data-note-path', result.path);
|
||||
|
||||
const $result = $('<li>').append(link);
|
||||
|
||||
@@ -73,7 +73,7 @@ async function saveSearch() {
|
||||
|
||||
await treeService.reload();
|
||||
|
||||
await treeService.activateNode(noteId);
|
||||
await treeService.activateNote(noteId);
|
||||
}
|
||||
|
||||
$searchInput.keyup(e => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import infoService from "./info.js";
|
||||
function getHeaders() {
|
||||
let protectedSessionId = null;
|
||||
|
||||
try { // this is because protected session might not be declared in some cases - like when it's included in migration page
|
||||
try { // this is because protected session might not be declared in some cases
|
||||
protectedSessionId = protectedSessionHolder.getProtectedSessionId();
|
||||
}
|
||||
catch(e) {}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import noteDetailService from "./note_detail.js";
|
||||
import treeUtils from "./tree_utils.js";
|
||||
import linkService from "./link.js";
|
||||
import server from "./server.js";
|
||||
|
||||
function setupTooltip() {
|
||||
$(document).tooltip({
|
||||
items: "#note-detail-text a",
|
||||
items: "body a",
|
||||
content: function (callback) {
|
||||
const notePath = linkService.getNotePathFromLink($(this).attr("href"));
|
||||
const $link = $(this);
|
||||
|
||||
if (notePath !== null) {
|
||||
if ($link.hasClass("no-tooltip-preview")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notePath = linkService.getNotePathFromLink($link.attr("href"));
|
||||
|
||||
if (!notePath) {
|
||||
notePath = $link.attr("data-note-path");
|
||||
}
|
||||
|
||||
if (notePath) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
noteDetailService.loadNote(noteId).then(note => callback(note.content));
|
||||
const notePromise = noteDetailService.loadNote(noteId);
|
||||
const attributePromise = server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
Promise.all([notePromise, attributePromise])
|
||||
.then(([note, attributes]) => renderTooltip(callback, note, attributes));
|
||||
}
|
||||
},
|
||||
close: function (event, ui) {
|
||||
@@ -27,6 +42,62 @@ function setupTooltip() {
|
||||
});
|
||||
}
|
||||
|
||||
async function renderTooltip(callback, note, attributes) {
|
||||
let content = '';
|
||||
const promoted = attributes.filter(attr =>
|
||||
(attr.type === 'label-definition' || attr.type === 'relation-definition')
|
||||
&& !attr.name.startsWith("child:")
|
||||
&& attr.value.isPromoted);
|
||||
|
||||
if (promoted.length > 0) {
|
||||
const $table = $("<table>").addClass("promoted-attributes-in-tooltip");
|
||||
|
||||
for (const definitionAttr of promoted) {
|
||||
const definitionType = definitionAttr.type;
|
||||
const valueType = definitionType.substr(0, definitionType.length - 11);
|
||||
|
||||
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
|
||||
|
||||
for (const valueAttr of valueAttrs) {
|
||||
if (!valueAttr.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let $value = "";
|
||||
|
||||
if (valueType === 'label') {
|
||||
$value = $("<td>").text(valueAttr.value);
|
||||
}
|
||||
else if (valueType === 'relation' && valueAttr.value) {
|
||||
$value = $("<td>").append(await linkService.createNoteLink(valueAttr.value));
|
||||
}
|
||||
|
||||
const $row = $("<tr>")
|
||||
.append($("<th>").text(definitionAttr.name))
|
||||
.append($value);
|
||||
|
||||
$table.append($row);
|
||||
}
|
||||
}
|
||||
|
||||
content += $table.prop('outerHTML');
|
||||
}
|
||||
|
||||
if (note.type === 'text') {
|
||||
content += note.content;
|
||||
}
|
||||
else if (note.type === 'code') {
|
||||
content += $("<pre>").text(note.content).prop('outerHTML');
|
||||
}
|
||||
// other types of notes don't have tooltip preview
|
||||
|
||||
if (!content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(content);
|
||||
}
|
||||
|
||||
export default {
|
||||
setupTooltip
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import treeChangesService from './branches.js';
|
||||
import treeUtils from './tree_utils.js';
|
||||
import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
import recentNotesDialog from '../dialogs/recent_notes.js';
|
||||
import treeCache from './tree_cache.js';
|
||||
import infoService from "./info.js";
|
||||
import treeBuilder from "./tree_builder.js";
|
||||
@@ -101,14 +100,22 @@ async function expandToNote(notePath, expandOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
async function activateNode(notePath) {
|
||||
async function activateNote(notePath, newNote) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const node = await expandToNote(notePath);
|
||||
|
||||
await node.setActive();
|
||||
if (newNote) {
|
||||
noteDetailService.newNoteCreated();
|
||||
}
|
||||
|
||||
// we use noFocus because when we reload the tree because of background changes
|
||||
// we don't want the reload event to steal focus from whatever was focused before
|
||||
await node.setActive(true, { noFocus: true });
|
||||
|
||||
clearSelectedNodes();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +190,7 @@ async function getRunPath(notePath) {
|
||||
return effectivePath.reverse();
|
||||
}
|
||||
|
||||
async function showParentList(noteId, node) {
|
||||
async function showPaths(noteId, node) {
|
||||
utils.assertArguments(noteId, node);
|
||||
|
||||
const note = await treeCache.getNote(noteId);
|
||||
@@ -191,26 +198,25 @@ async function showParentList(noteId, node) {
|
||||
|
||||
$notePathCount.html(parents.length + " path" + (parents.length > 0 ? "s" : ""));
|
||||
|
||||
if (parents.length <= 1) {
|
||||
}
|
||||
else {
|
||||
//$notePathList.show();
|
||||
$notePathList.empty();
|
||||
$notePathList.empty();
|
||||
|
||||
for (const parentNote of parents) {
|
||||
const parentNotePath = await getSomeNotePath(parentNote);
|
||||
// this is to avoid having root notes leading '/'
|
||||
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
|
||||
const title = await treeUtils.getNotePathTitle(notePath);
|
||||
for (const parentNote of parents) {
|
||||
const parentNotePath = await getSomeNotePath(parentNote);
|
||||
// this is to avoid having root notes leading '/'
|
||||
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
|
||||
const title = await treeUtils.getNotePathTitle(notePath);
|
||||
|
||||
const item = $("<li/>").append(await linkService.createNoteLink(notePath, title));
|
||||
const noteLink = await linkService.createNoteLink(notePath, title);
|
||||
|
||||
if (node.getParent().data.noteId === parentNote.noteId) {
|
||||
item.addClass("current");
|
||||
}
|
||||
noteLink.addClass("no-tooltip-preview");
|
||||
|
||||
$notePathList.append(item);
|
||||
const item = $("<li/>").append(noteLink);
|
||||
|
||||
if (node.getParent().data.noteId === parentNote.noteId) {
|
||||
item.addClass("current");
|
||||
}
|
||||
|
||||
$notePathList.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +250,15 @@ async function setExpandedToServer(branchId, isExpanded) {
|
||||
await server.put('branches/' + branchId + '/expanded/' + expandedNum);
|
||||
}
|
||||
|
||||
function addRecentNote(branchId, notePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === getCurrentNotePath()) {
|
||||
await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function setCurrentNotePathToHash(node) {
|
||||
utils.assertArguments(node);
|
||||
|
||||
@@ -252,7 +267,7 @@ function setCurrentNotePathToHash(node) {
|
||||
|
||||
document.location.hash = currentNotePath;
|
||||
|
||||
recentNotesDialog.addRecentNote(currentBranchId, currentNotePath);
|
||||
addRecentNote(currentBranchId, currentNotePath);
|
||||
}
|
||||
|
||||
function getSelectedNodes(stopOnParents = false) {
|
||||
@@ -280,11 +295,11 @@ async function treeInitialized() {
|
||||
}
|
||||
|
||||
if (startNotePath) {
|
||||
await activateNode(startNotePath);
|
||||
const node = await activateNote(startNotePath);
|
||||
|
||||
// looks like this this doesn't work when triggered immediatelly after activating node
|
||||
// so waiting a second helps
|
||||
setTimeout(scrollToCurrentNote, 1000);
|
||||
setTimeout(() => node.makeVisible({scrollIntoView: true}), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +338,7 @@ function initFancyTree(tree) {
|
||||
|
||||
noteDetailService.switchToNote(node.noteId);
|
||||
|
||||
showParentList(node.noteId, data.node);
|
||||
showPaths(node.noteId, data.node);
|
||||
},
|
||||
expand: (event, data) => setExpandedToServer(data.node.data.branchId, true),
|
||||
collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false),
|
||||
@@ -346,6 +361,7 @@ function initFancyTree(tree) {
|
||||
dnd: dragAndDropSetup,
|
||||
lazyLoad: function(event, data) {
|
||||
const noteId = data.node.data.noteId;
|
||||
|
||||
data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
|
||||
},
|
||||
clones: {
|
||||
@@ -403,7 +419,7 @@ function scrollToCurrentNote() {
|
||||
}
|
||||
|
||||
function setBranchBackgroundBasedOnProtectedStatus(noteId) {
|
||||
getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
|
||||
getNodesByNoteId(noteId).map(node => node.toggleClass("protected", node.data.isProtected));
|
||||
}
|
||||
|
||||
function setProtected(noteId, isProtected) {
|
||||
@@ -502,7 +518,13 @@ async function showTree() {
|
||||
initFancyTree(tree);
|
||||
}
|
||||
|
||||
messagingService.subscribeToMessages(syncData => {
|
||||
messagingService.subscribeToMessages(message => {
|
||||
if (message.type === 'refresh-tree') {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
messagingService.subscribeToSyncMessages(syncData => {
|
||||
if (syncData.some(sync => sync.entityName === 'branches')
|
||||
|| syncData.some(sync => sync.entityName === 'notes')) {
|
||||
|
||||
@@ -540,7 +562,7 @@ $(window).bind('hashchange', function() {
|
||||
if (getCurrentNotePath() !== notePath) {
|
||||
console.log("Switching to " + notePath + " because of hash change");
|
||||
|
||||
activateNode(notePath);
|
||||
activateNote(notePath);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -557,7 +579,7 @@ export default {
|
||||
setBranchBackgroundBasedOnProtectedStatus,
|
||||
setProtected,
|
||||
expandToNote,
|
||||
activateNode,
|
||||
activateNote,
|
||||
getCurrentNode,
|
||||
getCurrentNotePath,
|
||||
setCurrentNotePathToHash,
|
||||
|
||||
@@ -74,12 +74,11 @@ async function prepareRealBranch(parentNote) {
|
||||
|
||||
async function prepareSearchBranch(note) {
|
||||
const fullNote = await noteDetailService.loadNote(note.noteId);
|
||||
const results = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString));
|
||||
|
||||
const noteIds = results.map(res => res.noteId);
|
||||
const results = (await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString)))
|
||||
.filter(res => res.noteId !== note.noteId); // this is necessary because title of the search note is often the same as the search text which would match and create circle
|
||||
|
||||
// force to load all the notes at once instead of one by one
|
||||
await treeCache.getNotes(noteIds);
|
||||
await treeCache.getNotes(results.map(res => res.noteId));
|
||||
|
||||
for (const result of results) {
|
||||
const origBranch = await treeCache.getBranch(result.branchId);
|
||||
@@ -115,6 +114,10 @@ async function getExtraClasses(note) {
|
||||
extraClasses.push("multiple-parents");
|
||||
}
|
||||
|
||||
if (note.cssClass) {
|
||||
extraClasses.push(note.cssClass);
|
||||
}
|
||||
|
||||
extraClasses.push(note.type);
|
||||
|
||||
return extraClasses.join(" ");
|
||||
|
||||
@@ -2,10 +2,21 @@ import utils from "./utils.js";
|
||||
import Branch from "../entities/branch.js";
|
||||
import NoteShort from "../entities/note_short.js";
|
||||
import infoService from "./info.js";
|
||||
import messagingService from "./messaging.js";
|
||||
import server from "./server.js";
|
||||
|
||||
class TreeCache {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
load(noteRows, branchRows, relations) {
|
||||
this.init();
|
||||
|
||||
this.addResp(noteRows, branchRows, relations);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.parents = {};
|
||||
this.children = {};
|
||||
this.childParentToBranch = {};
|
||||
@@ -15,8 +26,6 @@ class TreeCache {
|
||||
|
||||
/** @type {Object.<string, Branch>} */
|
||||
this.branches = {};
|
||||
|
||||
this.addResp(noteRows, branchRows, relations);
|
||||
}
|
||||
|
||||
addResp(noteRows, branchRows, relations) {
|
||||
@@ -37,7 +46,7 @@ class TreeCache {
|
||||
}
|
||||
}
|
||||
|
||||
async getNotes(noteIds) {
|
||||
async getNotes(noteIds, silentNotFoundError = false) {
|
||||
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
|
||||
|
||||
if (missingNoteIds.length > 0) {
|
||||
@@ -47,13 +56,15 @@ class TreeCache {
|
||||
}
|
||||
|
||||
return noteIds.map(noteId => {
|
||||
if (!this.notes[noteId]) {
|
||||
throw new Error(`Can't find note ${noteId}`);
|
||||
if (!this.notes[noteId] && !silentNotFoundError) {
|
||||
messagingService.logError(`Can't find note ${noteId}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
});
|
||||
}).filter(note => note !== null);
|
||||
}
|
||||
|
||||
/** @return NoteShort */
|
||||
|
||||
@@ -42,7 +42,7 @@ const keyBindings = {
|
||||
},
|
||||
"shift+up": node => {
|
||||
node.navigate($.ui.keyCode.UP, true).then(() => {
|
||||
const currentNode = getCurrentNode();
|
||||
const currentNode = treeService.getCurrentNode();
|
||||
|
||||
if (currentNode.isSelected()) {
|
||||
node.setSelected(false);
|
||||
|
||||
@@ -34,7 +34,12 @@ function getNotePath(node) {
|
||||
async function getNoteTitle(noteId, parentNoteId = null) {
|
||||
utils.assertArguments(noteId);
|
||||
|
||||
let {title} = await treeCache.getNote(noteId);
|
||||
const note = await treeCache.getNote(noteId);
|
||||
if (!note) {
|
||||
return "[not found]";
|
||||
}
|
||||
|
||||
let {title} = note;
|
||||
|
||||
if (parentNoteId !== null) {
|
||||
const branch = await treeCache.getBranchByChildParent(noteId, parentNoteId);
|
||||
|
||||
@@ -24,7 +24,10 @@ function formatTimeWithSeconds(date) {
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
|
||||
// return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
|
||||
// instead of european format we'll just use ISO as that's pretty unambiguous
|
||||
|
||||
return formatDateISO(date);
|
||||
}
|
||||
|
||||
function formatDateISO(date) {
|
||||
@@ -46,7 +49,7 @@ function isElectron() {
|
||||
function assertArguments() {
|
||||
for (const i in arguments) {
|
||||
if (!arguments[i]) {
|
||||
throw new Error(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||
console.trace(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,15 @@ import server from "./server.js";
|
||||
import utils from "./utils.js";
|
||||
import optionsInitService from "./options_init.js";
|
||||
|
||||
function decreaseZoomFactor() {
|
||||
const webFrame = require('electron').webFrame;
|
||||
const MIN_ZOOM = 0.5;
|
||||
const MAX_ZOOM = 2.0;
|
||||
|
||||
if (webFrame.getZoomFactor() > 0.2) {
|
||||
const webFrame = require('electron').webFrame;
|
||||
const newZoomFactor = webFrame.getZoomFactor() - 0.1;
|
||||
|
||||
webFrame.setZoomFactor(newZoomFactor);
|
||||
|
||||
server.put('options/zoomFactor/' + newZoomFactor);
|
||||
}
|
||||
async function decreaseZoomFactor() {
|
||||
await setZoomFactorAndSave(getCurrentZoom() - 0.1);
|
||||
}
|
||||
|
||||
function increaseZoomFactor() {
|
||||
const webFrame = require('electron').webFrame;
|
||||
const newZoomFactor = webFrame.getZoomFactor() + 0.1;
|
||||
|
||||
webFrame.setZoomFactor(newZoomFactor);
|
||||
|
||||
server.put('options/zoomFactor/' + newZoomFactor);
|
||||
async function increaseZoomFactor() {
|
||||
await setZoomFactorAndSave(getCurrentZoom() + 0.1);
|
||||
}
|
||||
|
||||
function setZoomFactor(zoomFactor) {
|
||||
@@ -31,6 +20,25 @@ function setZoomFactor(zoomFactor) {
|
||||
webFrame.setZoomFactor(zoomFactor);
|
||||
}
|
||||
|
||||
async function setZoomFactorAndSave(zoomFactor) {
|
||||
if (!utils.isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (zoomFactor >= MIN_ZOOM && zoomFactor <= MAX_ZOOM) {
|
||||
setZoomFactor(zoomFactor);
|
||||
|
||||
await server.put('options/zoomFactor/' + zoomFactor);
|
||||
}
|
||||
else {
|
||||
console.log(`Zoom factor ${zoomFactor} outside of the range, ignored.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentZoom() {
|
||||
return require('electron').webFrame.getZoomFactor();
|
||||
}
|
||||
|
||||
if (utils.isElectron()) {
|
||||
optionsInitService.optionsReady.then(options => setZoomFactor(options.zoomFactor))
|
||||
}
|
||||
@@ -38,5 +46,6 @@ if (utils.isElectron()) {
|
||||
export default {
|
||||
decreaseZoomFactor,
|
||||
increaseZoomFactor,
|
||||
setZoomFactor
|
||||
setZoomFactor,
|
||||
setZoomFactorAndSave
|
||||
}
|
||||
@@ -1,36 +1,141 @@
|
||||
import server from './services/server.js';
|
||||
import utils from "./services/utils.js";
|
||||
|
||||
$("#setup-form").submit(() => {
|
||||
const username = $("#username").val();
|
||||
const password1 = $("#password1").val();
|
||||
const password2 = $("#password2").val();
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return false;
|
||||
function SetupModel() {
|
||||
if (syncInProgress) {
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
}
|
||||
|
||||
if (!password1) {
|
||||
showAlert("Password can't be empty");
|
||||
return false;
|
||||
}
|
||||
const serverAddress = location.protocol + '//' + location.host;
|
||||
|
||||
if (password1 !== password2) {
|
||||
showAlert("Both password fields need be identical.");
|
||||
return false;
|
||||
}
|
||||
$("#current-host").html(serverAddress);
|
||||
|
||||
server.post('setup', {
|
||||
username: username,
|
||||
password: password1
|
||||
}).then(() => {
|
||||
this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type");
|
||||
this.setupType = ko.observable();
|
||||
|
||||
this.setupNewDocument = ko.observable(false);
|
||||
this.setupSyncFromDesktop = ko.observable(false);
|
||||
this.setupSyncFromServer = ko.observable(false);
|
||||
|
||||
this.username = ko.observable();
|
||||
this.password1 = ko.observable();
|
||||
this.password2 = ko.observable();
|
||||
|
||||
this.syncServerHost = ko.observable();
|
||||
this.syncProxy = ko.observable();
|
||||
|
||||
this.instanceType = utils.isElectron() ? "desktop" : "server";
|
||||
|
||||
this.setupTypeSelected = this.getSetupType = () =>
|
||||
this.setupNewDocument()
|
||||
|| this.setupSyncFromDesktop()
|
||||
|| this.setupSyncFromServer();
|
||||
|
||||
this.selectSetupType = () => {
|
||||
this.step(this.getSetupType());
|
||||
this.setupType(this.getSetupType());
|
||||
};
|
||||
|
||||
this.back = () => {
|
||||
this.step("setup-type");
|
||||
|
||||
this.setupNewDocument(false);
|
||||
this.setupSyncFromServer(false);
|
||||
this.setupSyncFromDesktop(false);
|
||||
};
|
||||
|
||||
this.finish = async () => {
|
||||
if (this.setupNewDocument()) {
|
||||
const username = this.username();
|
||||
const password1 = this.password1();
|
||||
const password2 = this.password2();
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password1) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password1 !== password2) {
|
||||
showAlert("Both password fields need be identical.");
|
||||
return;
|
||||
}
|
||||
|
||||
// not using server.js because it loads too many dependencies
|
||||
$.post('/api/setup/new-document', {
|
||||
username: username,
|
||||
password: password1
|
||||
}).then(() => {
|
||||
window.location.replace("/");
|
||||
});
|
||||
}
|
||||
else if (this.setupSyncFromServer()) {
|
||||
const syncServerHost = this.syncServerHost();
|
||||
const syncProxy = this.syncProxy();
|
||||
const username = this.username();
|
||||
const password = this.password1();
|
||||
|
||||
if (!syncServerHost) {
|
||||
showAlert("Trilium server address can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// not using server.js because it loads too many dependencies
|
||||
const resp = await $.post('/api/setup/sync-from-server', {
|
||||
syncServerHost: syncServerHost,
|
||||
syncProxy: syncProxy,
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (resp.result === 'success') {
|
||||
this.step('sync-in-progress');
|
||||
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
|
||||
hideAlert();
|
||||
}
|
||||
else {
|
||||
showAlert('Sync setup failed: ' + resp.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function checkOutstandingSyncs() {
|
||||
const { stats, initialized } = await $.get('/api/sync/stats');
|
||||
|
||||
if (initialized) {
|
||||
window.location.replace("/");
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
|
||||
|
||||
$("#outstanding-syncs").html(totalOutstandingSyncs);
|
||||
}
|
||||
|
||||
function showAlert(message) {
|
||||
$("#alert").html(message);
|
||||
$("#alert").show();
|
||||
}
|
||||
}
|
||||
|
||||
function hideAlert() {
|
||||
$("#alert").hide();
|
||||
}
|
||||
|
||||
ko.applyBindings(new SetupModel(), document.getElementById('setup-dialog'));
|
||||
|
||||
$("#setup-dialog").show();
|
||||
6
src/public/libraries/ckeditor/ckeditor.js
vendored
6
src/public/libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -137,12 +137,14 @@
|
||||
CodeMirror.registerHelper("fold", "xml", function(cm, start) {
|
||||
var iter = new Iter(cm, start.line, 0);
|
||||
for (;;) {
|
||||
var openTag = toNextTag(iter), end;
|
||||
if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return;
|
||||
var openTag = toNextTag(iter)
|
||||
if (!openTag || iter.line != start.line) return
|
||||
var end = toTagEnd(iter)
|
||||
if (!end) return
|
||||
if (!openTag[1] && end != "selfClose") {
|
||||
var startPos = Pos(iter.line, iter.ch);
|
||||
var endPos = findMatchingClose(iter, openTag[2]);
|
||||
return endPos && {from: startPos, to: endPos.from};
|
||||
return endPos && cmp(endPos.from, startPos) > 0 ? {from: startPos, to: endPos.from} : null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
var state = cm.state.matchHighlighter;
|
||||
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
|
||||
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
|
||||
var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query;
|
||||
var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query;
|
||||
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
|
||||
{className: "CodeMirror-selection-highlight-scrollbar"});
|
||||
}
|
||||
|
||||
@@ -746,6 +746,16 @@ function collapsedSpanAtSide(line, start) {
|
||||
function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) }
|
||||
function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }
|
||||
|
||||
function collapsedSpanAround(line, ch) {
|
||||
var sps = sawCollapsedSpans && line.markedSpans, found
|
||||
if (sps) { for (var i = 0; i < sps.length; ++i) {
|
||||
var sp = sps[i]
|
||||
if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) &&
|
||||
(!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker }
|
||||
} }
|
||||
return found
|
||||
}
|
||||
|
||||
// Test whether there exists a collapsed span that partially
|
||||
// overlaps (covers the start or end, but not both) of a new span.
|
||||
// Such overlap is not allowed.
|
||||
@@ -2778,12 +2788,11 @@ function coordsChar(cm, x, y) {
|
||||
var lineObj = getLine(doc, lineN)
|
||||
for (;;) {
|
||||
var found = coordsCharInner(cm, lineObj, lineN, x, y)
|
||||
var merged = collapsedSpanAtEnd(lineObj)
|
||||
var mergedPos = merged && merged.find(0, true)
|
||||
if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
|
||||
{ lineN = lineNo(lineObj = mergedPos.to.line) }
|
||||
else
|
||||
{ return found }
|
||||
var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0))
|
||||
if (!collapsed) { return found }
|
||||
var rangeEnd = collapsed.find(1)
|
||||
if (rangeEnd.line == lineN) { return rangeEnd }
|
||||
lineObj = getLine(doc, lineN = rangeEnd.line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3543,6 +3552,7 @@ var NativeScrollbars = function(place, scroll, cm) {
|
||||
this.cm = cm
|
||||
var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar")
|
||||
var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar")
|
||||
vert.tabIndex = horiz.tabIndex = -1
|
||||
place(vert); place(horiz)
|
||||
|
||||
on(vert, "scroll", function () {
|
||||
@@ -4783,7 +4793,7 @@ function addChangeToHistory(doc, change, selAfter, opId) {
|
||||
|
||||
if ((hist.lastOp == opId ||
|
||||
hist.lastOrigin == change.origin && change.origin &&
|
||||
((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) ||
|
||||
((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) ||
|
||||
change.origin.charAt(0) == "*")) &&
|
||||
(cur = lastChangeEvent(hist, hist.lastOp == opId))) {
|
||||
// Merge this change into the last event
|
||||
@@ -5684,7 +5694,7 @@ LineWidget.prototype.changed = function () {
|
||||
this.height = null
|
||||
var diff = widgetHeight(this) - oldH
|
||||
if (!diff) { return }
|
||||
updateLineHeight(line, line.height + diff)
|
||||
if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff) }
|
||||
if (cm) {
|
||||
runInOp(cm, function () {
|
||||
cm.curOp.forceUpdate = true
|
||||
@@ -6567,8 +6577,6 @@ function registerGlobalHandlers() {
|
||||
// Called when the window resizes
|
||||
function onResize(cm) {
|
||||
var d = cm.display
|
||||
if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
|
||||
{ return }
|
||||
// Might be a text scaling operation, clear size caches.
|
||||
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null
|
||||
d.scrollbarsClipped = false
|
||||
@@ -6614,7 +6622,7 @@ keyMap.pcDefault = {
|
||||
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
|
||||
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
|
||||
"Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
|
||||
fallthrough: "basic"
|
||||
"fallthrough": "basic"
|
||||
}
|
||||
// Very basic readline/emacs-style bindings, which are standard on Mac.
|
||||
keyMap.emacsy = {
|
||||
@@ -6632,7 +6640,7 @@ keyMap.macDefault = {
|
||||
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
|
||||
"Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
|
||||
"Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
|
||||
fallthrough: ["basic", "emacsy"]
|
||||
"fallthrough": ["basic", "emacsy"]
|
||||
}
|
||||
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault
|
||||
|
||||
@@ -7312,8 +7320,8 @@ function leftButtonStartDrag(cm, event, pos, behavior) {
|
||||
var dragEnd = operation(cm, function (e) {
|
||||
if (webkit) { display.scroller.draggable = false }
|
||||
cm.state.draggingText = false
|
||||
off(document, "mouseup", dragEnd)
|
||||
off(document, "mousemove", mouseMove)
|
||||
off(display.wrapper.ownerDocument, "mouseup", dragEnd)
|
||||
off(display.wrapper.ownerDocument, "mousemove", mouseMove)
|
||||
off(display.scroller, "dragstart", dragStart)
|
||||
off(display.scroller, "drop", dragEnd)
|
||||
if (!moved) {
|
||||
@@ -7322,7 +7330,7 @@ function leftButtonStartDrag(cm, event, pos, behavior) {
|
||||
{ extendSelection(cm.doc, pos, null, null, behavior.extend) }
|
||||
// Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
|
||||
if (webkit || ie && ie_version == 9)
|
||||
{ setTimeout(function () {document.body.focus(); display.input.focus()}, 20) }
|
||||
{ setTimeout(function () {display.wrapper.ownerDocument.body.focus(); display.input.focus()}, 20) }
|
||||
else
|
||||
{ display.input.focus() }
|
||||
}
|
||||
@@ -7337,8 +7345,8 @@ function leftButtonStartDrag(cm, event, pos, behavior) {
|
||||
dragEnd.copy = !behavior.moveOnDrag
|
||||
// IE's approach to draggable
|
||||
if (display.scroller.dragDrop) { display.scroller.dragDrop() }
|
||||
on(document, "mouseup", dragEnd)
|
||||
on(document, "mousemove", mouseMove)
|
||||
on(display.wrapper.ownerDocument, "mouseup", dragEnd)
|
||||
on(display.wrapper.ownerDocument, "mousemove", mouseMove)
|
||||
on(display.scroller, "dragstart", dragStart)
|
||||
on(display.scroller, "drop", dragEnd)
|
||||
|
||||
@@ -7470,19 +7478,19 @@ function leftButtonSelect(cm, event, start, behavior) {
|
||||
counter = Infinity
|
||||
e_preventDefault(e)
|
||||
display.input.focus()
|
||||
off(document, "mousemove", move)
|
||||
off(document, "mouseup", up)
|
||||
off(display.wrapper.ownerDocument, "mousemove", move)
|
||||
off(display.wrapper.ownerDocument, "mouseup", up)
|
||||
doc.history.lastSelOrigin = null
|
||||
}
|
||||
|
||||
var move = operation(cm, function (e) {
|
||||
if (!e_button(e)) { done(e) }
|
||||
if (e.buttons === 0 || !e_button(e)) { done(e) }
|
||||
else { extend(e) }
|
||||
})
|
||||
var up = operation(cm, done)
|
||||
cm.state.selectingText = up
|
||||
on(document, "mousemove", move)
|
||||
on(document, "mouseup", up)
|
||||
on(display.wrapper.ownerDocument, "mousemove", move)
|
||||
on(display.wrapper.ownerDocument, "mouseup", up)
|
||||
}
|
||||
|
||||
// Used when mouse-selecting to adjust the anchor to the proper side
|
||||
@@ -7765,6 +7773,7 @@ function CodeMirror(place, options) {
|
||||
|
||||
var doc = options.value
|
||||
if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction) }
|
||||
else if (options.mode) { doc.modeOption = options.mode }
|
||||
this.doc = doc
|
||||
|
||||
var input = new CodeMirror.inputStyles[options.inputStyle](this)
|
||||
@@ -8755,8 +8764,12 @@ ContentEditableInput.prototype.showSelection = function (info, takeFocus) {
|
||||
this.showMultipleSelections(info)
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.getSelection = function () {
|
||||
return this.cm.display.wrapper.ownerDocument.getSelection()
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.showPrimarySelection = function () {
|
||||
var sel = window.getSelection(), cm = this.cm, prim = cm.doc.sel.primary()
|
||||
var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary()
|
||||
var from = prim.from(), to = prim.to()
|
||||
|
||||
if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) {
|
||||
@@ -8823,13 +8836,13 @@ ContentEditableInput.prototype.showMultipleSelections = function (info) {
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.rememberSelection = function () {
|
||||
var sel = window.getSelection()
|
||||
var sel = this.getSelection()
|
||||
this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
|
||||
this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.selectionInEditor = function () {
|
||||
var sel = window.getSelection()
|
||||
var sel = this.getSelection()
|
||||
if (!sel.rangeCount) { return false }
|
||||
var node = sel.getRangeAt(0).commonAncestorContainer
|
||||
return contains(this.div, node)
|
||||
@@ -8864,14 +8877,14 @@ ContentEditableInput.prototype.receivedFocus = function () {
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.selectionChanged = function () {
|
||||
var sel = window.getSelection()
|
||||
var sel = this.getSelection()
|
||||
return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
|
||||
sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.pollSelection = function () {
|
||||
if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return }
|
||||
var sel = window.getSelection(), cm = this.cm
|
||||
var sel = this.getSelection(), cm = this.cm
|
||||
// On Android Chrome (version 56, at least), backspacing into an
|
||||
// uneditable block element will put the cursor in that element,
|
||||
// and then, because it's not editable, hide the virtual keyboard.
|
||||
@@ -9005,7 +9018,7 @@ ContentEditableInput.prototype.setUneditable = function (node) {
|
||||
};
|
||||
|
||||
ContentEditableInput.prototype.onKeyPress = function (e) {
|
||||
if (e.charCode == 0) { return }
|
||||
if (e.charCode == 0 || this.composing) { return }
|
||||
e.preventDefault()
|
||||
if (!this.cm.isReadOnly())
|
||||
{ operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) }
|
||||
@@ -9045,12 +9058,13 @@ function isInGutter(node) {
|
||||
function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos }
|
||||
|
||||
function domTextBetween(cm, from, to, fromLine, toLine) {
|
||||
var text = "", closing = false, lineSep = cm.doc.lineSeparator()
|
||||
var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false
|
||||
function recognizeMarker(id) { return function (marker) { return marker.id == id; } }
|
||||
function close() {
|
||||
if (closing) {
|
||||
text += lineSep
|
||||
closing = false
|
||||
if (extraLinebreak) { text += lineSep }
|
||||
closing = extraLinebreak = false
|
||||
}
|
||||
}
|
||||
function addText(str) {
|
||||
@@ -9062,8 +9076,8 @@ function domTextBetween(cm, from, to, fromLine, toLine) {
|
||||
function walk(node) {
|
||||
if (node.nodeType == 1) {
|
||||
var cmText = node.getAttribute("cm-text")
|
||||
if (cmText != null) {
|
||||
addText(cmText || node.textContent.replace(/\u200b/g, ""))
|
||||
if (cmText) {
|
||||
addText(cmText)
|
||||
return
|
||||
}
|
||||
var markerID = node.getAttribute("cm-marker"), range
|
||||
@@ -9074,19 +9088,24 @@ function domTextBetween(cm, from, to, fromLine, toLine) {
|
||||
return
|
||||
}
|
||||
if (node.getAttribute("contenteditable") == "false") { return }
|
||||
var isBlock = /^(pre|div|p)$/i.test(node.nodeName)
|
||||
var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName)
|
||||
if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return }
|
||||
|
||||
if (isBlock) { close() }
|
||||
for (var i = 0; i < node.childNodes.length; i++)
|
||||
{ walk(node.childNodes[i]) }
|
||||
|
||||
if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true }
|
||||
if (isBlock) { closing = true }
|
||||
} else if (node.nodeType == 3) {
|
||||
addText(node.nodeValue)
|
||||
addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " "))
|
||||
}
|
||||
}
|
||||
for (;;) {
|
||||
walk(from)
|
||||
if (from == to) { break }
|
||||
from = from.nextSibling
|
||||
extraLinebreak = false
|
||||
}
|
||||
return text
|
||||
}
|
||||
@@ -9187,13 +9206,10 @@ TextareaInput.prototype.init = function (display) {
|
||||
var this$1 = this;
|
||||
|
||||
var input = this, cm = this.cm
|
||||
this.createField(display)
|
||||
var te = this.textarea
|
||||
|
||||
// Wraps and hides input textarea
|
||||
var div = this.wrapper = hiddenTextarea()
|
||||
// The semihidden textarea that is focused when the editor is
|
||||
// focused, and receives input.
|
||||
var te = this.textarea = div.firstChild
|
||||
display.wrapper.insertBefore(div, display.wrapper.firstChild)
|
||||
display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild)
|
||||
|
||||
// Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
|
||||
if (ios) { te.style.width = "0px" }
|
||||
@@ -9260,6 +9276,14 @@ TextareaInput.prototype.init = function (display) {
|
||||
})
|
||||
};
|
||||
|
||||
TextareaInput.prototype.createField = function (_display) {
|
||||
// Wraps and hides input textarea
|
||||
this.wrapper = hiddenTextarea()
|
||||
// The semihidden textarea that is focused when the editor is
|
||||
// focused, and receives input.
|
||||
this.textarea = this.wrapper.firstChild
|
||||
};
|
||||
|
||||
TextareaInput.prototype.prepareSelection = function () {
|
||||
// Redraw the selection and/or cursor
|
||||
var cm = this.cm, display = cm.display, doc = cm.doc
|
||||
@@ -9653,7 +9677,7 @@ CodeMirror.fromTextArea = fromTextArea
|
||||
|
||||
addLegacyProps(CodeMirror)
|
||||
|
||||
CodeMirror.version = "5.35.0"
|
||||
CodeMirror.version = "5.39.2"
|
||||
|
||||
return CodeMirror;
|
||||
|
||||
|
||||
@@ -216,15 +216,15 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
|
||||
indent: function(state, textAfter) {
|
||||
if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
|
||||
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
|
||||
var closing = firstChar == ctx.type;
|
||||
if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
|
||||
if (parserConfig.dontIndentStatements)
|
||||
while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
|
||||
ctx = ctx.prev
|
||||
if (hooks.indent) {
|
||||
var hook = hooks.indent(state, ctx, textAfter);
|
||||
var hook = hooks.indent(state, ctx, textAfter, indentUnit);
|
||||
if (typeof hook == "number") return hook
|
||||
}
|
||||
var closing = firstChar == ctx.type;
|
||||
var switchBlock = ctx.prev && ctx.prev.info == "switch";
|
||||
if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
|
||||
while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
|
||||
@@ -374,7 +374,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
|
||||
blockKeywords: words("case do else for if switch while struct"),
|
||||
defKeywords: words("struct"),
|
||||
typeFirstDefinitions: true,
|
||||
atoms: words("null true false"),
|
||||
atoms: words("NULL true false"),
|
||||
hooks: {"#": cppHook, "*": pointerHook},
|
||||
modeProps: {fold: ["brace", "include"]}
|
||||
});
|
||||
@@ -390,7 +390,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
|
||||
blockKeywords: words("catch class do else finally for if struct switch try while"),
|
||||
defKeywords: words("class namespace struct enum union"),
|
||||
typeFirstDefinitions: true,
|
||||
atoms: words("true false null"),
|
||||
atoms: words("true false NULL"),
|
||||
dontIndentStatements: /^template$/,
|
||||
isIdentifierChar: /[\w\$_~\xa1-\uffff]/,
|
||||
hooks: {
|
||||
@@ -597,34 +597,51 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
|
||||
name: "clike",
|
||||
keywords: words(
|
||||
/*keywords*/
|
||||
"package as typealias class interface this super val " +
|
||||
"var fun for is in This throw return " +
|
||||
"package as typealias class interface this super val operator " +
|
||||
"var fun for is in This throw return annotation " +
|
||||
"break continue object if else while do try when !in !is as? " +
|
||||
|
||||
/*soft keywords*/
|
||||
"file import where by get set abstract enum open inner override private public internal " +
|
||||
"protected catch finally out final vararg reified dynamic companion constructor init " +
|
||||
"sealed field property receiver param sparam lateinit data inline noinline tailrec " +
|
||||
"external annotation crossinline const operator infix suspend actual expect"
|
||||
"external annotation crossinline const operator infix suspend actual expect setparam"
|
||||
),
|
||||
types: words(
|
||||
/* package java.lang */
|
||||
"Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
|
||||
"Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
|
||||
"Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
|
||||
"StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
|
||||
"StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void Annotation Any BooleanArray " +
|
||||
"ByteArray Char CharArray DeprecationLevel DoubleArray Enum FloatArray Function Int IntArray Lazy " +
|
||||
"LazyThreadSafetyMode LongArray Nothing ShortArray Unit"
|
||||
),
|
||||
intendSwitch: false,
|
||||
indentStatements: false,
|
||||
multiLineStrings: true,
|
||||
number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
|
||||
number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+(\.\d+)?|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
|
||||
blockKeywords: words("catch class do else finally for if where try while enum"),
|
||||
defKeywords: words("class val var object interface fun"),
|
||||
atoms: words("true false null this"),
|
||||
hooks: {
|
||||
"@": function(stream) {
|
||||
stream.eatWhile(/[\w\$_]/);
|
||||
return "meta";
|
||||
},
|
||||
'"': function(stream, state) {
|
||||
state.tokenize = tokenKotlinString(stream.match('""'));
|
||||
return state.tokenize(stream, state);
|
||||
},
|
||||
indent: function(state, ctx, textAfter, indentUnit) {
|
||||
var firstChar = textAfter && textAfter.charAt(0);
|
||||
if ((state.prevToken == "}" || state.prevToken == ")") && textAfter == "")
|
||||
return state.indented;
|
||||
if (state.prevToken == "operator" && textAfter != "}" ||
|
||||
state.prevToken == "variable" && firstChar == "." ||
|
||||
(state.prevToken == "}" || state.prevToken == ")") && firstChar == ".")
|
||||
return indentUnit * 2 + ctx.indented;
|
||||
if (ctx.align && ctx.type == "}")
|
||||
return ctx.indented + (state.context.type == (textAfter || "").charAt(0) ? 0 : indentUnit);
|
||||
}
|
||||
},
|
||||
modeProps: {closeBrackets: {triples: '"'}}
|
||||
|
||||
@@ -11,30 +11,64 @@
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var from = "from";
|
||||
var fromRegex = new RegExp("^(\\s*)\\b(" + from + ")\\b", "i");
|
||||
|
||||
var shells = ["run", "cmd", "entrypoint", "shell"];
|
||||
var shellsAsArrayRegex = new RegExp("^(\\s*)(" + shells.join('|') + ")(\\s+\\[)", "i");
|
||||
|
||||
var expose = "expose";
|
||||
var exposeRegex = new RegExp("^(\\s*)(" + expose + ")(\\s+)", "i");
|
||||
|
||||
var others = [
|
||||
"arg", "from", "maintainer", "label", "env",
|
||||
"add", "copy", "volume", "user",
|
||||
"workdir", "onbuild", "stopsignal", "healthcheck", "shell"
|
||||
];
|
||||
|
||||
// Collect all Dockerfile directives
|
||||
var instructions = ["from", "maintainer", "run", "cmd", "expose", "env",
|
||||
"add", "copy", "entrypoint", "volume", "user",
|
||||
"workdir", "onbuild"],
|
||||
var instructions = [from, expose].concat(shells).concat(others),
|
||||
instructionRegex = "(" + instructions.join('|') + ")",
|
||||
instructionOnlyLine = new RegExp(instructionRegex + "\\s*$", "i"),
|
||||
instructionWithArguments = new RegExp(instructionRegex + "(\\s+)", "i");
|
||||
instructionOnlyLine = new RegExp("^(\\s*)" + instructionRegex + "(\\s*)(#.*)?$", "i"),
|
||||
instructionWithArguments = new RegExp("^(\\s*)" + instructionRegex + "(\\s+)", "i");
|
||||
|
||||
CodeMirror.defineSimpleMode("dockerfile", {
|
||||
start: [
|
||||
// Block comment: This is a line starting with a comment
|
||||
{
|
||||
regex: /#.*$/,
|
||||
regex: /^\s*#.*$/,
|
||||
sol: true,
|
||||
token: "comment"
|
||||
},
|
||||
{
|
||||
regex: fromRegex,
|
||||
token: [null, "keyword"],
|
||||
sol: true,
|
||||
next: "from"
|
||||
},
|
||||
// Highlight an instruction without any arguments (for convenience)
|
||||
{
|
||||
regex: instructionOnlyLine,
|
||||
token: "variable-2"
|
||||
token: [null, "keyword", null, "error"],
|
||||
sol: true
|
||||
},
|
||||
{
|
||||
regex: shellsAsArrayRegex,
|
||||
token: [null, "keyword", null],
|
||||
sol: true,
|
||||
next: "array"
|
||||
},
|
||||
{
|
||||
regex: exposeRegex,
|
||||
token: [null, "keyword", null],
|
||||
sol: true,
|
||||
next: "expose"
|
||||
},
|
||||
// Highlight an instruction followed by arguments
|
||||
{
|
||||
regex: instructionWithArguments,
|
||||
token: ["variable-2", null],
|
||||
token: [null, "keyword", null],
|
||||
sol: true,
|
||||
next: "arguments"
|
||||
},
|
||||
{
|
||||
@@ -42,26 +76,21 @@
|
||||
token: null
|
||||
}
|
||||
],
|
||||
arguments: [
|
||||
from: [
|
||||
{
|
||||
regex: /\s*$/,
|
||||
token: null,
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
// Line comment without instruction arguments is an error
|
||||
regex: /#.*$/,
|
||||
token: "error",
|
||||
regex: /(\s*)(#.*)$/,
|
||||
token: [null, "error"],
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /[^#]+\\$/,
|
||||
token: null
|
||||
},
|
||||
{
|
||||
// Match everything except for the inline comment
|
||||
regex: /[^#]+/,
|
||||
token: null,
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /$/,
|
||||
token: null,
|
||||
regex: /(\s*\S+\s+)(as)/i,
|
||||
token: [null, "keyword"],
|
||||
next: "start"
|
||||
},
|
||||
// Fail safe return to start
|
||||
@@ -70,9 +99,112 @@
|
||||
next: "start"
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
lineComment: "#"
|
||||
single: [
|
||||
{
|
||||
regex: /(?:[^\\']|\\.)/,
|
||||
token: "string"
|
||||
},
|
||||
{
|
||||
regex: /'/,
|
||||
token: "string",
|
||||
pop: true
|
||||
}
|
||||
],
|
||||
double: [
|
||||
{
|
||||
regex: /(?:[^\\"]|\\.)/,
|
||||
token: "string"
|
||||
},
|
||||
{
|
||||
regex: /"/,
|
||||
token: "string",
|
||||
pop: true
|
||||
}
|
||||
],
|
||||
array: [
|
||||
{
|
||||
regex: /\]/,
|
||||
token: null,
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /"(?:[^\\"]|\\.)*"?/,
|
||||
token: "string"
|
||||
}
|
||||
],
|
||||
expose: [
|
||||
{
|
||||
regex: /\d+$/,
|
||||
token: "number",
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /[^\d]+$/,
|
||||
token: null,
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /\d+/,
|
||||
token: "number"
|
||||
},
|
||||
{
|
||||
regex: /[^\d]+/,
|
||||
token: null
|
||||
},
|
||||
// Fail safe return to start
|
||||
{
|
||||
token: null,
|
||||
next: "start"
|
||||
}
|
||||
],
|
||||
arguments: [
|
||||
{
|
||||
regex: /^\s*#.*$/,
|
||||
sol: true,
|
||||
token: "comment"
|
||||
},
|
||||
{
|
||||
regex: /"(?:[^\\"]|\\.)*"?$/,
|
||||
token: "string",
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /"/,
|
||||
token: "string",
|
||||
push: "double"
|
||||
},
|
||||
{
|
||||
regex: /'(?:[^\\']|\\.)*'?$/,
|
||||
token: "string",
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /'/,
|
||||
token: "string",
|
||||
push: "single"
|
||||
},
|
||||
{
|
||||
regex: /[^#"']+[\\`]$/,
|
||||
token: null
|
||||
},
|
||||
{
|
||||
regex: /[^#"']+$/,
|
||||
token: null,
|
||||
next: "start"
|
||||
},
|
||||
{
|
||||
regex: /[^#"']+/,
|
||||
token: null
|
||||
},
|
||||
// Fail safe return to start
|
||||
{
|
||||
token: null,
|
||||
next: "start"
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
lineComment: "#"
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-dockerfile", "dockerfile");
|
||||
|
||||
128
src/public/libraries/codemirror/mode/dockerfile/test.js
vendored
Normal file
128
src/public/libraries/codemirror/mode/dockerfile/test.js
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function() {
|
||||
var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-dockerfile");
|
||||
function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
|
||||
|
||||
MT("simple_nodejs_dockerfile",
|
||||
"[keyword FROM] node:carbon",
|
||||
"[comment # Create app directory]",
|
||||
"[keyword WORKDIR] /usr/src/app",
|
||||
"[comment # Install app dependencies]",
|
||||
"[comment # A wildcard is used to ensure both package.json AND package-lock.json are copied]",
|
||||
"[comment # where available (npm@5+)]",
|
||||
"[keyword COPY] package*.json ./",
|
||||
"[keyword RUN] npm install",
|
||||
"[keyword COPY] . .",
|
||||
"[keyword EXPOSE] [number 8080] [number 3000]",
|
||||
"[keyword ENV] NODE_ENV development",
|
||||
"[keyword CMD] [[ [string \"npm\"], [string \"start\"] ]]");
|
||||
|
||||
// Ideally the last space should not be highlighted.
|
||||
MT("instruction_without_args_1",
|
||||
"[keyword CMD] ");
|
||||
|
||||
MT("instruction_without_args_2",
|
||||
"[comment # An instruction without args...]",
|
||||
"[keyword ARG] [error #...is an error]");
|
||||
|
||||
MT("multiline",
|
||||
"[keyword RUN] apt-get update && apt-get install -y \\",
|
||||
" mercurial \\",
|
||||
" subversion \\",
|
||||
" && apt-get clean \\",
|
||||
" && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*");
|
||||
|
||||
MT("from_comment",
|
||||
" [keyword FROM] debian:stretch # I tend to use stable as that is more stable",
|
||||
" [keyword FROM] debian:stretch [keyword AS] stable # I am even more stable",
|
||||
" [keyword FROM] [error # this is an error]");
|
||||
|
||||
MT("from_as",
|
||||
"[keyword FROM] golang:1.9.2-alpine3.6 [keyword AS] build",
|
||||
"[keyword COPY] --from=build /bin/project /bin/project",
|
||||
"[keyword ENTRYPOINT] [[ [string \"/bin/project\"] ]]",
|
||||
"[keyword CMD] [[ [string \"--help\"] ]]");
|
||||
|
||||
MT("arg",
|
||||
"[keyword ARG] VERSION=latest",
|
||||
"[keyword FROM] busybox:$VERSION",
|
||||
"[keyword ARG] VERSION",
|
||||
"[keyword RUN] echo $VERSION > image_version");
|
||||
|
||||
MT("label",
|
||||
"[keyword LABEL] com.example.label-with-value=[string \"foo\"]");
|
||||
|
||||
MT("label_multiline",
|
||||
"[keyword LABEL] description=[string \"This text illustrates ]\\",
|
||||
"[string that label-values can span multiple lines.\"]");
|
||||
|
||||
MT("maintainer",
|
||||
"[keyword MAINTAINER] Foo Bar [string \"foo@bar.com\"] ",
|
||||
"[keyword MAINTAINER] Bar Baz <bar@baz.com>");
|
||||
|
||||
MT("env",
|
||||
"[keyword ENV] BUNDLE_PATH=[string \"$GEM_HOME\"] \\",
|
||||
" BUNDLE_APP_CONFIG=[string \"$GEM_HOME\"]");
|
||||
|
||||
MT("verify_keyword",
|
||||
"[keyword RUN] add-apt-repository ppa:chris-lea/node.js");
|
||||
|
||||
MT("scripts",
|
||||
"[comment # Set an entrypoint, to automatically install node modules]",
|
||||
"[keyword ENTRYPOINT] [[ [string \"/bin/bash\"], [string \"-c\"], [string \"if [[ ! -d node_modules ]]; then npm install; fi; exec \\\"${@:0}\\\";\"] ]]",
|
||||
"[keyword CMD] npm start",
|
||||
"[keyword RUN] npm run build && \\",
|
||||
"[comment # a comment between the shell commands]",
|
||||
" npm run test");
|
||||
|
||||
MT("strings_single",
|
||||
"[keyword FROM] buildpack-deps:stretch",
|
||||
"[keyword RUN] { \\",
|
||||
" echo [string 'install: --no-document']; \\",
|
||||
" echo [string 'update: --no-document']; \\",
|
||||
" } >> /usr/local/etc/gemrc");
|
||||
|
||||
MT("strings_single_multiline",
|
||||
"[keyword RUN] set -ex \\",
|
||||
" \\",
|
||||
" && buildDeps=[string ' ]\\",
|
||||
"[string bison ]\\",
|
||||
"[string dpkg-dev ]\\",
|
||||
"[string libgdbm-dev ]\\",
|
||||
"[string ruby ]\\",
|
||||
"[string '] \\",
|
||||
" && apt-get update");
|
||||
|
||||
MT("strings_single_multiline_2",
|
||||
"[keyword RUN] echo [string 'say \\' ]\\",
|
||||
"[string it works'] ");
|
||||
|
||||
MT("strings_double",
|
||||
"[keyword RUN] apt-get install -y --no-install-recommends $buildDeps \\",
|
||||
" \\",
|
||||
" && wget -O ruby.tar.xz [string \"https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz\"] \\",
|
||||
" && echo [string \"$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz\"] | sha256sum -c - ");
|
||||
|
||||
MT("strings_double_multiline",
|
||||
"[keyword RUN] echo [string \"say \\\" ]\\",
|
||||
"[string it works\"] ");
|
||||
|
||||
MT("escape",
|
||||
"[comment # escape=`]",
|
||||
"[keyword FROM] microsoft/windowsservercore",
|
||||
"[keyword RUN] powershell.exe -Command `",
|
||||
" $ErrorActionPreference = [string 'Stop']; `",
|
||||
" wget https://www.python.org/ftp/python/3.5.1/python-3.5.1.exe -OutFile c:\python-3.5.1.exe ; `",
|
||||
" Start-Process c:\python-3.5.1.exe -ArgumentList [string '/quiet InstallAllUsers=1 PrependPath=1'] -Wait ; `",
|
||||
" Remove-Item c:\python-3.5.1.exe -Force)");
|
||||
|
||||
MT("escape_strings",
|
||||
"[comment # escape=`]",
|
||||
"[keyword FROM] python:3.6-windowsservercore [keyword AS] python",
|
||||
"[keyword RUN] $env:PATH = [string 'C:\\Python;C:\\Python\\Scripts;{0}'] -f $env:PATH ; `",
|
||||
// It should not consider \' as escaped.
|
||||
// " Set-ItemProperty -Path [string 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\'] -Name Path -Value $env:PATH ;");
|
||||
" Set-ItemProperty -Path [string 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' -Name Path -Value $env:PATH ;]");
|
||||
})();
|
||||
@@ -46,7 +46,11 @@
|
||||
comment: [
|
||||
{ regex: /\}\}/, pop: true, token: "comment" },
|
||||
{ regex: /./, token: "comment" }
|
||||
]
|
||||
],
|
||||
meta: {
|
||||
blockCommentStart: "{{--",
|
||||
blockCommentEnd: "--}}"
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineMode("handlebars", function(config, parserConfig) {
|
||||
|
||||
@@ -197,13 +197,14 @@ CodeMirror.defineMode("haskell", function(_config, modeConfig) {
|
||||
"\.\.", ":", "::", "=", "\\", "<-", "->", "@", "~", "=>");
|
||||
|
||||
setType("builtin")(
|
||||
"!!", "$!", "$", "&&", "+", "++", "-", ".", "/", "/=", "<", "<=", "=<<",
|
||||
"==", ">", ">=", ">>", ">>=", "^", "^^", "||", "*", "**");
|
||||
"!!", "$!", "$", "&&", "+", "++", "-", ".", "/", "/=", "<", "<*", "<=",
|
||||
"<$>", "<*>", "=<<", "==", ">", ">=", ">>", ">>=", "^", "^^", "||", "*",
|
||||
"*>", "**");
|
||||
|
||||
setType("builtin")(
|
||||
"Bool", "Bounded", "Char", "Double", "EQ", "Either", "Enum", "Eq",
|
||||
"False", "FilePath", "Float", "Floating", "Fractional", "Functor", "GT",
|
||||
"IO", "IOError", "Int", "Integer", "Integral", "Just", "LT", "Left",
|
||||
"Applicative", "Bool", "Bounded", "Char", "Double", "EQ", "Either", "Enum",
|
||||
"Eq", "False", "FilePath", "Float", "Floating", "Fractional", "Functor",
|
||||
"GT", "IO", "IOError", "Int", "Integer", "Integral", "Just", "LT", "Left",
|
||||
"Maybe", "Monad", "Nothing", "Num", "Ord", "Ordering", "Rational", "Read",
|
||||
"ReadS", "Real", "RealFloat", "RealFrac", "Right", "Show", "ShowS",
|
||||
"String", "True");
|
||||
@@ -223,7 +224,7 @@ CodeMirror.defineMode("haskell", function(_config, modeConfig) {
|
||||
"lcm", "length", "lex", "lines", "log", "logBase", "lookup", "map",
|
||||
"mapM", "mapM_", "max", "maxBound", "maximum", "maybe", "min", "minBound",
|
||||
"minimum", "mod", "negate", "not", "notElem", "null", "odd", "or",
|
||||
"otherwise", "pi", "pred", "print", "product", "properFraction",
|
||||
"otherwise", "pi", "pred", "print", "product", "properFraction", "pure",
|
||||
"putChar", "putStr", "putStrLn", "quot", "quotRem", "read", "readFile",
|
||||
"readIO", "readList", "readLn", "readParen", "reads", "readsPrec",
|
||||
"realToFrac", "recip", "rem", "repeat", "replicate", "return", "reverse",
|
||||
|
||||
@@ -80,7 +80,7 @@ option.</p>
|
||||
<li><a href="javascript/index.html">JavaScript</a> (<a href="jsx/index.html">JSX</a>)</li>
|
||||
<li><a href="jinja2/index.html">Jinja2</a></li>
|
||||
<li><a href="julia/index.html">Julia</a></li>
|
||||
<li><a href="kotlin/index.html">Kotlin</a></li>
|
||||
<li><a href="clike/index.html">Kotlin</a></li>
|
||||
<li><a href="css/less.html">LESS</a></li>
|
||||
<li><a href="livescript/index.html">LiveScript</a></li>
|
||||
<li><a href="lua/index.html">Lua</a></li>
|
||||
|
||||
@@ -75,17 +75,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
return ret(ch);
|
||||
} else if (ch == "=" && stream.eat(">")) {
|
||||
return ret("=>", "operator");
|
||||
} else if (ch == "0" && stream.eat(/x/i)) {
|
||||
stream.eatWhile(/[\da-f]/i);
|
||||
return ret("number", "number");
|
||||
} else if (ch == "0" && stream.eat(/o/i)) {
|
||||
stream.eatWhile(/[0-7]/i);
|
||||
return ret("number", "number");
|
||||
} else if (ch == "0" && stream.eat(/b/i)) {
|
||||
stream.eatWhile(/[01]/i);
|
||||
} else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) {
|
||||
return ret("number", "number");
|
||||
} else if (/\d/.test(ch)) {
|
||||
stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
|
||||
stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/);
|
||||
return ret("number", "number");
|
||||
} else if (ch == "/") {
|
||||
if (stream.eat("*")) {
|
||||
@@ -96,7 +89,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
return ret("comment", "comment");
|
||||
} else if (expressionAllowed(stream, state, 1)) {
|
||||
readRegexp(stream);
|
||||
stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
|
||||
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
|
||||
return ret("regexp", "string-2");
|
||||
} else {
|
||||
stream.eat("=");
|
||||
@@ -126,7 +119,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
var kw = keywords[word]
|
||||
return ret(kw.type, kw.style, word)
|
||||
}
|
||||
if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\(\w]/, false))
|
||||
if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false))
|
||||
return ret("async", "keyword", word)
|
||||
}
|
||||
return ret("variable", "variable", word)
|
||||
@@ -265,21 +258,42 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
pass.apply(null, arguments);
|
||||
return true;
|
||||
}
|
||||
function inList(name, list) {
|
||||
for (var v = list; v; v = v.next) if (v.name == name) return true
|
||||
return false;
|
||||
}
|
||||
function register(varname) {
|
||||
function inList(list) {
|
||||
for (var v = list; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
return false;
|
||||
}
|
||||
var state = cx.state;
|
||||
cx.marked = "def";
|
||||
if (state.context) {
|
||||
if (inList(state.localVars)) return;
|
||||
state.localVars = {name: varname, next: state.localVars};
|
||||
if (state.lexical.info == "var" && state.context && state.context.block) {
|
||||
// FIXME function decls are also not block scoped
|
||||
var newContext = registerVarScoped(varname, state.context)
|
||||
if (newContext != null) {
|
||||
state.context = newContext
|
||||
return
|
||||
}
|
||||
} else if (!inList(varname, state.localVars)) {
|
||||
state.localVars = new Var(varname, state.localVars)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fall through means this is global
|
||||
if (parserConfig.globalVars && !inList(varname, state.globalVars))
|
||||
state.globalVars = new Var(varname, state.globalVars)
|
||||
}
|
||||
function registerVarScoped(varname, context) {
|
||||
if (!context) {
|
||||
return null
|
||||
} else if (context.block) {
|
||||
var inner = registerVarScoped(varname, context.prev)
|
||||
if (!inner) return null
|
||||
if (inner == context.prev) return context
|
||||
return new Context(inner, context.vars, true)
|
||||
} else if (inList(varname, context.vars)) {
|
||||
return context
|
||||
} else {
|
||||
if (inList(state.globalVars)) return;
|
||||
if (parserConfig.globalVars)
|
||||
state.globalVars = {name: varname, next: state.globalVars};
|
||||
return new Context(context.prev, new Var(varname, context.vars), false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,15 +303,23 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
|
||||
// Combinators
|
||||
|
||||
var defaultVars = {name: "this", next: {name: "arguments"}};
|
||||
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
|
||||
function Var(name, next) { this.name = name; this.next = next }
|
||||
|
||||
var defaultVars = new Var("this", new Var("arguments", null))
|
||||
function pushcontext() {
|
||||
cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
|
||||
cx.state.localVars = defaultVars;
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
|
||||
cx.state.localVars = defaultVars
|
||||
}
|
||||
function pushblockcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
|
||||
cx.state.localVars = null
|
||||
}
|
||||
function popcontext() {
|
||||
cx.state.localVars = cx.state.context.vars;
|
||||
cx.state.context = cx.state.context.prev;
|
||||
cx.state.localVars = cx.state.context.vars
|
||||
cx.state.context = cx.state.context.prev
|
||||
}
|
||||
popcontext.lex = true
|
||||
function pushlex(type, info) {
|
||||
var result = function() {
|
||||
var state = cx.state, indent = state.indented;
|
||||
@@ -322,19 +344,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
function expect(wanted) {
|
||||
function exp(type) {
|
||||
if (type == wanted) return cont();
|
||||
else if (wanted == ";") return pass();
|
||||
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
|
||||
else return cont(exp);
|
||||
};
|
||||
return exp;
|
||||
}
|
||||
|
||||
function statement(type, value) {
|
||||
if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
|
||||
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
|
||||
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
|
||||
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
|
||||
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
|
||||
if (type == "debugger") return cont(expect(";"));
|
||||
if (type == "{") return cont(pushlex("}"), block, poplex);
|
||||
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
|
||||
if (type == ";") return cont();
|
||||
if (type == "if") {
|
||||
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
|
||||
@@ -345,34 +367,38 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
|
||||
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), className, poplex); }
|
||||
if (type == "variable") {
|
||||
if (isTS && value == "type") {
|
||||
cx.marked = "keyword"
|
||||
return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
|
||||
} else if (isTS && value == "declare") {
|
||||
if (isTS && value == "declare") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else if (isTS && (value == "module" || value == "enum") && cx.stream.match(/^\s*\w/, false)) {
|
||||
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
|
||||
if (value == "enum") return cont(enumdef);
|
||||
else if (value == "type") return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
|
||||
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
|
||||
} else if (isTS && value == "namespace") {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form"), expression, block, poplex)
|
||||
} else if (isTS && value == "abstract") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else {
|
||||
return cont(pushlex("stat"), maybelabel);
|
||||
}
|
||||
}
|
||||
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"),
|
||||
block, poplex, poplex);
|
||||
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
|
||||
block, poplex, poplex, popcontext);
|
||||
if (type == "case") return cont(expression, expect(":"));
|
||||
if (type == "default") return cont(expect(":"));
|
||||
if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
|
||||
statement, poplex, popcontext);
|
||||
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
|
||||
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
|
||||
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
|
||||
if (type == "async") return cont(statement)
|
||||
if (value == "@") return cont(expression, statement)
|
||||
return pass(pushlex("stat"), expression, expect(";"), poplex);
|
||||
}
|
||||
function maybeCatchBinding(type) {
|
||||
if (type == "(") return cont(funarg, expect(")"))
|
||||
}
|
||||
function expression(type, value) {
|
||||
return expressionInner(type, value, false);
|
||||
}
|
||||
@@ -401,6 +427,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
|
||||
if (type == "quasi") return pass(quasi, maybeop);
|
||||
if (type == "new") return cont(maybeTarget(noComma));
|
||||
if (type == "import") return cont(expression);
|
||||
return cont();
|
||||
}
|
||||
function maybeexpression(type) {
|
||||
@@ -560,19 +587,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
}
|
||||
}
|
||||
function typeexpr(type, value) {
|
||||
if (value == "keyof" || value == "typeof") {
|
||||
cx.marked = "keyword"
|
||||
return cont(value == "keyof" ? typeexpr : expressionNoComma)
|
||||
}
|
||||
if (type == "variable" || value == "void") {
|
||||
if (value == "keyof") {
|
||||
cx.marked = "keyword"
|
||||
return cont(typeexpr)
|
||||
} else {
|
||||
cx.marked = "type"
|
||||
return cont(afterType)
|
||||
}
|
||||
cx.marked = "type"
|
||||
return cont(afterType)
|
||||
}
|
||||
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
|
||||
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
|
||||
if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType)
|
||||
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType)
|
||||
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
|
||||
}
|
||||
function maybeReturnType(type) {
|
||||
if (type == "=>") return cont(typeexpr)
|
||||
@@ -589,13 +616,14 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
return cont(expression, maybetype, expect("]"), typeprop)
|
||||
}
|
||||
}
|
||||
function typearg(type) {
|
||||
if (type == "variable") return cont(typearg)
|
||||
else if (type == ":") return cont(typeexpr)
|
||||
function typearg(type, value) {
|
||||
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
|
||||
if (type == ":") return cont(typeexpr)
|
||||
return pass(typeexpr)
|
||||
}
|
||||
function afterType(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
if (value == "|" || type == ".") return cont(typeexpr)
|
||||
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
|
||||
if (type == "[") return cont(expect("]"), afterType)
|
||||
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
|
||||
}
|
||||
@@ -608,7 +636,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
function maybeTypeDefault(_, value) {
|
||||
if (value == "=") return cont(typeexpr)
|
||||
}
|
||||
function vardef() {
|
||||
function vardef(_, value) {
|
||||
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
|
||||
return pass(pattern, maybetype, maybeAssign, vardefCont);
|
||||
}
|
||||
function pattern(type, value) {
|
||||
@@ -637,7 +666,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
function maybeelse(type, value) {
|
||||
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
|
||||
}
|
||||
function forspec(type) {
|
||||
function forspec(type, value) {
|
||||
if (value == "await") return cont(forspec);
|
||||
if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
|
||||
}
|
||||
function forspec1(type) {
|
||||
@@ -680,8 +710,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
}
|
||||
function classNameAfter(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
|
||||
if (value == "extends" || value == "implements" || (isTS && type == ","))
|
||||
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
|
||||
if (value == "implements") cx.marked = "keyword";
|
||||
return cont(isTS ? typeexpr : expression, classNameAfter);
|
||||
}
|
||||
if (type == "{") return cont(pushlex("}"), classBody, poplex);
|
||||
}
|
||||
function classBody(type, value) {
|
||||
@@ -724,6 +756,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
}
|
||||
function afterImport(type) {
|
||||
if (type == "string") return cont();
|
||||
if (type == "(") return pass(expression);
|
||||
return pass(importSpec, maybeMoreImports, maybeFrom);
|
||||
}
|
||||
function importSpec(type, value) {
|
||||
@@ -745,6 +778,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
if (type == "]") return cont();
|
||||
return pass(commasep(expressionNoComma, "]"));
|
||||
}
|
||||
function enumdef() {
|
||||
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
|
||||
}
|
||||
function enummember() {
|
||||
return pass(pattern, maybeAssign);
|
||||
}
|
||||
|
||||
function isContinuedStatement(state, textAfter) {
|
||||
return state.lastType == "operator" || state.lastType == "," ||
|
||||
@@ -768,7 +807,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
cc: [],
|
||||
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
|
||||
localVars: parserConfig.localVars,
|
||||
context: parserConfig.localVars && {vars: parserConfig.localVars},
|
||||
context: parserConfig.localVars && new Context(null, null, false),
|
||||
indented: basecolumn || 0
|
||||
};
|
||||
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
|
||||
@@ -809,7 +848,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
lexical = lexical.prev;
|
||||
var type = lexical.type, closing = firstChar == type;
|
||||
|
||||
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
|
||||
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
|
||||
else if (type == "form" && firstChar == "{") return lexical.indented;
|
||||
else if (type == "form") return lexical.indented + indentUnit;
|
||||
else if (type == "stat")
|
||||
|
||||
@@ -63,6 +63,12 @@
|
||||
MT("import_trailing_comma",
|
||||
"[keyword import] {[def foo], [def bar],} [keyword from] [string 'baz']")
|
||||
|
||||
MT("import_dynamic",
|
||||
"[keyword import]([string 'baz']).[property then]")
|
||||
|
||||
MT("import_dynamic",
|
||||
"[keyword const] [def t] [operator =] [keyword import]([string 'baz']).[property then]")
|
||||
|
||||
MT("const",
|
||||
"[keyword function] [def f]() {",
|
||||
" [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
|
||||
@@ -71,12 +77,44 @@
|
||||
MT("for/of",
|
||||
"[keyword for]([keyword let] [def of] [keyword of] [variable something]) {}");
|
||||
|
||||
MT("for await",
|
||||
"[keyword for] [keyword await]([keyword let] [def of] [keyword of] [variable something]) {}");
|
||||
|
||||
MT("generator",
|
||||
"[keyword function*] [def repeat]([def n]) {",
|
||||
" [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
|
||||
" [keyword yield] [variable-2 i];",
|
||||
"}");
|
||||
|
||||
MT("let_scoping",
|
||||
"[keyword function] [def scoped]([def n]) {",
|
||||
" { [keyword var] [def i]; } [variable-2 i];",
|
||||
" { [keyword let] [def j]; [variable-2 j]; } [variable j];",
|
||||
" [keyword if] ([atom true]) { [keyword const] [def k]; [variable-2 k]; } [variable k];",
|
||||
"}");
|
||||
|
||||
MT("switch_scoping",
|
||||
"[keyword switch] ([variable x]) {",
|
||||
" [keyword default]:",
|
||||
" [keyword let] [def j];",
|
||||
" [keyword return] [variable-2 j]",
|
||||
"}",
|
||||
"[variable j];")
|
||||
|
||||
MT("leaving_scope",
|
||||
"[keyword function] [def a]() {",
|
||||
" {",
|
||||
" [keyword const] [def x] [operator =] [number 1]",
|
||||
" [keyword if] ([atom true]) {",
|
||||
" [keyword let] [def y] [operator =] [number 2]",
|
||||
" [keyword var] [def z] [operator =] [number 3]",
|
||||
" [variable console].[property log]([variable-2 x], [variable-2 y], [variable-2 z])",
|
||||
" }",
|
||||
" [variable console].[property log]([variable-2 x], [variable y], [variable-2 z])",
|
||||
" }",
|
||||
" [variable console].[property log]([variable x], [variable y], [variable-2 z])",
|
||||
"}")
|
||||
|
||||
MT("quotedStringAddition",
|
||||
"[keyword let] [def f] [operator =] [variable a] [operator +] [string 'fatarrow'] [operator +] [variable c];");
|
||||
|
||||
@@ -230,6 +268,8 @@
|
||||
"[keyword const] [def async] [operator =] {[property a]: [number 1]};",
|
||||
"[keyword const] [def foo] [operator =] [string-2 `bar ${][variable async].[property a][string-2 }`];")
|
||||
|
||||
MT("bigint", "[number 1n] [operator +] [number 0x1afn] [operator +] [number 0o064n] [operator +] [number 0b100n];")
|
||||
|
||||
MT("async_comment",
|
||||
"[keyword async] [comment /**/] [keyword function] [def foo]([def args]) { [keyword return] [atom true]; }");
|
||||
|
||||
@@ -383,6 +423,25 @@
|
||||
" }",
|
||||
"}")
|
||||
|
||||
TS("type as variable",
|
||||
"[variable type] [operator =] [variable x] [keyword as] [type Bar];");
|
||||
|
||||
TS("enum body",
|
||||
"[keyword export] [keyword const] [keyword enum] [def CodeInspectionResultType] {",
|
||||
" [def ERROR] [operator =] [string 'problem_type_error'],",
|
||||
" [def WARNING] [operator =] [string 'problem_type_warning'],",
|
||||
" [def META],",
|
||||
"}")
|
||||
|
||||
TS("parenthesized type",
|
||||
"[keyword class] [def Foo] {",
|
||||
" [property x] [operator =] [keyword new] [variable A][operator <][type B], [type string][operator |](() [operator =>] [type void])[operator >]();",
|
||||
" [keyword private] [property bar]();",
|
||||
"}")
|
||||
|
||||
TS("abstract class",
|
||||
"[keyword export] [keyword abstract] [keyword class] [def Foo] {}")
|
||||
|
||||
var jsonld_mode = CodeMirror.getMode(
|
||||
{indentUnit: 2},
|
||||
{name: "javascript", jsonld: true}
|
||||
|
||||
@@ -54,11 +54,13 @@ CodeMirror.defineMode("julia", function(config, parserConf) {
|
||||
return inGenerator(state, '[')
|
||||
}
|
||||
|
||||
function inGenerator(state, bracket) {
|
||||
var curr = currentScope(state),
|
||||
prev = currentScope(state, 1);
|
||||
function inGenerator(state, bracket, depth) {
|
||||
if (typeof(bracket) === "undefined") { bracket = '('; }
|
||||
if (curr === bracket || (prev === bracket && curr === "for")) {
|
||||
if (typeof(depth) === "undefined") { depth = 0; }
|
||||
var scope = currentScope(state, depth);
|
||||
if ((depth == 0 && scope === "if" && inGenerator(state, bracket, depth + 1)) ||
|
||||
(scope === "for" && inGenerator(state, bracket, depth + 1)) ||
|
||||
(scope === bracket)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -119,16 +121,16 @@ CodeMirror.defineMode("julia", function(config, parserConf) {
|
||||
state.scopes.push('(');
|
||||
}
|
||||
|
||||
var scope = currentScope(state);
|
||||
|
||||
if (inArray(state) && ch === ']') {
|
||||
if (scope === "for") { state.scopes.pop(); }
|
||||
if (currentScope(state) === "if") { state.scopes.pop(); }
|
||||
while (currentScope(state) === "for") { state.scopes.pop(); }
|
||||
state.scopes.pop();
|
||||
state.leavingExpr = true;
|
||||
}
|
||||
|
||||
if (inGenerator(state) && ch === ')') {
|
||||
if (scope === "for") { state.scopes.pop(); }
|
||||
if (currentScope(state) === "if") { state.scopes.pop(); }
|
||||
while (currentScope(state) === "for") { state.scopes.pop(); }
|
||||
state.scopes.pop();
|
||||
state.leavingExpr = true;
|
||||
}
|
||||
@@ -143,12 +145,14 @@ CodeMirror.defineMode("julia", function(config, parserConf) {
|
||||
}
|
||||
|
||||
var match;
|
||||
if (match = stream.match(openers, false)) {
|
||||
if (match = stream.match(openers)) {
|
||||
state.scopes.push(match[0]);
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (stream.match(closers, false)) {
|
||||
if (stream.match(closers)) {
|
||||
state.scopes.pop();
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
// Handle type annotations
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user