Merge branch 'master' into stable

This commit is contained in:
zadam 2019-12-18 20:49:24 +01:00
commit 6d9b702d4c
124 changed files with 10383 additions and 6152 deletions

View file

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

View file

@ -1,4 +1,4 @@
FROM node:12.13.0-alpine
FROM node:12.13.1-alpine
# Create app directory
WORKDIR /usr/src/app

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=12.13.0
NODE_VERSION=12.13.1
if [ "$1" != "DONTCOPY" ]
then

View file

@ -2,6 +2,9 @@
# Instance name can be used to distinguish between different instances
instanceName=
# Disable automatically generating desktop icon
# noDesktopIcon=true
[Network]
# host setting is relevant only for web deployments - set the host on which the server will listen
# host=0.0.0.0

Binary file not shown.

View file

@ -0,0 +1,3 @@
UPDATE attributes SET name = 'internalLink' WHERE name = 'internal-link';
UPDATE attributes SET name = 'imageLink' WHERE name = 'image-link';
UPDATE attributes SET name = 'relationMapLink' WHERE name = 'relation-map-link';

View file

@ -0,0 +1,5 @@
DROP INDEX IF EXISTS IDX_attributes_name_index;
DROP INDEX IF EXISTS IDX_branches_noteId;
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);

View file

@ -9,6 +9,8 @@ CREATE TABLE IF NOT EXISTS "source_ids" (
`utcDateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
@ -47,8 +49,6 @@ CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_name_index
on attributes (name);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
@ -80,7 +80,6 @@ CREATE TABLE IF NOT EXISTS "branches" (
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId` ON `branches` (`noteId`);
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,

View file

@ -396,7 +396,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line313">line 313</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line394">line 394</a>
</li></ul></dd>
@ -534,6 +534,375 @@ the backend.
<h4 class="name" id="createDataNote"><span class="type-signature"></span>createDataNote<span class="signature">(parentNoteId, title, content)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
<div class="description">
Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
JSON MIME type. See also createNewNote() for more options.
</div>
<h5>Parameters:</h5>
<table class="params">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>parentNoteId</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>title</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>content</code></td>
<td class="type">
<span class="param-type">object</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line204">line 204</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}></span>
</dd>
</dl>
<h4 class="name" id="createNewNote"><span class="type-signature"></span>createNewNote<span class="signature">(params<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
<h5>Parameters:</h5>
<table class="params">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Attributes</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>params</code></td>
<td class="type">
<span class="param-type"><a href="global.html#CreateNewNoteParams">CreateNewNoteParams</a></span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line231">line 231</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<div class="param-desc">
object contains newly created entities note and branch
</div>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}></span>
</dd>
</dl>
<h4 class="name" id="createNote"><span class="type-signature"></span>createNote<span class="signature">(parentNoteId, title, content<span class="signature-attributes">opt</span>, extraOptions<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
@ -760,7 +1129,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line198">line 198</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line258">line 258</a>
</li></ul></dd>
@ -818,7 +1187,7 @@ the backend.
<h4 class="name" id="createNoteAndRefresh"><span class="type-signature"></span>createNoteAndRefresh<span class="signature">(parentNoteId, title, content<span class="signature-attributes">opt</span>, extraOptions<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
<h4 class="name" id="createTextNote"><span class="type-signature"></span>createTextNote<span class="signature">(parentNoteId, title, content)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
@ -826,7 +1195,7 @@ the backend.
<div class="description">
Creates new note according to given params and force all connected clients to refresh their tree.
Create text note. See also createNewNote() for more options.
</div>
@ -850,12 +1219,8 @@ the backend.
<th>Type</th>
<th>Attributes</th>
<th>Default</th>
<th class="last">Description</th>
</tr>
@ -879,22 +1244,10 @@ the backend.
</td>
<td class="attributes">
</td>
<td class="default">
</td>
<td class="description last">create new note under this parent</td>
<td class="description last"></td>
</tr>
@ -914,19 +1267,7 @@ the backend.
</td>
<td class="attributes">
</td>
<td class="default">
</td>
<td class="description last"></td>
@ -949,62 +1290,7 @@ the backend.
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="default">
""
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>extraOptions</code></td>
<td class="type">
<span class="param-type"><a href="global.html#CreateNoteExtraOptions">CreateNoteExtraOptions</a></span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="default">
{}
</td>
<td class="description last"></td>
@ -1048,7 +1334,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line211">line 211</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line188">line 188</a>
</li></ul></dd>
@ -1076,10 +1362,6 @@ the backend.
<h5>Returns:</h5>
<div class="param-desc">
object contains newly created entities note and branch
</div>
<dl>
@ -1533,7 +1815,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line318">line 318</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line399">line 399</a>
</li></ul></dd>
@ -1997,7 +2279,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line241">line 241</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line314">line 314</a>
</li></ul></dd>
@ -2765,7 +3047,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line260">line 260</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line341">line 341</a>
</li></ul></dd>
@ -3418,7 +3700,113 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line232">line 232</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line305">line 305</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Promise.&lt;(<a href="Note.html">Note</a>|null)></span>
</dd>
</dl>
<h4 class="name" id="getTodayNote"><span class="type-signature"></span>getTodayNote<span class="signature">()</span><span class="type-signature"> &rarr; {Promise.&lt;(<a href="Note.html">Note</a>|null)>}</span></h4>
<div class="description">
Returns today's day note. If such note doesn't exist, it is created.
</div>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line322">line 322</a>
</li></ul></dd>
@ -3596,7 +3984,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line251">line 251</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line332">line 332</a>
</li></ul></dd>
@ -3751,7 +4139,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line269">line 269</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line350">line 350</a>
</li></ul></dd>
@ -3901,7 +4289,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line224">line 224</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line297">line 297</a>
</li></ul></dd>
@ -4404,7 +4792,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line290">line 290</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line371">line 371</a>
</li></ul></dd>
@ -4537,7 +4925,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line276">line 276</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line357">line 357</a>
</li></ul></dd>
@ -4912,7 +5300,7 @@ transactional by default.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line303">line 303</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line384">line 384</a>
</li></ul></dd>

File diff suppressed because it is too large Load diff

View file

@ -107,6 +107,10 @@ class Attribute extends Entity {
async beforeSaving() {
if (!this.value) {
if (this.type === 'relation') {
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
}
// null value isn't allowed
this.value = "";
}
@ -137,6 +141,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note;
}
}

View file

@ -142,6 +142,10 @@ class Note extends Entity {
/** @returns {Promise} */
async setContent(content) {
if (content === null || content === undefined) {
throw new Error(`Cannot set null content to note ${this.noteId}`);
}
// force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.length;
@ -220,26 +224,36 @@ class Note extends Entity {
return null;
}
async loadOwnedAttributesToCache() {
this.__ownedAttributeCache = await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
return this.__ownedAttributeCache;
}
/**
* @returns {Promise&lt;Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
* This method is a faster variant of getAttributes() which looks for only owned attributes.
* Use when inheritance is not needed and/or in batch/performance sensitive operations.
*
* This method can be significantly faster than the getAttributes()
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise&lt;Attribute[]>} note's "owned" attributes - excluding inherited ones
*/
async getOwnedAttributes(type, name) {
let query = `SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`;
const params = [this.noteId];
if (type) {
query += ` AND type = ?`;
params.push(type);
if (!this.__ownedAttributeCache) {
await this.loadOwnedAttributesToCache();
}
if (name) {
query += ` AND name = ?`;
params.push(name);
if (type &amp;&amp; name) {
return this.__ownedAttributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
}
else if (type) {
return this.__ownedAttributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__ownedAttributeCache.filter(attr => attr.name === name);
}
else {
return this.__ownedAttributeCache.slice();
}
return await repository.getEntities(query, params);
}
/**
@ -261,19 +275,26 @@ class Note extends Entity {
}
/**
* @param {string} [name] - attribute name to filter
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise&lt;Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
async getAttributes(type, name) {
if (!this.__attributeCache) {
await this.loadAttributesToCache();
}
if (name) {
if (type &amp;&amp; name) {
return this.__attributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.__attributeCache;
return this.__attributeCache.slice();
}
}
@ -282,7 +303,15 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), excluding inherited ones
*/
async getOwnedLabels(name) {
return await this.getOwnedAttributes(LABEL, name);
}
/**
@ -290,7 +319,7 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@ -298,7 +327,15 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones
*/
async getOwnedRelations(name) {
return await this.getOwnedAttributes(RELATION, name);
}
/**
@ -321,7 +358,7 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@ -330,6 +367,7 @@ class Note extends Entity {
*/
invalidateAttributeCache() {
this.__attributeCache = null;
this.__ownedAttributeCache = null;
}
/** @returns {Promise&lt;void>} */
@ -339,11 +377,10 @@ class Note extends Entity {
tree(noteId, level) AS (
SELECT ?, 0
UNION
SELECT branches.parentNoteId, tree.level + 1 FROM branches
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
WHERE branches.isDeleted = 0
),
treeWithAttrs(noteId, level) AS (
SELECT * FROM tree
@ -362,6 +399,11 @@ class Note extends Entity {
// 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 this exact attribute already appears then don't include it (can happen via cloning)
if (attributes.findIndex(it => it.attributeId === attr.attributeId) !== index) {
return false;
}
if (attr.isDefinition()) {
const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type &amp;&amp; el.name === attr.name);
@ -405,6 +447,15 @@ class Note extends Entity {
return !!await this.getAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;boolean>} true if note has an attribute with given type and name (excluding inherited)
*/
async hasOwnedAttribute(type, name) {
return !!await this.getOwnedAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@ -419,7 +470,7 @@ class Note extends Entity {
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;string>} attribute value of given type and name or null if no such attribute exists.
* @returns {Promise&lt;string|null>} 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);
@ -427,6 +478,17 @@ class Note extends Entity {
return attr ? attr.value : null;
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;string|null>} attribute value of given type and name or null if no such attribute exists.
*/
async getOwnedAttributeValue(type, name) {
const attr = await this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
}
/**
* Based on enabled, attribute is either set or removed.
*
@ -454,7 +516,7 @@ class Note extends Entity {
* @returns {Promise&lt;void>}
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
let attr = attributes.find(attr => attr.type === type &amp;&amp; attr.name === name);
if (attr) {
@ -488,7 +550,7 @@ class Note extends Entity {
* @returns {Promise&lt;void>}
*/
async removeAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type &amp;&amp; (value === undefined || value === attribute.value)) {
@ -500,42 +562,104 @@ class Note extends Entity {
}
}
/**
* @return {Promise&lt;Attribute>}
*/
async addAttribute(type, name, value = "") {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
});
await attr.save();
this.invalidateAttributeCache();
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
}
/**
* @param {string} name - label name
* @returns {Promise&lt;boolean>} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;boolean>} true if label exists (excluding inherited)
*/
async hasOwnedLabel(name) { return await this.hasOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;boolean>} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;boolean>} true if relation exists (excluding inherited)
*/
async hasOwnedRelation(name) { return await this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;Attribute>} label if it exists, null otherwise
* @returns {Promise&lt;Attribute|null>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;Attribute|null>} label if it exists, null otherwise
*/
async getOwnedLabel(name) { return await this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;Attribute>} relation if it exists, null otherwise
* @returns {Promise&lt;Attribute|null>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;Attribute|null>} relation if it exists, null otherwise
*/
async getOwnedRelation(name) { return await this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;string>} label value if label exists, null otherwise
* @returns {Promise&lt;string|null>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;string|null>} label value if label exists, null otherwise
*/
async getOwnedLabelValue(name) { return await this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;string>} relation value if relation exists, null otherwise
* @returns {Promise&lt;string|null>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;string|null>} relation value if relation exists, null otherwise
*/
async getOwnedRelationValue(name) { return await this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
@ -546,6 +670,16 @@ class Note extends Entity {
return relation ? await repository.getNote(relation.value) : null;
}
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getOwnedRelationTarget(name) {
const relation = await this.getOwnedRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Based on enabled, label is either set or removed.
*
@ -699,7 +833,7 @@ class Note extends Entity {
WHERE noteId = ? AND
isDeleted = 0 AND
type = 'relation' AND
name IN ('internal-link', 'image-link', 'relation-map-link')`, [this.noteId]);
name IN ('internalLink', 'imageLink', 'relationMapLink')`, [this.noteId]);
}
/**
@ -776,6 +910,16 @@ class Note extends Entity {
return notePaths;
}
/**
* @param ancestorNoteId
* @return {Promise&lt;boolean>} - true if ancestorNoteId occurs in at least one of the note's paths
*/
async isDescendantOfNote(ancestorNoteId) {
const notePaths = await this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId));
}
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
@ -815,7 +959,9 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.__ownedAttributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;
}
}

View file

@ -102,6 +102,313 @@
<h4 class="name" id="CreateNewNoteParams">CreateNewNoteParams</h4>
<h5>Type:</h5>
<ul>
<li>
<span class="param-type">object</span>
</li>
</ul>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>parentNoteId</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">MANDATORY</td>
</tr>
<tr>
<td class="name"><code>title</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">MANDATORY</td>
</tr>
<tr>
<td class="name"><code>content</code></td>
<td class="type">
<span class="param-type">string</span>
|
<span class="param-type">buffer</span>
</td>
<td class="description last">MANDATORY</td>
</tr>
<tr>
<td class="name"><code>type</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">text, code, file, image, search, book, relation-map - MANDATORY</td>
</tr>
<tr>
<td class="name"><code>mime</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">value is derived from default mimes for type</td>
</tr>
<tr>
<td class="name"><code>isProtected</code></td>
<td class="type">
<span class="param-type">boolean</span>
</td>
<td class="description last">default is false</td>
</tr>
<tr>
<td class="name"><code>isExpanded</code></td>
<td class="type">
<span class="param-type">boolean</span>
</td>
<td class="description last">default is false</td>
</tr>
<tr>
<td class="name"><code>prefix</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">default is empty string</td>
</tr>
<tr>
<td class="name"><code>notePosition</code></td>
<td class="type">
<span class="param-type">int</span>
</td>
<td class="description last">default is last existing notePosition in a parent + 10</td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line212">line 212</a>
</li></ul></dd>
</dl>
<h4 class="name" id="CreateNoteAttribute">CreateNoteAttribute</h4>
@ -290,6 +597,194 @@
<h4 class="name" id="CreateNoteAttribute">CreateNoteAttribute</h4>
<h5>Type:</h5>
<ul>
<li>
<span class="param-type">object</span>
</li>
</ul>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Attributes</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>type</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
</td>
<td class="description last">attribute type - label, relation etc.</td>
</tr>
<tr>
<td class="name"><code>name</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
</td>
<td class="description last">attribute name</td>
</tr>
<tr>
<td class="name"><code>value</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="description last">attribute value</td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line233">line 233</a>
</li></ul></dd>
</dl>
<h4 class="name" id="CreateNoteExtraOptions">CreateNoteExtraOptions</h4>
@ -558,7 +1053,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line180">line 180</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line240">line 240</a>
</li></ul></dd>

View file

@ -198,6 +198,66 @@ function BackendScriptApi(currentNote, apiParams) {
*/
this.toggleNoteInParent = cloningService.toggleNoteInParent;
/**
* @typedef {object} CreateNoteAttribute
* @property {string} type - attribute type - label, relation etc.
* @property {string} name - attribute name
* @property {string} [value] - attribute value
*/
/**
* Create text note. See also createNewNote() for more options.
*
* @param {string} parentNoteId
* @param {string} title
* @param {string} content
* @return {Promise&lt;{note: Note, branch: Branch}>}
*/
this.createTextNote = async (parentNoteId, title, content = '') => await noteService.createNewNote({
parentNoteId,
title,
content,
type: 'text'
});
/**
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
* JSON MIME type. See also createNewNote() for more options.
*
* @param {string} parentNoteId
* @param {string} title
* @param {object} content
* @return {Promise&lt;{note: Note, branch: Branch}>}
*/
this.createDataNote = async (parentNoteId, title, content = {}) => await noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content),
type: 'code',
mime: 'application/json'
});
/**
* @typedef {object} CreateNewNoteParams
* @property {string} parentNoteId - MANDATORY
* @property {string} title - MANDATORY
* @property {string|buffer} content - MANDATORY
* @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY
* @property {string} mime - value is derived from default mimes for type
* @property {boolean} isProtected - default is false
* @property {boolean} isExpanded - default is false
* @property {string} prefix - default is empty string
* @property {int} notePosition - default is last existing notePosition in a parent + 10
*/
/**
* @method
*
* @param {CreateNewNoteParams} [params]
* @returns {Promise&lt;{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNewNote = noteService.createNewNote;
/**
* @typedef {object} CreateNoteAttribute
* @property {string} type - attribute type - label, relation etc.
@ -223,25 +283,38 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise&lt;{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNote = noteService.createNote;
this.createNote = async (parentNoteId, title, content = "", extraOptions= {}) => {
extraOptions.parentNoteId = parentNoteId;
extraOptions.title = title;
/**
* Creates new note according to given params and force all connected clients to refresh their tree.
*
* @method
*
* @param {string} parentNoteId - create new note under this parent
* @param {string} title
* @param {string} [content=""]
* @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise&lt;{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNoteAndRefresh = async function(parentNoteId, title, content, extraOptions) {
const ret = await noteService.createNote(parentNoteId, title, content, extraOptions);
const parentNote = await repository.getNote(parentNoteId);
ws.refreshTree();
return ret;
// code note type can be inherited, otherwise text is default
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
if (extraOptions.json) {
extraOptions.content = JSON.stringify(content || {}, null, '\t');
extraOptions.type = 'code';
extraOptions.mime = 'application/json';
}
else {
extraOptions.content = content;
}
const {note, branch} = await noteService.createNewNote(extraOptions);
for (const attr of extraOptions.attributes || []) {
await attributeService.createAttribute({
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: !!attr.isInheritable
});
}
return {note, branch};
};
/**
@ -268,6 +341,14 @@ function BackendScriptApi(currentNote, apiParams) {
*/
this.getDateNote = dateNoteService.getDateNote;
/**
* Returns today's day note. If such note doesn't exist, it is created.
*
* @method
* @returns {Promise&lt;Note|null>}
*/
this.getTodayNote = dateNoteService.getTodayNote;
/**
* Returns note for the first date of the week of the given date.
*

View file

@ -4975,6 +4975,90 @@ Internally this serializes the anonymous function into string and sends it to ba
<h4 class="name" id="waitUntilSynced"><span class="type-signature"></span>waitUntilSynced<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line389">line 389</a>
</li></ul></dd>
</dl>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Class: KeyboardActions</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Class: KeyboardActions</h1>
<section>
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>KeyboardActions<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">blaa vlaa</div>
</header>
<article>
<div class="container-overview">
<h2>Constructor</h2>
<h4 class="name" id="KeyboardActions"><span class="type-signature"></span>new KeyboardActions<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line5">line 5</a>
</li></ul></dd>
</dl>
</div>
<h3 class="subsection-title">Members</h3>
<h4 class="name" id="JUMP_TO"><span class="type-signature"></span>JUMP_TO<span class="type-signature"></span></h4>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line7">line 7</a>
</li></ul></dd>
</dl>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="KeyboardActions.html">KeyboardActions</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View file

@ -1220,7 +1220,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line220">line 220</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line227">line 227</a>
</li></ul></dd>
@ -1278,7 +1278,7 @@
<h4 class="name" id="getAttributes"><span class="type-signature">(async) </span>getAttributes<span class="signature">(name<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;Array.&lt;<a href="Attribute.html">Attribute</a>>>}</span></h4>
<h4 class="name" id="getAttributes"><span class="type-signature">(async) </span>getAttributes<span class="signature">(type<span class="signature-attributes">opt</span>, name<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;Array.&lt;<a href="Attribute.html">Attribute</a>>>}</span></h4>
@ -1318,6 +1318,39 @@
<tbody>
<tr>
<td class="name"><code>type</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="description last">(optional) attribute type to filter</td>
</tr>
<tr>
<td class="name"><code>name</code></td>
@ -1346,7 +1379,7 @@
<td class="description last">attribute name to filter</td>
<td class="description last">(optional) attribute name to filter</td>
</tr>
@ -1387,7 +1420,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line160">line 160</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line161">line 161</a>
</li></ul></dd>
@ -1415,6 +1448,10 @@
<h5>Returns:</h5>
<div class="param-desc">
all note's attributes, including inherited ones
</div>
<dl>
@ -1561,7 +1598,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line231">line 231</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line238">line 238</a>
</li></ul></dd>
@ -2124,7 +2161,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line253">line 253</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line260">line 260</a>
</li></ul></dd>
@ -2291,7 +2328,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line186">line 186</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line193">line 193</a>
</li></ul></dd>
@ -2458,7 +2495,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line178">line 178</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line185">line 185</a>
</li></ul></dd>
@ -2613,7 +2650,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line265">line 265</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line272">line 272</a>
</li></ul></dd>
@ -2972,7 +3009,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line259">line 259</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line266">line 266</a>
</li></ul></dd>
@ -3139,7 +3176,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line202">line 202</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line209">line 209</a>
</li></ul></dd>
@ -3306,7 +3343,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line194">line 194</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line201">line 201</a>
</li></ul></dd>
@ -3461,7 +3498,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line277">line 277</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line284">line 284</a>
</li></ul></dd>
@ -3631,7 +3668,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line287">line 287</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line294">line 294</a>
</li></ul></dd>
@ -3782,7 +3819,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line271">line 271</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line278">line 278</a>
</li></ul></dd>
@ -3892,7 +3929,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line311">line 311</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line318">line 318</a>
</li></ul></dd>
@ -4066,7 +4103,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line211">line 211</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line218">line 218</a>
</li></ul></dd>
@ -4323,7 +4360,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line241">line 241</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line248">line 248</a>
</li></ul></dd>
@ -4478,7 +4515,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line247">line 247</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line254">line 254</a>
</li></ul></dd>
@ -4536,7 +4573,7 @@
<h4 class="name" id="invalidateAttributeCache"><span class="type-signature"></span>invalidateAttributeCache<span class="signature">()</span><span class="type-signature"></span></h4>
<h4 class="name" id="invalidate__attributeCache"><span class="type-signature"></span>invalidate__attributeCache<span class="signature">()</span><span class="type-signature"></span></h4>
@ -4589,7 +4626,7 @@ Cache is note instance scoped.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line302">line 302</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line309">line 309</a>
</li></ul></dd>

View file

@ -47,7 +47,7 @@ class Branch {
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
return this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */

View file

@ -182,20 +182,27 @@ class NoteShort {
}
/**
* @param {string} [name] - attribute name to filter
* @returns {Promise&lt;Attribute[]>}
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise&lt;Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
if (!this.attributeCache) {
this.attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
async getAttributes(type, name) {
if (!this.__attributeCache) {
this.__attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
.map(attrRow => new Attribute(this.treeCache, attrRow));
}
if (name) {
return this.attributeCache.filter(attr => attr.name === name);
if (type &amp;&amp; name) {
return this.__attributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.attributeCache;
return this.__attributeCache.slice();
}
}
@ -204,7 +211,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
@ -212,7 +219,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@ -220,7 +227,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
@ -228,7 +235,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@ -327,8 +334,8 @@ class NoteShort {
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.attributeCache = null;
invalidate__attributeCache() {
this.__attributeCache = null;
}
/**
@ -349,7 +356,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
delete dto.__attributeCache;
return dto;
}

View file

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Class: exports</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Class: exports</h1>
<section>
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>exports<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">blaa vlaa</div>
</header>
<article>
<div class="container-overview">
<h2>Constructor</h2>
<h4 class="name" id="exports"><span class="type-signature"></span>new exports<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line5">line 5</a>
</li></ul></dd>
</dl>
</div>
<h3 class="subsection-title">Members</h3>
<h4 class="name" id="JUMP_TO"><span class="type-signature"></span>JUMP_TO<span class="type-signature"></span></h4>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line7">line 7</a>
</li></ul></dd>
</dl>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View file

@ -410,6 +410,11 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
* @param {function} handler
*/
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
}
export default FrontendScriptApi;</code></pre>

View file

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: services/keyboard_action.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: services/keyboard_action.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* blaa vlaa
*/
class KeyboardAction {
constructor(params) {
this.optionName = params.optionName;
}
}
/**
* Open "Jump to note" dialog
* @static
*/
KeyboardAction.JumpToNote = new KeyboardAction({
optionName: "JumpToNote",
defaultShortcuts: "mod+j",
description: 'Open "Jump to note" dialog'
});
/** @static */
KeyboardAction.MarkdownToHTML = new KeyboardAction({
optionName: "MarkdownToHTML",
defaultShortcuts: "mod+return"
});
/** @static */
KeyboardAction.NewTab = new KeyboardAction({
optionName: "NewTab",
defaultShortcuts: "mod+t"
});
/** @static */
KeyboardAction.CloseTab = new KeyboardAction({
optionName: "CloseTab",
defaultShortcuts: "mod+w"
});
/** @static */
KeyboardAction.NextTab = new KeyboardAction({
optionName: "NextTab",
defaultShortcuts: "mod+tab"
});
/** @static */
KeyboardAction.PreviousTab = new KeyboardAction({
optionName: "PreviousTab",
defaultShortcuts: "mod+shift+tab"
});
/** @static */
KeyboardAction.CreateNoteAfter = new KeyboardAction({
optionName: "CreateNoteAfter",
defaultShortcuts: "mod+o"
});
/** @static */
KeyboardAction.CreateNoteInto = new KeyboardAction({
optionName: "CreateNoteInto",
defaultShortcuts: "mod+p"
});
/** @static */
KeyboardAction.ScrollToActiveNote = new KeyboardAction({
optionName: "ScrollToActiveNote",
defaultShortcuts: "mod+."
});
/** @static */
KeyboardAction.CollapseTree = new KeyboardAction({
optionName: "CollapseTree",
defaultShortcuts: "alt+c"
});
/** @static */
KeyboardAction.RunSQL = new KeyboardAction({
optionName: "RunSQL",
defaultShortcuts: "mod+return"
});
/** @static */
KeyboardAction.FocusNote = new KeyboardAction({
optionName: "FocusNote",
defaultShortcuts: "return"
});
/** @static */
KeyboardAction.RunCurrentNote = new KeyboardAction({
optionName: "RunCurrentNote",
defaultShortcuts: "mod+return"
});
/** @static */
KeyboardAction.ClipboardCopy = new KeyboardAction({
optionName: "ClipboardCopy",
defaultShortcuts: "mod+c"
});
/** @static */
KeyboardAction.ClipboardPaste = new KeyboardAction({
optionName: "ClipboardPaste",
defaultShortcuts: "mod+v"
});
/** @static */
KeyboardAction.ClipboardCut = new KeyboardAction({
optionName: "ClipboardCut",
defaultShortcuts: "mod+x"
});
/** @static */
KeyboardAction.SelectAllNotesInParent = new KeyboardAction({
optionName: "SelectAllNotesInParent",
defaultShortcuts: "mod+a"
});
/** @static */
KeyboardAction.Undo = new KeyboardAction({
optionName: "Undo",
defaultShortcuts: "mod+z"
});
/** @static */
KeyboardAction.Redo = new KeyboardAction({
optionName: "Redo",
defaultShortcuts: "mod+y"
});
/** @static */
KeyboardAction.AddLinkToText = new KeyboardAction({
optionName: "AddLinkToText",
defaultShortcuts: "mod+l"
});
/** @static */
KeyboardAction.CloneNotesTo = new KeyboardAction({
optionName: "CloneNotesTo",
defaultShortcuts: "mod+shift+c"
});
/** @static */
KeyboardAction.MoveNotesTo = new KeyboardAction({
optionName: "MoveNotesTo",
defaultShortcuts: "mod+shift+c"
});
/** @static */
KeyboardAction.SearchNotes = new KeyboardAction({
optionName: "SearchNotes",
defaultShortcuts: "mod+s"
});
/** @static */
KeyboardAction.ShowAttributes = new KeyboardAction({
optionName: "ShowAttributes",
defaultShortcuts: "alt+a"
});
/** @static */
KeyboardAction.ShowHelp = new KeyboardAction({
optionName: "ShowHelp",
defaultShortcuts: "f1"
});
/** @static */
KeyboardAction.OpenSQLConsole = new KeyboardAction({
optionName: "OpenSQLConsole",
defaultShortcuts: "alt+o"
});
/** @static */
KeyboardAction.BackInNoteHistory = new KeyboardAction({
optionName: "BackInNoteHistory",
defaultShortcuts: "alt+left"
});
/** @static */
KeyboardAction.ForwardInNoteHistory = new KeyboardAction({
optionName: "ForwardInNoteHistory",
defaultShortcuts: "alt+right"
});
/** @static */
KeyboardAction.ToggleZenMode = new KeyboardAction({
optionName: "ToggleZenMode",
defaultShortcuts: "alt+m"
});
/** @static */
KeyboardAction.InsertDateTime = new KeyboardAction({
optionName: "InsertDateTime",
defaultShortcuts: "alt+t"
});
/** @static */
KeyboardAction.ReloadApp = new KeyboardAction({
optionName: "ReloadApp",
defaultShortcuts: ["f5", "mod+r"]
});
/** @static */
KeyboardAction.OpenDevTools = new KeyboardAction({
optionName: "OpenDevTools",
defaultShortcuts: "mod+shift+i"
});
/** @static */
KeyboardAction.FindInText = new KeyboardAction({
optionName: "FindInText",
defaultShortcuts: "mod+f"
});
/** @static */
KeyboardAction.ToggleFullscreen = new KeyboardAction({
optionName: "ToggleFullscreen",
defaultShortcuts: "f11"
});
/** @static */
KeyboardAction.ZoomOut = new KeyboardAction({
optionName: "ZoomOut",
defaultShortcuts: "mod+-"
});
/** @static */
KeyboardAction.ZoomIn = new KeyboardAction({
optionName: "ZoomIn",
defaultShortcuts: "mod+="
});
export default KeyboardAction;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="KeyboardAction.html">KeyboardAction</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: services/keyboard_action.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: services/keyboard_action.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* blaa vlaa
*/
class KeyboardActions {
constructor() {
/** @property {string} */
this.JUMP_TO = "";
}
}
export default KeyboardActions;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="KeyboardActions.html">KeyboardActions</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View file

@ -8,6 +8,7 @@ const cls = require('./src/services/cls');
const url = require("url");
const port = require('./src/services/port');
const env = require('./src/services/env');
const keyboardActionsService = require('./src/services/keyboard_actions');
const appIconService = require('./src/services/app_icon');
const windowStateKeeper = require('electron-window-state');
@ -95,21 +96,44 @@ app.on('activate', () => {
}
});
async function registerGlobalShortcuts() {
await sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions();
for (const action of allActions) {
if (!action.effectiveShortcuts) {
continue;
}
for (const shortcut of action.effectiveShortcuts) {
if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('globalShortcut', action.actionName);
}));
if (result) {
log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`);
}
else {
log.info(`Could not register global shortcut ${translatedShortcut}`);
}
}
}
}
}
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
mainWindow = await createMainWindow();
const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('create-day-sub-note');
}));
if (!result) {
log.error("Could not register global shortcut CTRL+ALT+P");
}
registerGlobalShortcuts();
});
app.on('will-quit', () => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4501
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.37.8",
"version": "0.38.2",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@ -29,11 +29,11 @@
"csurf": "1.10.0",
"dayjs": "1.8.17",
"debug": "4.1.1",
"ejs": "2.7.1",
"ejs": "2.7.4",
"electron-debug": "3.0.1",
"electron-dl": "1.14.0",
"electron-find": "1.0.6",
"electron-spellchecker": "2.2.0",
"electron-spellchecker": "2.2.1",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.0",
@ -45,16 +45,16 @@
"http-proxy-agent": "2.1.0",
"https-proxy-agent": "3.0.1",
"image-type": "4.1.0",
"imagemin": "7.0.0",
"imagemin": "7.0.1",
"imagemin-giflossy": "5.1.10",
"imagemin-mozjpeg": "8.0.0",
"imagemin-pngquant": "8.0.0",
"ini": "1.3.5",
"jimp": "0.8.5",
"mime-types": "2.1.24",
"jimp": "0.9.3",
"mime-types": "2.1.25",
"moment": "2.24.0",
"multer": "1.4.2",
"node-abi": "2.12.0",
"node-abi": "2.13.0",
"open": "7.0.0",
"pngjs": "3.4.0",
"portscanner": "2.2.0",
@ -68,7 +68,7 @@
"session-file-store": "1.3.1",
"simple-node-logger": "18.12.23",
"sqlite": "3.0.3",
"sqlite3": "4.1.0",
"sqlite3": "4.1.1",
"string-similarity": "3.0.0",
"tar-stream": "2.1.0",
"turndown": "5.0.3",
@ -79,18 +79,10 @@
"devDependencies": {
"electron": "6.0.12",
"electron-builder": "22.1.0",
"electron-compile": "6.4.4",
"electron-installer-debian": "2.0.1",
"electron-packager": "14.1.0",
"electron-rebuild": "1.8.6",
"electron-packager": "14.1.1",
"electron-rebuild": "1.8.8",
"jsdoc": "3.6.3",
"lorem-ipsum": "2.0.3",
"xo": "0.25.3"
},
"xo": {
"envs": [
"node",
"browser"
]
"lorem-ipsum": "2.0.3"
}
}

View file

@ -79,6 +79,10 @@ class Attribute extends Entity {
async beforeSaving() {
if (!this.value) {
if (this.type === 'relation') {
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
}
// null value isn't allowed
this.value = "";
}
@ -109,6 +113,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note;
}
}

View file

@ -114,6 +114,10 @@ class Note extends Entity {
/** @returns {Promise} */
async setContent(content) {
if (content === null || content === undefined) {
throw new Error(`Cannot set null content to note ${this.noteId}`);
}
// force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.length;
@ -192,26 +196,36 @@ class Note extends Entity {
return null;
}
async loadOwnedAttributesToCache() {
this.__ownedAttributeCache = await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
return this.__ownedAttributeCache;
}
/**
* @returns {Promise<Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
* This method is a faster variant of getAttributes() which looks for only owned attributes.
* Use when inheritance is not needed and/or in batch/performance sensitive operations.
*
* This method can be significantly faster than the getAttributes()
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} note's "owned" attributes - excluding inherited ones
*/
async getOwnedAttributes(type, name) {
let query = `SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`;
const params = [this.noteId];
if (type) {
query += ` AND type = ?`;
params.push(type);
if (!this.__ownedAttributeCache) {
await this.loadOwnedAttributesToCache();
}
if (name) {
query += ` AND name = ?`;
params.push(name);
if (type && name) {
return this.__ownedAttributeCache.filter(attr => attr.type === type && attr.name === name);
}
else if (type) {
return this.__ownedAttributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__ownedAttributeCache.filter(attr => attr.name === name);
}
else {
return this.__ownedAttributeCache.slice();
}
return await repository.getEntities(query, params);
}
/**
@ -233,19 +247,26 @@ class Note extends Entity {
}
/**
* @param {string} [name] - attribute name to filter
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
async getAttributes(type, name) {
if (!this.__attributeCache) {
await this.loadAttributesToCache();
}
if (name) {
if (type && name) {
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.__attributeCache;
return this.__attributeCache.slice();
}
}
@ -254,7 +275,15 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), excluding inherited ones
*/
async getOwnedLabels(name) {
return await this.getOwnedAttributes(LABEL, name);
}
/**
@ -262,7 +291,7 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@ -270,7 +299,15 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones
*/
async getOwnedRelations(name) {
return await this.getOwnedAttributes(RELATION, name);
}
/**
@ -293,7 +330,7 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@ -302,6 +339,7 @@ class Note extends Entity {
*/
invalidateAttributeCache() {
this.__attributeCache = null;
this.__ownedAttributeCache = null;
}
/** @returns {Promise<void>} */
@ -311,11 +349,10 @@ class Note extends Entity {
tree(noteId, level) AS (
SELECT ?, 0
UNION
SELECT branches.parentNoteId, tree.level + 1 FROM branches
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
WHERE branches.isDeleted = 0
),
treeWithAttrs(noteId, level) AS (
SELECT * FROM tree
@ -382,6 +419,15 @@ class Note extends Entity {
return !!await this.getAttribute(type, name);
}
/**
* @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 (excluding inherited)
*/
async hasOwnedAttribute(type, name) {
return !!await this.getOwnedAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@ -396,7 +442,7 @@ class Note extends Entity {
/**
* @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.
* @returns {Promise<string|null>} 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);
@ -404,6 +450,17 @@ class Note extends Entity {
return attr ? attr.value : null;
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
*/
async getOwnedAttributeValue(type, name) {
const attr = await this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
}
/**
* Based on enabled, attribute is either set or removed.
*
@ -431,7 +488,7 @@ class Note extends Entity {
* @returns {Promise<void>}
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
let attr = attributes.find(attr => attr.type === type && attr.name === name);
if (attr) {
@ -465,7 +522,7 @@ class Note extends Entity {
* @returns {Promise<void>}
*/
async removeAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type && (value === undefined || value === attribute.value)) {
@ -477,42 +534,104 @@ class Note extends Entity {
}
}
/**
* @return {Promise<Attribute>}
*/
async addAttribute(type, name, value = "") {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
});
await attr.save();
this.invalidateAttributeCache();
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
}
/**
* @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 - label name
* @returns {Promise<boolean>} true if label exists (excluding inherited)
*/
async hasOwnedLabel(name) { return await this.hasOwnedAttribute(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 - relation name
* @returns {Promise<boolean>} true if relation exists (excluding inherited)
*/
async hasOwnedRelation(name) { return await this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute>} label if it exists, null otherwise
* @returns {Promise<Attribute|null>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute|null>} label if it exists, null otherwise
*/
async getOwnedLabel(name) { return await this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute>} relation if it exists, null otherwise
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise
*/
async getOwnedRelation(name) { return await this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<string>} label value if label exists, null otherwise
* @returns {Promise<string|null>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<string|null>} label value if label exists, null otherwise
*/
async getOwnedLabelValue(name) { return await this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string>} relation value if relation exists, null otherwise
* @returns {Promise<string|null>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string|null>} relation value if relation exists, null otherwise
*/
async getOwnedRelationValue(name) { return await this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
@ -523,6 +642,16 @@ class Note extends Entity {
return relation ? await repository.getNote(relation.value) : null;
}
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getOwnedRelationTarget(name) {
const relation = await this.getOwnedRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Based on enabled, label is either set or removed.
*
@ -676,7 +805,7 @@ class Note extends Entity {
WHERE noteId = ? AND
isDeleted = 0 AND
type = 'relation' AND
name IN ('internal-link', 'image-link', 'relation-map-link')`, [this.noteId]);
name IN ('internalLink', 'imageLink', 'relationMapLink')`, [this.noteId]);
}
/**
@ -753,6 +882,16 @@ class Note extends Entity {
return notePaths;
}
/**
* @param ancestorNoteId
* @return {Promise<boolean>} - true if ancestorNoteId occurs in at least one of the note's paths
*/
async isDescendantOfNote(ancestorNoteId) {
const notePaths = await this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId));
}
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
@ -792,6 +931,7 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.__ownedAttributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;

View file

@ -30,6 +30,7 @@ import cssLoader from './services/css_loader.js';
import dateNoteService from './services/date_notes.js';
import sidebarService from './services/sidebar.js';
import importService from './services/import.js';
import keyboardActionService from "./services/keyboard_actions.js";
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
@ -108,18 +109,8 @@ $("body").on("click", "a.external", function () {
});
if (utils.isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event) {
const todayNote = await dateNoteService.getTodayNote();
const notePath = await treeService.getSomeNotePath(todayNote);
const node = await treeService.expandToNote(notePath);
await noteDetailService.openEmptyTab(false);
await treeService.createNote(node, todayNote.noteId, 'into', {
type: "text",
isProtected: node.data.isProtected
});
require('electron').ipcRenderer.on('globalShortcut', async function(event, actionName) {
keyboardActionService.triggerAction(actionName);
});
}
@ -136,7 +127,7 @@ $noteTabContainer.on("click", ".export-note-button", function () {
$noteTabContainer.on("click", ".import-files-button",
() => import('./dialogs/import.js').then(d => d.showDialog(treeService.getActiveNode())));
$noteTabContainer.on("click", ".print-note-button", async function () {
async function printActiveNote() {
if ($(this).hasClass("disabled")) {
return;
}
@ -154,7 +145,11 @@ $noteTabContainer.on("click", ".print-note-button", async function () {
loadCSS: "libraries/codemirror/codemirror.css",
debug: true
});
});
}
keyboardActionService.setGlobalActionHandler("PrintActiveNote", printActiveNote);
$noteTabContainer.on("click", ".print-note-button", printActiveNote);
$('[data-toggle="tooltip"]').tooltip({
html: true

View file

@ -0,0 +1,32 @@
import server from "../services/server.js";
import utils from "../services/utils.js";
const $dialog = $("#backend-log-dialog");
const $backendLogTextArea = $("#backend-log-textarea");
const $refreshBackendLog = $("#refresh-backend-log-button");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
load();
}
function scrollToBottom() {
$backendLogTextArea.scrollTop($backendLogTextArea[0].scrollHeight);
}
async function load() {
const backendLog = await server.get('backend-log');
$backendLogTextArea.text(backendLog);
scrollToBottom();
}
$refreshBackendLog.on('click', load);
$dialog.on('shown.bs.modal', scrollToBottom);

View file

@ -17,11 +17,21 @@ export async function showDialog(node) {
glob.activeDialog = $dialog;
$dialog.modal();
branchId = node.data.branchId;
const branch = treeCache.getBranch(branchId);
if (branch.noteId === 'root') {
return;
}
const parentNote = await treeCache.getNote(branch.parentNoteId);
if (parentNote.type === 'search') {
return;
}
$dialog.modal();
$treePrefixInput.val(branch.prefix);
const noteTitle = await treeUtils.getNoteTitle(node.data.noteId);

View file

@ -26,6 +26,10 @@ async function convertMarkdownToHtml(text) {
}
export async function importMarkdownInline() {
if (noteDetailService.getActiveTabNoteType() !== 'text') {
return;
}
if (utils.isElectron()) {
const {clipboard} = require('electron');
const text = clipboard.readText();

View file

@ -22,6 +22,7 @@ export async function showDialog() {
import('./options/other.js'),
import('./options/sidebar.js'),
import('./options/sync.js'),
import('./options/keyboard_shortcuts.js'),
]))
.map(m => new m.default)
.forEach(tab => {

View file

@ -3,19 +3,23 @@ import toastService from "../../services/toast.js";
const TPL = `
<h4 style="margin-top: 0;">Sync</h4>
<button id="force-full-sync-button" class="btn btn-secondary">Force full sync</button>
<button id="force-full-sync-button" class="btn">Force full sync</button>
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn btn-secondary">Fill sync rows</button>
<button id="fill-sync-rows-button" class="btn">Fill sync rows</button>
<br/>
<br/>
<h4>Consistency checks</h4>
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn btn-secondary">Save anonymized database</button><br/><br/>
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
@ -24,7 +28,7 @@ const TPL = `
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn btn-secondary">Vacuum database</button>`;
<button id="vacuum-database-button" class="btn">Vacuum database</button>`;
export default class AdvancedOptions {
constructor() {
@ -33,9 +37,8 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$anonymizeButton = $("#anonymize-button");
this.$cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
this.$cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
this.$forceFullSyncButton.on('click', async () => {
await server.post('sync/force-full-sync');
@ -55,26 +58,16 @@ export default class AdvancedOptions {
toastService.showMessage("Created anonymized database");
});
this.$cleanupSoftDeletedButton.on('click', async () => {
if (confirm("Do you really want to clean up soft-deleted items?")) {
await server.post('cleanup/cleanup-soft-deleted-items');
toastService.showMessage("Soft deleted items have been cleaned up");
}
});
this.$cleanupUnusedImagesButton.on('click', async () => {
if (confirm("Do you really want to clean up unused images?")) {
await server.post('cleanup/cleanup-unused-images');
toastService.showMessage("Unused images have been cleaned up");
}
});
this.$vacuumDatabaseButton.on('click', async () => {
await server.post('cleanup/vacuum-database');
toastService.showMessage("Database has been vacuumed");
});
this.$findAndFixConsistencyIssuesButton.on('click', async () => {
await server.post('cleanup/find-and-fix-consistency-issues');
toastService.showMessage("Consistency issues should be fixed.");
});
}
}

View file

@ -0,0 +1,141 @@
import server from "../../services/server.js";
import utils from "../../services/utils.js";
const TPL = `
<h4>Keyboard shortcuts</h4>
<p>Multiple shortcuts for the same action can be separated by comma.</p>
<div class="form-group">
<input type="text" class="form-control" id="keyboard-shortcut-filter" placeholder="Type text to filter shortcuts...">
</div>
<div style="overflow: auto; height: 500px;">
<table id="keyboard-shortcut-table" cellpadding="10">
<thead>
<tr>
<th>Action name</th>
<th>Shortcuts</th>
<th>Default shortcuts</th>
<th>Description</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div style="display: flex; justify-content: space-between">
<button class="btn btn-primary" id="options-keyboard-shortcuts-reload-app">Reload app to apply changes</button>
<button class="btn" id="options-keyboard-shortcuts-set-all-to-default">Set all shortcuts to the default</button>
</div>
`;
let globActions;
export default class KeyboardShortcutsOptions {
constructor() {
$("#options-keyboard-shortcuts").html(TPL);
$("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadApp());
const $table = $("#keyboard-shortcut-table tbody");
server.get('keyboard-actions').then(actions => {
globActions = actions;
for (const action of actions) {
const $tr = $("<tr>");
if (action.separator) {
$tr.append(
$('<td colspan="4">')
.attr("style","background-color: var(--accented-background-color); font-weight: bold;")
.text(action.separator)
)
}
else {
$tr.append($("<td>").text(action.actionName))
.append($("<td>").append(
$(`<input type="text" class="form-control">`)
.val(action.effectiveShortcuts.join(", "))
.attr('data-keyboard-action-name', action.actionName)
.attr('data-default-keyboard-shortcuts', action.defaultShortcuts.join(", "))
)
)
.append($("<td>").text(action.defaultShortcuts.join(", ")))
.append($("<td>").text(action.description));
}
$table.append($tr);
}
});
$table.on('change', 'input.form-control', e => {
const $input = $(e.target);
const actionName = $input.attr('data-keyboard-action-name');
const shortcuts = $input.val()
.replace('+,', "+Comma")
.split(",")
.map(shortcut => shortcut.replace("+Comma", "+,"))
.filter(shortcut => !!shortcut);
const opts = {};
opts['keyboardShortcuts' + actionName] = JSON.stringify(shortcuts);
server.put('options', opts);
});
$("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
const confirmDialog = await import('../confirm.js');
if (!await confirmDialog.confirm("Do you really want to reset all keyboard shortcuts to the default?")) {
return;
}
$table.find('input.form-control').each(function() {
const defaultShortcuts = $(this).attr('data-default-keyboard-shortcuts');
if ($(this).val() !== defaultShortcuts) {
$(this)
.val(defaultShortcuts)
.trigger('change');
}
});
});
const $filter = $("#keyboard-shortcut-filter");
$filter.on('keyup', () => {
const filter = $filter.val().trim().toLowerCase();
$table.find("tr").each((i, el) => {
if (!filter) {
$(el).show();
return;
}
const actionName = $(el).find('input').attr('data-keyboard-action-name');
if (!actionName) {
$(el).hide();
return;
}
const action = globActions.find(act => act.actionName === actionName);
if (!action) {
$(el).hide();
return;
}
$(el).toggle(!!( // !! to avoid toggle overloads with different behavior
action.actionName.toLowerCase().includes(filter)
|| action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|| action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|| (action.description && action.description.toLowerCase().includes(filter))
));
});
});
}
}

View file

@ -17,7 +17,7 @@ const TPL = `
<div class="form-group">
<label for="spell-check-language-code">Language code</label>
<input type="email" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
</div>
<p>Changes to the spell check options will take effect after application restart.</p>
@ -132,4 +132,4 @@ export default class ProtectedSessionOptions {
this.$imageMaxWidthHeight.val(options['imageMaxWidthHeight']);
this.$imageJpegQuality.val(options['imageJpegQuality']);
}
}
}

View file

@ -25,7 +25,7 @@ const TPL = `
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary" type="button" data-help-page="Synchronization">Help</button>
<button class="btn" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
@ -35,7 +35,7 @@ const TPL = `
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<button id="test-sync-button" class="btn btn-secondary">Test sync</button>`;
<button id="test-sync-button" class="btn">Test sync</button>`;
export default class SyncOptions {
constructor() {

View file

@ -6,9 +6,8 @@ import utils from "../services/utils.js";
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
const $tables = $("#sql-console-tables");
const $tableSchemas = $("#sql-console-table-schemas");
const $resultContainer = $("#result-container");
let codeEditor;
@ -19,7 +18,7 @@ export async function showDialog() {
glob.activeDialog = $dialog;
await showTables();
await showTableSchemas();
$dialog.modal();
}
@ -45,6 +44,10 @@ async function initEditor() {
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.setValue(`SELECT title, isProtected, type, mime FROM notes WHERE noteId = 'root';
---
SELECT noteId, parentNoteId, notePosition, prefix FROM branches WHERE branchId = 'root';`);
}
codeEditor.focus();
@ -70,55 +73,66 @@ async function execute() {
toastService.showMessage("Query was executed successfully.");
}
const rows = result.rows;
const results = result.results;
$resultHead.empty();
$resultBody.empty();
$resultContainer.empty();
for (const rows of results) {
if (rows.length === 0) {
continue;
}
const $table = $('<table class="table table-striped">');
$resultContainer.append($table);
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
const $row = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
$row.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
$table.append($row);
for (const result of rows) {
const rowEl = $("<tr>");
for (const result of rows) {
const $row = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
for (const key in result) {
$row.append($("<td>").html(result[key]));
}
$table.append($row);
}
$resultBody.append(rowEl);
}
}
async function showTables() {
async function showTableSchemas() {
const tables = await server.get('sql/schema');
$tables.empty();
$tableSchemas.empty();
for (const table of tables) {
const $tableLink = $('<button class="btn">').text(table.name);
const $columns = $("<table>");
const $columns = $("<ul>");
for (const column of table.columns) {
$columns.append(
$("<tr>")
.append($("<td>").text(column.name))
.append($("<td>").text(column.type))
$("<li>")
.append($("<span>").text(column.name))
.append($("<span>").text(column.type))
);
}
$tables.append($tableLink).append(" ");
$tableSchemas.append($tableLink).append(" ");
$tableLink
.tooltip({html: true, title: $columns.html()})
.tooltip({
html: true,
placement: 'bottom',
boundary: 'window',
title: $columns[0].outerHTML
})
.on('click', () => codeEditor.setValue("SELECT * FROM " + table.name + " LIMIT 100"));
}
}

View file

@ -6,7 +6,6 @@ class Branch {
this.branchId = row.branchId;
/** @param {string} */
this.noteId = row.noteId;
this.note = null;
/** @param {string} */
this.parentNoteId = row.parentNoteId;
/** @param {int} */
@ -19,7 +18,7 @@ class Branch {
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
return this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */

View file

@ -154,20 +154,27 @@ class NoteShort {
}
/**
* @param {string} [name] - attribute name to filter
* @returns {Promise<Attribute[]>}
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
if (!this.attributeCache) {
this.attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
async getAttributes(type, name) {
if (!this.__attributeCache) {
this.__attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
.map(attrRow => new Attribute(this.treeCache, attrRow));
}
if (name) {
return this.attributeCache.filter(attr => attr.name === name);
if (type && name) {
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.attributeCache;
return this.__attributeCache.slice();
}
}
@ -176,7 +183,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
@ -184,7 +191,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@ -192,7 +199,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
@ -200,7 +207,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@ -299,8 +306,8 @@ class NoteShort {
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.attributeCache = null;
invalidate__attributeCache() {
this.__attributeCache = null;
}
/**
@ -321,7 +328,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
delete dto.__attributeCache;
return dto;
}

View file

@ -1,3 +1,4 @@
import keyboardActionService from './keyboard_actions.js';
const $contextMenuContainer = $("#context-menu-container");
let dateContextMenuOpenedMs = 0;
@ -69,6 +70,8 @@ async function initContextMenu(event, contextMenu) {
addItems($contextMenuContainer, await contextMenu.getContextMenuItems());
keyboardActionService.updateDisplayedShortcuts($contextMenuContainer);
// code below tries to detect when dropdown would overflow from page
// in such case we'll position it above click coordinates so it will fit into client
const clickPosition = event.pageY;

View file

@ -4,6 +4,11 @@ import zoomService from "./zoom.js";
import protectedSessionService from "./protected_session.js";
import searchNotesService from "./search_notes.js";
import treeService from "./tree.js";
import dateNoteService from "./date_notes.js";
import noteDetailService from "./note_detail.js";
import keyboardActionService from "./keyboard_actions.js";
import hoistedNoteService from "./hoisted_note.js";
import treeCache from "./tree_cache.js";
const NOTE_REVISIONS = "../dialogs/note_revisions.js";
const OPTIONS = "../dialogs/options.js";
@ -12,6 +17,7 @@ const JUMP_TO_NOTE = "../dialogs/jump_to_note.js";
const NOTE_SOURCE = "../dialogs/note_source.js";
const RECENT_CHANGES = "../dialogs/recent_changes.js";
const SQL_CONSOLE = "../dialogs/sql_console.js";
const BACKEND_LOG = "../dialogs/backend_log.js";
const ATTRIBUTES = "../dialogs/attributes.js";
const HELP = "../dialogs/help.js";
const NOTE_INFO = "../dialogs/note_info.js";
@ -26,92 +32,112 @@ function registerEntrypoints() {
jQuery.hotkeys.options.filterContentEditable = false;
jQuery.hotkeys.options.filterTextInputs = false;
utils.bindGlobalShortcut('ctrl+l', () => import(ADD_LINK).then(d => d.showDialog()));
utils.bindGlobalShortcut('ctrl+shift+l', () => import(ADD_LINK).then(d => d.showDialogForClone()));
keyboardActionService.setGlobalActionHandler("AddLinkToText", () => import(ADD_LINK).then(d => d.showDialog()));
$("#jump-to-note-dialog-button").on('click', () => import(JUMP_TO_NOTE).then(d => d.showDialog()));
utils.bindGlobalShortcut('ctrl+j', () => import(JUMP_TO_NOTE).then(d => d.showDialog()));
const showJumpToNoteDialog = () => import(JUMP_TO_NOTE).then(d => d.showDialog());
$("#jump-to-note-dialog-button").on('click', showJumpToNoteDialog);
keyboardActionService.setGlobalActionHandler("JumpToNote", showJumpToNoteDialog);
$("#recent-changes-button").on('click', () => import(RECENT_CHANGES).then(d => d.showDialog()));
const showRecentChanges = () => import(RECENT_CHANGES).then(d => d.showDialog());
$("#recent-changes-button").on('click', showRecentChanges);
keyboardActionService.setGlobalActionHandler("ShowRecentChanges", showRecentChanges);
$("#enter-protected-session-button").on('click', protectedSessionService.enterProtectedSession);
$("#leave-protected-session-button").on('click', protectedSessionService.leaveProtectedSession);
$("#toggle-search-button").on('click', searchNotesService.toggleSearch);
utils.bindGlobalShortcut('ctrl+s', searchNotesService.toggleSearch);
keyboardActionService.setGlobalActionHandler('SearchNotes', searchNotesService.toggleSearch);
const $noteTabContainer = $("#note-tab-container");
$noteTabContainer.on("click", ".show-attributes-button", () => import(ATTRIBUTES).then(d => d.showDialog()));
utils.bindGlobalShortcut('alt+a', () => import(ATTRIBUTES).then(d => d.showDialog()));
$noteTabContainer.on("click", ".show-note-info-button", () => import(NOTE_INFO).then(d => d.showDialog()));
const showAttributesDialog = () => import(ATTRIBUTES).then(d => d.showDialog());
$noteTabContainer.on("click", ".show-attributes-button", showAttributesDialog);
keyboardActionService.setGlobalActionHandler("ShowAttributes", showAttributesDialog);
$noteTabContainer.on("click", ".show-note-revisions-button", function() {
const showNoteInfoDialog = () => import(NOTE_INFO).then(d => d.showDialog());
$noteTabContainer.on("click", ".show-note-info-button", showNoteInfoDialog);
keyboardActionService.setGlobalActionHandler("ShowNoteInfo", showNoteInfoDialog);
const showNoteRevisionsDialog = function() {
if ($(this).hasClass("disabled")) {
return;
}
import(NOTE_REVISIONS).then(d => d.showCurrentNoteRevisions());
});
};
$noteTabContainer.on("click", ".show-source-button", function() {
$noteTabContainer.on("click", ".show-note-revisions-button", showNoteRevisionsDialog);
keyboardActionService.setGlobalActionHandler("ShowNoteRevisions", showNoteRevisionsDialog);
const showNoteSourceDialog = function() {
if ($(this).hasClass("disabled")) {
return;
}
import(NOTE_SOURCE).then(d => d.showDialog());
});
};
$noteTabContainer.on("click", ".show-link-map-button", function() {
import(LINK_MAP).then(d => d.showDialog());
});
$noteTabContainer.on("click", ".show-source-button", showNoteSourceDialog);
keyboardActionService.setGlobalActionHandler("ShowNoteSource", showNoteSourceDialog);
$("#options-button").on('click', () => import(OPTIONS).then(d => d.showDialog()));
const showLinkMapDialog = () => import(LINK_MAP).then(d => d.showDialog());
$noteTabContainer.on("click", ".show-link-map-button", showLinkMapDialog);
keyboardActionService.setGlobalActionHandler("ShowLinkMap", showLinkMapDialog);
$("#show-help-button").on('click', () => import(HELP).then(d => d.showDialog()));
utils.bindGlobalShortcut('f1', () => import(HELP).then(d => d.showDialog()));
const showOptionsDialog = () => import(OPTIONS).then(d => d.showDialog());
$("#options-button").on('click', showOptionsDialog);
keyboardActionService.setGlobalActionHandler("ShowOptions", showOptionsDialog);
$("#open-sql-console-button").on('click', () => import(SQL_CONSOLE).then(d => d.showDialog()));
utils.bindGlobalShortcut('alt+o', () => import(SQL_CONSOLE).then(d => d.showDialog()));
const showHelpDialog = () => import(HELP).then(d => d.showDialog());
$("#show-help-button").on('click', showHelpDialog);
keyboardActionService.setGlobalActionHandler("ShowHelp", showHelpDialog);
const showSqlConsoleDialog = () => import(SQL_CONSOLE).then(d => d.showDialog());
$("#open-sql-console-button").on('click', showSqlConsoleDialog);
keyboardActionService.setGlobalActionHandler("ShowSQLConsole", showSqlConsoleDialog);
const showBackendLogDialog = () => import(BACKEND_LOG).then(d => d.showDialog());
$("#show-backend-log-button").on('click', showBackendLogDialog);
keyboardActionService.setGlobalActionHandler("ShowBackendLog", showBackendLogDialog);
$("#show-about-dialog-button").on('click', () => import(ABOUT).then(d => d.showDialog()));
if (utils.isElectron()) {
$("#history-navigation").show();
$("#history-back-button").on('click', window.history.back);
$("#history-forward-button").on('click', window.history.forward);
keyboardActionService.setGlobalActionHandler("BackInNoteHistory", window.history.back);
if (utils.isMac()) {
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
utils.bindGlobalShortcut('meta+left', window.history.back);
utils.bindGlobalShortcut('meta+right', window.history.forward);
}
else {
utils.bindGlobalShortcut('alt+left', window.history.back);
utils.bindGlobalShortcut('alt+right', window.history.forward);
}
$("#history-forward-button").on('click', window.history.forward);
keyboardActionService.setGlobalActionHandler("ForwardInNoteHistory", window.history.forward);
}
utils.bindGlobalShortcut('alt+m', e => {
$(".hide-toggle").toggle();
$("#container").toggleClass("distraction-free-mode");
});
// hide (toggle) everything except for the note content for zen mode
const toggleZenMode = () => {
$(".hide-in-zen-mode").toggle();
$("#container").toggleClass("zen-mode");
};
// hide (toggle) everything except for the note content for distraction free writing
utils.bindGlobalShortcut('alt+t', e => {
$("#toggle-zen-mode-button").on('click', toggleZenMode);
keyboardActionService.setGlobalActionHandler("ToggleZenMode", toggleZenMode);
keyboardActionService.setGlobalActionHandler("InsertDateTimeToText", () => {
const date = new Date();
const dateString = utils.formatDateTime(date);
linkService.addTextToEditor(dateString);
});
utils.bindGlobalShortcut('f5', utils.reloadApp);
$("#reload-frontend-button").on('click', utils.reloadApp);
utils.bindGlobalShortcut('ctrl+r', utils.reloadApp);
keyboardActionService.setGlobalActionHandler("ReloadFrontendApp", utils.reloadApp);
$("#open-dev-tools-button").toggle(utils.isElectron());
keyboardActionService.setGlobalActionHandler("PasteMarkdownIntoText", async () => {
const dialog = await import("../dialogs/markdown_import.js");
dialog.importMarkdownInline();
});
if (utils.isElectron()) {
const openDevTools = () => {
require('electron').remote.getCurrentWindow().toggleDevTools();
@ -119,8 +145,8 @@ function registerEntrypoints() {
return false;
};
utils.bindGlobalShortcut('ctrl+shift+i', openDevTools);
$("#open-dev-tools-button").on('click', openDevTools);
keyboardActionService.setGlobalActionHandler("OpenDevTools", openDevTools);
}
let findInPage;
@ -141,18 +167,16 @@ function registerEntrypoints() {
textHoverBgColor: '#555',
caseSelectedColor: 'var(--main-border-color)'
});
}
if (utils.isElectron()) {
utils.bindGlobalShortcut('ctrl+f', () => {
findInPage.openFindWindow();
return false;
keyboardActionService.setGlobalActionHandler("FindInText", () => {
if (!glob.activeDialog || !glob.activeDialog.is(":visible")) {
findInPage.openFindWindow();
}
});
}
if (utils.isElectron()) {
const toggleFullscreen = function() {
const toggleFullscreen = () => {
const win = require('electron').remote.getCurrentWindow();
if (win.isFullScreenable()) {
@ -164,7 +188,7 @@ function registerEntrypoints() {
$("#toggle-fullscreen-button").on('click', toggleFullscreen);
utils.bindGlobalShortcut('f11', toggleFullscreen);
keyboardActionService.setGlobalActionHandler("ToggleFullscreen", toggleFullscreen);
}
else {
// outside of electron this is handled by the browser
@ -172,8 +196,8 @@ function registerEntrypoints() {
}
if (utils.isElectron()) {
utils.bindGlobalShortcut('ctrl+-', zoomService.decreaseZoomFactor);
utils.bindGlobalShortcut('ctrl+=', zoomService.increaseZoomFactor);
keyboardActionService.setGlobalActionHandler("ZoomOut", zoomService.decreaseZoomFactor);
keyboardActionService.setGlobalActionHandler("ZoomIn", zoomService.increaseZoomFactor);
}
$(document).on('click', "a[data-action='note-revision']", async event => {
@ -188,7 +212,7 @@ function registerEntrypoints() {
return false;
});
utils.bindGlobalShortcut('ctrl+shift+c', () => import(CLONE_TO).then(d => {
keyboardActionService.setGlobalActionHandler("CloneNotesTo", () => import(CLONE_TO).then(d => {
const activeNode = treeService.getActiveNode();
const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode);
@ -198,14 +222,57 @@ function registerEntrypoints() {
d.showDialog(noteIds);
}));
utils.bindGlobalShortcut('ctrl+shift+x', () => import(MOVE_TO).then(d => {
keyboardActionService.setGlobalActionHandler("MoveNotesTo", () => import(MOVE_TO).then(d => {
const activeNode = treeService.getActiveNode();
const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode);
d.showDialog(selectedOrActiveNodes);
}));
keyboardActionService.setGlobalActionHandler("CreateNoteIntoDayNote", async () => {
const todayNote = await dateNoteService.getTodayNote();
const notePath = await treeService.getSomeNotePath(todayNote);
const node = await treeService.expandToNote(notePath);
const {note} = await treeService.createNote(node, todayNote.noteId, 'into', {
type: "text",
isProtected: node.data.isProtected
});
await noteDetailService.openInTab(note.noteId, true);
});
keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => {
const node = treeService.getActiveNode();
const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js");
editBranchPrefixDialog.showDialog(node);
});
keyboardActionService.setGlobalActionHandler("ToggleNoteHoisting", async () => {
const node = treeService.getActiveNode();
hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => {
if (node.data.noteId === hoistedNoteId) {
hoistedNoteService.unhoist();
}
else {
const note = await treeCache.getNote(node.data.noteId);
if (note.type !== 'search') {
hoistedNoteService.setHoistedNoteId(node.data.noteId);
}
}
});
});
keyboardActionService.setGlobalActionHandler("SearchInSubtree", () => {
const node = treeService.getActiveNode();
searchNotesService.searchInSubtree(node.data.noteId);
});
}
export default {

View file

@ -382,6 +382,17 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
* @param {function} handler
*/
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* Trilium runs in backend and frontend process, when something is changed on the backend from script,
* frontend will get asynchronously synchronized.
*
* This method returns a promise which resolves once all the backend -> frontend synchronization is finished.
* Typical use case is when new note has been created, we should wait until it is synced into frontend and only then activate it.
*
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
}
export default FrontendScriptApi;

View file

@ -0,0 +1,117 @@
import server from "./server.js";
import utils from "./utils.js";
const keyboardActionRepo = {};
const keyboardActionsLoaded = server.get('keyboard-actions').then(actions => {
for (const action of actions) {
keyboardActionRepo[action.actionName] = action;
}
});
server.get('keyboard-shortcuts-for-notes').then(shortcutForNotes => {
for (const shortcut in shortcutForNotes) {
utils.bindGlobalShortcut(shortcut, async () => {
const treeService = (await import("./tree.js")).default;
treeService.activateNote(shortcutForNotes[shortcut]);
});
}
});
function setGlobalActionHandler(actionName, handler) {
keyboardActionsLoaded.then(() => {
const action = keyboardActionRepo[actionName];
if (!action) {
throw new Error(`Cannot find keyboard action '${actionName}'`);
}
action.handler = handler;
for (const shortcut of action.effectiveShortcuts) {
if (shortcut && !shortcut.startsWith("global:")) { // global shortcuts should be handled in the electron code
utils.bindGlobalShortcut(shortcut, handler);
}
}
});
}
function setElementActionHandler($el, actionName, handler) {
keyboardActionsLoaded.then(() => {
const action = keyboardActionRepo[actionName];
if (!action) {
throw new Error(`Cannot find keyboard action '${actionName}'`);
}
// not setting action.handler since this is not global
for (const shortcut of action.effectiveShortcuts) {
if (shortcut) {
utils.bindElShortcut($el, shortcut, handler);
}
}
});
}
async function triggerAction(actionName) {
const action = await getAction(actionName);
if (!action.handler) {
throw new Error(`Action ${actionName} has no handler`);
}
await action.handler();
}
async function getAction(actionName, silent = false) {
await keyboardActionsLoaded;
const action = keyboardActionRepo[actionName];
if (!action) {
if (silent) {
console.log(`Cannot find action ${actionName}`);
}
else {
throw new Error(`Cannot find action ${actionName}`);
}
}
return action;
}
function updateDisplayedShortcuts($container) {
$container.find('kbd[data-kb-action]').each(async (i, el) => {
const actionName = $(el).attr('data-kb-action');
const action = await getAction(actionName, true);
if (action) {
$(el).text(action.effectiveShortcuts.join(', '));
}
});
$container.find('button[data-kb-action],a.icon-action[data-kb-action]').each(async (i, el) => {
const actionName = $(el).attr('data-kb-action');
const action = await getAction(actionName, true);
if (action) {
const title = $(el).attr('title');
const shortcuts = action.effectiveShortcuts.join(', ');
const newTitle = !title || !title.trim() ? shortcuts : `${title} (${shortcuts})`;
$(el).attr('title', newTitle);
}
});
}
$(() => updateDisplayedShortcuts($(document)));
export default {
setGlobalActionHandler,
setElementActionHandler,
triggerAction,
getAction,
updateDisplayedShortcuts
};

View file

@ -0,0 +1,16 @@
class Actions {
constructor() {
this.JUMP_TO = "";
}
}
const actions = new Actions();
function bind() {
}
export default {
actions,
bind
};

View file

@ -72,7 +72,7 @@ function goToLink(e) {
if (notePath) {
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
noteDetailService.openInTab(notePath);
noteDetailService.openInTab(notePath, false);
}
else if (e.which === 1) {
treeService.activateNote(notePath);

View file

@ -1,6 +1,7 @@
import optionsService from "./options.js";
const MIME_TYPES_DICT = [
{ default: true, title: "Plain text", mime: "text/plain" },
{ title: "APL", mime: "text/apl" },
{ title: "PGP", mime: "application/pgp" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
@ -91,7 +92,6 @@ const MIME_TYPES_DICT = [
{ default: true, title: "Perl", mime: "text/x-perl" },
{ default: true, title: "PHP", mime: "text/x-php" },
{ title: "Pig", mime: "text/x-pig" },
{ title: "Plain Text", mime: "text/plain" },
{ title: "PLSQL", mime: "text/x-plsql" },
{ title: "PostgreSQL", mime: "text/x-pgsql" },
{ title: "PowerShell", mime: "application/x-powershell" },
@ -168,7 +168,7 @@ function loadMimeTypes(options) {
|| MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime);
for (const mt of mimeTypes) {
mt.enabled = enabledMimeTypes.includes(mt.mime);
mt.enabled = enabledMimeTypes.includes(mt.mime) || mt.mime === 'text/plain'; // text/plain is always enabled
}
}

View file

@ -8,6 +8,7 @@ import utils from "./utils.js";
import contextMenuService from "./context_menu.js";
import treeUtils from "./tree_utils.js";
import tabRow from "./tab_row.js";
import keyboardActionService from "./keyboard_actions.js";
const $tabContentsContainer = $("#note-tab-container");
const $savedIndicator = $(".saved-indicator");
@ -34,8 +35,8 @@ async function reloadAllTabs() {
}
}
async function openInTab(notePath) {
await loadNoteDetail(notePath, { newTab: true });
async function openInTab(notePath, activate) {
await loadNoteDetail(notePath, { newTab: true, activate });
}
async function switchToNote(notePath) {
@ -421,33 +422,31 @@ $(tabRow.el).on('contextmenu', '.note-tab', e => {
});
});
if (utils.isElectron()) {
utils.bindGlobalShortcut('ctrl+t', () => {
openEmptyTab();
});
keyboardActionService.setGlobalActionHandler('OpenNewTab', () => {
openEmptyTab();
});
utils.bindGlobalShortcut('ctrl+w', () => {
if (tabRow.activeTabEl) {
tabRow.removeTab(tabRow.activeTabEl);
}
});
keyboardActionService.setGlobalActionHandler('CloseActiveTab', () => {
if (tabRow.activeTabEl) {
tabRow.removeTab(tabRow.activeTabEl);
}
});
utils.bindGlobalShortcut('ctrl+tab', () => {
const nextTab = tabRow.nextTabEl;
keyboardActionService.setGlobalActionHandler('ActivateNextTab', () => {
const nextTab = tabRow.nextTabEl;
if (nextTab) {
tabRow.activateTab(nextTab);
}
});
if (nextTab) {
tabRow.activateTab(nextTab);
}
});
utils.bindGlobalShortcut('ctrl+shift+tab', () => {
const prevTab = tabRow.previousTabEl;
keyboardActionService.setGlobalActionHandler('ActivatePreviousTab', () => {
const prevTab = tabRow.previousTabEl;
if (prevTab) {
tabRow.activateTab(prevTab);
}
});
}
if (prevTab) {
tabRow.activateTab(prevTab);
}
});
tabRow.addListener('activeTabChange', openTabsChanged);
tabRow.addListener('tabRemove', openTabsChanged);

View file

@ -3,7 +3,7 @@ import bundleService from "./bundle.js";
import toastService from "./toast.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
import utils from "./utils.js";
import keyboardActionService from "./keyboard_actions.js";
class NoteDetailCode {
@ -17,7 +17,7 @@ class NoteDetailCode {
this.$editorEl = this.$component.find('.note-detail-code-editor');
this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button");
utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote());
keyboardActionService.setElementActionHandler(ctx.$tabContent, 'RunActiveNote', () => this.executeCurrentNote());
this.$executeScriptButton.on('click', () => this.executeCurrentNote());
}
@ -55,18 +55,21 @@ class NoteDetailCode {
this.onNoteChange(() => this.ctx.noteChanged());
}
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'code') {
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);
if (info) {
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
if (info) {
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
}
this.show();
}
this.show();
}
show() {

View file

@ -159,7 +159,7 @@ class NoteDetailRelationMap {
const noteId = this.idToNoteId($noteBox.prop("id"));
if (cmd === "open-in-new-tab") {
noteDetailService.openInTab(noteId);
noteDetailService.openInTab(noteId, false);
}
else if (cmd === "remove") {
const confirmDialog = await import('../dialogs/confirm.js');
@ -238,14 +238,17 @@ class NoteDetailRelationMap {
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
this.loadMapData();
jsPlumb.ready(() => {
this.initJsPlumbInstance();
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'relation-map') {
this.loadMapData();
this.initPanZoom();
this.initJsPlumbInstance();
this.loadNotesAndRelations();
this.initPanZoom();
this.loadNotesAndRelations();
}
});
}

View file

@ -1,6 +1,41 @@
import libraryLoader from "./library_loader.js";
import treeService from './tree.js';
import noteAutocompleteService from './note_autocomplete.js';
import mimeTypesService from './mime_types.js';
const mentionSetup = {
feeds: [
{
marker: '@',
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
rows = [];
}
for (const row of rows) {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
}
res(rows);
});
});
},
itemRenderer: item => {
const itemElement = document.createElement('span');
itemElement.classList.add('mentions-item');
itemElement.innerHTML = `${item.highlightedTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
};
class NoteDetailText {
/**
@ -33,6 +68,16 @@ class NoteDetailText {
if (!this.textEditor) {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const codeBlockLanguages =
(await mimeTypesService.getMimeTypes())
.filter(mt => mt.enabled)
.map(mt => {
return {
language: mt.mime.toLowerCase().replace(/[\W_]+/g,"-"),
label: mt.title
}
});
// CKEditor since version 12 needs the element to be visible before initialization. At the same time
// we want to avoid flicker - i.e. show editor only once everything is ready. That's why we have separate
// display of $component in both branches.
@ -43,40 +88,9 @@ class NoteDetailText {
if (!this.textEditor) {
this.textEditor = await BalloonEditor.create(this.$editorEl[0], {
placeholder: "Type the content of your note here ...",
mention: {
feeds: [
{
marker: '@',
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
rows = [];
}
for (const row of rows) {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
}
console.log(rows.slice(0, Math.min(5, rows.length)));
res(rows);
});
});
},
itemRenderer: item => {
const itemElement = document.createElement('span');
itemElement.classList.add('mentions-item');
itemElement.innerHTML = `${item.highlightedTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
mention: mentionSetup,
codeBlock: {
languages: codeBlockLanguages
}
});
@ -84,11 +98,14 @@ class NoteDetailText {
}
}
this.textEditor.isReadOnly = await this.isReadOnly();
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'text') {
this.textEditor.isReadOnly = await this.isReadOnly();
this.$component.show();
this.$component.show();
this.textEditor.setData(this.ctx.note.content);
this.textEditor.setData(this.ctx.note.content);
}
}
getContent() {

View file

@ -13,6 +13,10 @@ class Options {
return this.arr[key];
}
getNames() {
return Object.keys(this.arr);
}
getJson(key) {
try {
return JSON.parse(this.arr[key]);

View file

@ -42,12 +42,16 @@ async function remove(url, headers = {}) {
let i = 1;
const reqResolves = {};
let maxKnownSyncId = 0;
async function call(method, url, data, headers = {}) {
let resp;
if (utils.isElectron()) {
const ipc = require('electron').ipcRenderer;
const requestId = i++;
return new Promise((resolve, reject) => {
resp = await new Promise((resolve, reject) => {
reqResolves[requestId] = resolve;
if (REQUEST_LOGGING_ENABLED) {
@ -64,32 +68,58 @@ async function call(method, url, data, headers = {}) {
});
}
else {
return await ajax(url, method, data, headers);
resp = await ajax(url, method, data, headers);
}
const maxSyncIdStr = resp.headers['trilium-max-sync-id'];
if (maxSyncIdStr && maxSyncIdStr.trim()) {
maxKnownSyncId = Math.max(maxKnownSyncId, parseInt(maxSyncIdStr));
}
return resp.body;
}
async function ajax(url, method, data, headers) {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
timeout: 60000
};
function ajax(url, method, data, headers) {
return new Promise((res, rej) => {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
timeout: 60000,
success: (body, textStatus, jqXhr) => {
const respHeaders = {};
if (data) {
try {
options.data = JSON.stringify(data);
}
catch (e) {
console.log("Can't stringify data: ", data, " because of error: ", e)
}
options.contentType = "application/json";
}
jqXhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(line => {
const parts = line.split(': ');
const header = parts.shift();
respHeaders[header] = parts.join(': ');
});
return await $.ajax(options).catch(e => {
const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
toastService.showError(message);
toastService.throwError(message);
res({
body,
headers: respHeaders
});
},
error: (jqXhr, textStatus, error) => {
const message = "Error when calling " + method + " " + url + ": " + textStatus + " - " + error;
toastService.showError(message);
toastService.throwError(message);
rej(error);
}
};
if (data) {
try {
options.data = JSON.stringify(data);
} catch (e) {
console.log("Can't stringify data: ", data, " because of error: ", e)
}
options.contentType = "application/json";
}
$.ajax(options);
});
}
@ -101,7 +131,10 @@ if (utils.isElectron()) {
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
}
reqResolves[arg.requestId](arg.body);
reqResolves[arg.requestId]({
body: arg.body,
headers: arg.headers
});
delete reqResolves[arg.requestId];
});
@ -114,5 +147,6 @@ export default {
remove,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
getHeaders,
getMaxKnownSyncId: () => maxKnownSyncId
};

View file

@ -29,6 +29,7 @@ const tabTemplate = `
</div>`;
const newTabButtonTemplate = `<div class="note-new-tab" title="Add new tab">+</div>`;
const fillerTemplate = `<div class="tab-row-filler"></div>`;
class TabRow {
constructor(el) {
@ -40,9 +41,10 @@ class TabRow {
this.setupStyleEl();
this.setupEvents();
this.layoutTabs();
this.setupDraggabilly();
this.setupNewButton();
this.setupFiller();
this.layoutTabs();
this.setVisibility();
}
@ -109,12 +111,17 @@ class TabRow {
const widths = [];
let extraWidthRemaining = totalExtraWidthDueToFlooring;
for (let i = 0; i < numberOfTabs; i += 1) {
const extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0;
widths.push(flooredClampedTargetWidth + extraWidth);
if (extraWidthRemaining > 0) extraWidthRemaining -= 1;
}
if (this.fillerEl) {
this.fillerEl.style.width = extraWidthRemaining + "px";
}
return widths;
}
@ -129,8 +136,9 @@ class TabRow {
});
const newTabPosition = position;
const fillerPosition = position + 32;
return {tabPositions, newTabPosition};
return {tabPositions, newTabPosition, fillerPosition};
}
layoutTabs() {
@ -151,13 +159,14 @@ class TabRow {
let styleHTML = '';
const {tabPositions, newTabPosition} = this.getTabPositions();
const {tabPositions, newTabPosition, fillerPosition} = this.getTabPositions();
tabPositions.forEach((position, i) => {
styleHTML += `.note-tab:nth-child(${ i + 1 }) { transform: translate3d(${ position }px, 0, 0)} `;
});
styleHTML += `.note-new-tab { transform: translate3d(${ newTabPosition }px, 0, 0) } `;
styleHTML += `.tab-row-filler { transform: translate3d(${ fillerPosition }px, 0, 0) } `;
this.styleEl.innerHTML = styleHTML;
}
@ -387,11 +396,18 @@ class TabRow {
this.newTabEl = div.firstElementChild;
this.tabContentEl.appendChild(this.newTabEl);
this.layoutTabs();
this.newTabEl.addEventListener('click', _ => this.emit('newTab'));
}
setupFiller() {
const div = document.createElement('div');
div.innerHTML = fillerTemplate;
this.fillerEl = div.firstElementChild;
this.tabContentEl.appendChild(this.fillerEl);
}
closest(value, array) {
let closest = Infinity;
let closestIndex = -1;

View file

@ -9,13 +9,12 @@ import server from './server.js';
import treeCache from './tree_cache.js';
import toastService from "./toast.js";
import treeBuilder from "./tree_builder.js";
import treeKeyBindings from "./tree_keybindings.js";
import Branch from '../entities/branch.js';
import NoteShort from '../entities/note_short.js';
import treeKeyBindingService from "./tree_keybindings.js";
import hoistedNoteService from '../services/hoisted_note.js';
import optionsService from "../services/options.js";
import TreeContextMenu from "./tree_context_menu.js";
import bundle from "./bundle.js";
import keyboardActionService from "./keyboard_actions.js";
const $tree = $("#tree");
const $createTopLevelNoteButton = $("#create-top-level-note-button");
@ -368,7 +367,7 @@ async function treeInitialized() {
const notePath = location.hash.substr(1);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (await treeCache.noteExists(noteId)) {
if (noteId && await treeCache.noteExists(noteId)) {
for (const tab of openTabs) {
tab.active = false;
}
@ -430,7 +429,7 @@ async function treeInitialized() {
setFrontendAsLoaded();
}
function initFancyTree(tree) {
async function initFancyTree(tree) {
utils.assertArguments(tree);
$tree.fancytree({
@ -473,7 +472,7 @@ function initFancyTree(tree) {
collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false),
init: (event, data) => treeInitialized(), // don't collapse to short form
hotkeys: {
keydown: treeKeyBindings
keydown: await treeKeyBindingService.getKeyboardBindings()
},
dnd5: dragAndDropSetup,
lazyLoad: function(event, data) {
@ -627,6 +626,8 @@ async function createNewTopLevelNote() {
async function createNote(node, parentNoteId, target, extraOptions = {}) {
utils.assertArguments(node, parentNoteId, target);
extraOptions.activate = extraOptions.activate === undefined ? true : !!extraOptions.activate;
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
// but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
if (!extraOptions.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) {
@ -649,11 +650,9 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
const newNoteName = extraOptions.title || "new note";
const {note, branch} = await server.post('notes/' + parentNoteId + '/children', {
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${target}&targetBranchId=${node.data.branchId}`, {
title: newNoteName,
content: extraOptions.content,
target: target,
target_branchId: node.data.branchId,
content: extraOptions.content || "",
isProtected: extraOptions.isProtected,
type: extraOptions.type
});
@ -670,7 +669,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
const noteEntity = await treeCache.getNote(note.noteId);
const branchEntity = treeCache.getBranch(branch.branchId);
let newNode = {
let newNodeData = {
title: newNoteName,
noteId: branchEntity.noteId,
parentNoteId: parentNoteId,
@ -685,8 +684,11 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
/** @var {FancytreeNode} */
let newNode;
if (target === 'after') {
await node.appendSibling(newNode).setActive(true);
newNode = node.appendSibling(newNodeData);
}
else if (target === 'into') {
if (!node.getChildren() && node.isFolder()) {
@ -696,10 +698,10 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
await node.setExpanded();
}
else {
node.addChildren(newNode);
node.addChildren(newNodeData);
}
await node.getLastChild().setActive(true);
newNode = node.getLastChild();
const parentNoteEntity = await treeCache.getNote(node.data.noteId);
@ -711,14 +713,18 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
toastService.throwError("Unrecognized target: " + target);
}
if (extraOptions.activate) {
await newNode.setActive(true);
}
clearSelectedNodes(); // to unmark previously active node
// need to refresh because original doesn't have methods like .getParent()
newNode = getNodesByNoteId(branchEntity.noteId)[0];
newNodeData = getNodesByNoteId(branchEntity.noteId)[0];
// following for cycle will make sure that also clones of a parent are refreshed
for (const newParentNode of getNodesByNoteId(parentNoteId)) {
if (newParentNode.key === newNode.getParent().key) {
if (newParentNode.key === newNodeData.getParent().key) {
// we've added a note into this one so no need to refresh
continue;
}
@ -756,7 +762,7 @@ async function sortAlphabetically(noteId) {
async function showTree() {
const tree = await loadTree();
initFancyTree(tree);
await initFancyTree(tree);
}
ws.subscribeToMessages(message => {
@ -789,7 +795,7 @@ ws.subscribeToOutsideSyncMessages(async syncData => {
syncData.filter(sync => sync.entityName === 'attributes').forEach(sync => {
const note = treeCache.notes[sync.noteId];
if (note && note.attributeCache) {
if (note && note.__attributeCache) {
noteIdsToRefresh.add(sync.entityId);
}
});
@ -799,7 +805,7 @@ ws.subscribeToOutsideSyncMessages(async syncData => {
}
});
utils.bindGlobalShortcut('ctrl+o', async () => {
keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => {
const node = getActiveNode();
const parentNoteId = node.data.parentNoteId;
const isProtected = await treeUtils.getParentProtectedStatus(node);
@ -863,7 +869,7 @@ async function reloadNotes(noteIds, activateNotePath = null) {
if (activateNotePath) {
const node = await getNodeFromPath(activateNotePath);
if (node) {
if (node && !node.isActive()) {
await node.setActive(true);
}
}
@ -871,9 +877,9 @@ async function reloadNotes(noteIds, activateNotePath = null) {
window.glob.createNoteInto = createNoteInto;
utils.bindGlobalShortcut('ctrl+p', createNoteInto);
keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto);
utils.bindGlobalShortcut('ctrl+.', scrollToActiveNote);
keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote);
$(window).bind('hashchange', async function() {
if (isNotePathInAddress()) {
@ -890,7 +896,7 @@ $tree.on('mousedown', '.fancytree-title', e => {
treeUtils.getNotePath(node).then(notePath => {
if (notePath) {
noteDetailService.openInTab(notePath);
noteDetailService.openInTab(notePath, false);
}
});
@ -911,7 +917,7 @@ async function duplicateNote(noteId, parentNoteId) {
}
utils.bindGlobalShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
keyboardActionService.setGlobalActionHandler('CollapseTree', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
$collapseTreeButton.on('click', () => collapseTree());
$createTopLevelNoteButton.on('click', createNewTopLevelNote);

View file

@ -65,6 +65,11 @@ async function getIcon(note) {
async function prepareNode(branch) {
const note = await branch.getNote();
if (!note) {
throw new Error(`Branch has no note ` + branch.noteId);
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();

View file

@ -133,7 +133,7 @@ class TreeCache {
/** @return {Promise<NoteShort[]>} */
async getNotes(noteIds, silentNotFoundError = false) {
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
if (missingNoteIds.length > 0) {
await this.reloadNotes(missingNoteIds);
@ -161,6 +161,11 @@ class TreeCache {
/** @return {Promise<NoteShort>} */
async getNote(noteId, silentNotFoundError = false) {
if (noteId === 'none') {
console.log(`No 'none' note.`);
return null;
}
else if (!noteId) {
console.log(`Falsy noteId ${noteId}, returning null.`);
return null;
}

View file

@ -46,37 +46,37 @@ class TreeContextMenu {
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
{ title: "Open in new tab", cmd: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
{ title: "Insert note after <kbd>Ctrl+O</kbd>", cmd: "insertNoteAfter", uiIcon: "plus",
{ title: 'Open in new tab', cmd: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
{ title: 'Insert note after <kbd data-kb-action="CreateNoteAfter"></kbd>', cmd: "insertNoteAfter", uiIcon: "plus",
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes },
{ title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "plus",
{ title: 'Insert child note <kbd data-kb-action="CreateNoteInto"></kbd>', cmd: "insertChildNote", uiIcon: "plus",
items: notSearch ? this.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes },
{ title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash",
{ title: 'Delete <kbd data-kb-action="DeleteNotes"></kbd>', cmd: "delete", uiIcon: "trash",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: "----" },
{ title: "Search in subtree <kbd>Ctrl+Shift+S</kbd>", cmd: "searchInSubtree", uiIcon: "search",
{ title: 'Search in subtree <kbd data-kb-action="SearchInSubtree"></kbd>', cmd: "searchInSubtree", uiIcon: "search",
enabled: notSearch && noSelectedNotes },
isHoisted ? null : { title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: "Unhoist note <kbd>Ctrl-H</kbd>", cmd: "unhoist", uiIcon: "arrow-up" },
{ title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "empty",
isHoisted ? null : { title: 'Hoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "unhoist", uiIcon: "arrow-up" },
{ title: 'Edit branch prefix <kbd data-kb-action="EditBranchPrefix"></kbd>', cmd: "editBranchPrefix", uiIcon: "empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "----" },
{ title: "Protect subtree", cmd: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes },
{ title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes },
{ title: "----" },
{ title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "copy",
{ title: 'Copy / clone <kbd data-kb-action="CopyNotesToClipboard"></kbd>', cmd: "copy", uiIcon: "copy",
enabled: isNotRoot && !isHoisted },
{ title: "Clone to ... <kbd>Ctrl+Shift+C</kbd>", cmd: "cloneTo", uiIcon: "empty",
{ title: 'Clone to ... <kbd data-kb-action="CloneNotesTo"></kbd>', cmd: "cloneTo", uiIcon: "empty",
enabled: isNotRoot && !isHoisted },
{ title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "cut",
{ title: 'Cut <kbd data-kb-action="CutNotesToClipboard"></kbd>', cmd: "cut", uiIcon: "cut",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: "Move to ... <kbd>Ctrl+Shift+X</kbd>", cmd: "moveTo", uiIcon: "empty",
{ title: 'Move to ... <kbd data-kb-action="MoveNotesTo"></kbd>', cmd: "moveTo", uiIcon: "empty",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "paste",
{ title: 'Paste into <kbd data-kb-action="PasteNotesFromClipboard"></kbd>', cmd: "pasteInto", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: "Paste after", cmd: "pasteAfter", uiIcon: "paste",
{ title: 'Paste after', cmd: "pasteAfter", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
enabled: noSelectedNotes && parentNotSearch && isNotRoot && !isHoisted && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
@ -85,10 +85,11 @@ class TreeContextMenu {
enabled: notSearch && noSelectedNotes },
{ title: "Import into note", cmd: "importIntoNote", uiIcon: "empty",
enabled: notSearch && noSelectedNotes },
{ title: "----" },
{ title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-kb-action="CollapseSubtree"></kbd>', cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-kb-action="SortChildNotes"></kbd>', cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
] },
].filter(row => row !== null);
}
@ -96,7 +97,7 @@ class TreeContextMenu {
if (cmd === 'openInTab') {
const notePath = await treeUtils.getNotePath(this.node);
noteDetailService.openInTab(notePath);
noteDetailService.openInTab(notePath, false);
}
else if (cmd.startsWith("insertNoteAfter")) {
const parentNoteId = this.node.data.parentNoteId;

View file

@ -3,150 +3,10 @@ import treeChangesService from "./branches.js";
import treeService from "./tree.js";
import hoistedNoteService from "./hoisted_note.js";
import clipboard from "./clipboard.js";
import treeCache from "./tree_cache.js";
import searchNoteService from "./search_notes.js";
import utils from "./utils.js";
import keyboardActionService from "./keyboard_actions.js";
const keyBindings = {
"del": node => {
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node));
},
"ctrl+up": node => {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
treeChangesService.moveBeforeNode([node], beforeNode);
}
return false;
},
"ctrl+down": node => {
const afterNode = node.getNextSibling();
if (afterNode !== null) {
treeChangesService.moveAfterNode([node], afterNode);
}
return false;
},
"ctrl+left": node => {
treeChangesService.moveNodeUpInHierarchy(node);
return false;
},
"ctrl+right": node => {
const toNode = node.getPrevSibling();
if (toNode !== null) {
treeChangesService.moveToNode([node], toNode);
}
return false;
},
"shift+up": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
node.navigate($.ui.keyCode.UP, false).then(() => {
const currentNode = treeService.getFocusedNode();
if (currentNode.isSelected()) {
node.setSelected(false);
}
currentNode.setSelected(true);
});
return false;
},
"shift+down": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
node.navigate($.ui.keyCode.DOWN, false).then(() => {
const currentNode = treeService.getFocusedNode();
if (currentNode.isSelected()) {
node.setSelected(false);
}
currentNode.setSelected(true);
});
return false;
},
"f2": async node => {
const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js");
editBranchPrefixDialog.showDialog(node);
},
"alt+-": node => {
treeService.collapseTree(node);
},
"alt+s": node => {
treeService.sortAlphabetically(node.data.noteId);
return false;
},
"ctrl+a": node => {
for (const child of node.getParent().getChildren()) {
child.setSelected(true);
}
return false;
},
"ctrl+c": node => {
clipboard.copy(treeService.getSelectedOrActiveNodes(node));
return false;
},
"ctrl+x": node => {
clipboard.cut(treeService.getSelectedOrActiveNodes(node));
return false;
},
"ctrl+v": node => {
clipboard.pasteInto(node);
return false;
},
"return": node => {
noteDetailService.focusOnTitle();
return false;
},
"backspace": async node => {
if (!await hoistedNoteService.isRootNode(node)) {
node.getParent().setActive().then(treeService.clearSelectedNodes);
}
},
"ctrl+h": node => {
hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => {
if (node.data.noteId === hoistedNoteId) {
hoistedNoteService.unhoist();
}
else {
const note = await treeCache.getNote(node.data.noteId);
if (note.type !== 'search') {
hoistedNoteService.setHoistedNoteId(node.data.noteId);
}
}
});
return false;
},
const fixedKeyBindings = {
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
// after opening context menu, standard shortcuts don't work, but they are detected here
// so we essentially takeover the standard handling with our implementation.
@ -168,13 +28,153 @@ const keyBindings = {
"down": node => {
node.navigate($.ui.keyCode.DOWN, true).then(treeService.clearSelectedNodes);
return false;
},
"ctrl+shift+s": node => {
searchNoteService.searchInSubtree(node.data.noteId);
return false;
}
};
export default keyBindings;
const templates = {
"DeleteNotes": node => {
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node));
},
"MoveNoteUp": node => {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
treeChangesService.moveBeforeNode([node], beforeNode);
}
return false;
},
"MoveNoteDown": node => {
const afterNode = node.getNextSibling();
if (afterNode !== null) {
treeChangesService.moveAfterNode([node], afterNode);
}
return false;
},
"MoveNoteUpInHierarchy": node => {
treeChangesService.moveNodeUpInHierarchy(node);
return false;
},
"MoveNoteDownInHierarchy": node => {
const toNode = node.getPrevSibling();
if (toNode !== null) {
treeChangesService.moveToNode([node], toNode);
}
return false;
},
"AddNoteAboveToSelection": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
const prevSibling = node.getPrevSibling();
if (prevSibling) {
prevSibling.setActive(true, {noEvents: true});
if (prevSibling.isSelected()) {
node.setSelected(false);
}
prevSibling.setSelected(true);
}
return false;
},
"AddNoteBelowToSelection": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
const nextSibling = node.getNextSibling();
if (nextSibling) {
nextSibling.setActive(true, {noEvents: true});
if (nextSibling.isSelected()) {
node.setSelected(false);
}
nextSibling.setSelected(true);
}
return false;
},
"CollapseSubtree": node => {
treeService.collapseTree(node);
},
"SortChildNotes": node => {
treeService.sortAlphabetically(node.data.noteId);
return false;
},
"SelectAllNotesInParent": node => {
for (const child of node.getParent().getChildren()) {
child.setSelected(true);
}
return false;
},
"CopyNotesToClipboard": node => {
clipboard.copy(treeService.getSelectedOrActiveNodes(node));
return false;
},
"CutNotesToClipboard": node => {
clipboard.cut(treeService.getSelectedOrActiveNodes(node));
return false;
},
"PasteNotesFromClipboard": node => {
clipboard.pasteInto(node);
return false;
},
"EditNoteTitle": node => {
noteDetailService.focusOnTitle();
return false;
},
"ActivateParentNote": async node => {
if (!await hoistedNoteService.isRootNode(node)) {
node.getParent().setActive().then(treeService.clearSelectedNodes);
}
}
};
async function getKeyboardBindings() {
const bindings = Object.assign({}, fixedKeyBindings);
for (const actionName in templates) {
const action = await keyboardActionService.getAction(actionName);
for (const shortcut of action.effectiveShortcuts || []) {
const normalizedShortcut = utils.normalizeShortcut(shortcut);
bindings[normalizedShortcut] = templates[actionName];
}
}
return bindings;
}
export default {
getKeyboardBindings
};

View file

@ -137,10 +137,7 @@ function bindGlobalShortcut(keyboardShortcut, handler) {
function bindElShortcut($el, keyboardShortcut, handler) {
if (isDesktop()) {
if (isMac()) {
// use CMD (meta) instead of CTRL for all shortcuts
keyboardShortcut = keyboardShortcut.replace("ctrl", "meta");
}
keyboardShortcut = normalizeShortcut(keyboardShortcut);
$el.bind('keydown', keyboardShortcut, e => {
handler(e);
@ -151,6 +148,18 @@ function bindElShortcut($el, keyboardShortcut, handler) {
}
}
/**
* Normalize to the form expected by the jquery.hotkeys.js
*/
function normalizeShortcut(shortcut) {
return shortcut
.toLowerCase()
.replace("enter", "return")
.replace("delete", "del")
.replace("ctrl+alt", "alt+ctrl")
.replace("meta+alt", "alt+meta"); // alt needs to be first;
}
function isMobile() {
return window.device === "mobile"
// window.device is not available in setup
@ -259,5 +268,6 @@ export default {
closeActiveDialog,
isHtmlEmpty,
clearBrowserCache,
getUrlForDownload
getUrlForDownload,
normalizeShortcut
};

View file

@ -1,5 +1,6 @@
import utils from './utils.js';
import toastService from "./toast.js";
import server from "./server.js";
const $outstandingSyncsCount = $("#outstanding-syncs-count");
@ -8,7 +9,8 @@ const outsideSyncMessageHandlers = [];
const messageHandlers = [];
let ws;
let lastSyncId = window.glob.maxSyncIdAtLoad;
let lastAcceptedSyncId = window.glob.maxSyncIdAtLoad;
let lastProcessedSyncId = window.glob.maxSyncIdAtLoad;
let lastPingTs;
let syncDataQueue = [];
@ -57,21 +59,26 @@ async function handleMessage(event) {
syncDataQueue.push(...syncRows);
// we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id);
sendPing();
// first wait for all the preceding consumers to finish
while (consumeQueuePromise) {
await consumeQueuePromise;
}
// it's my turn so start it up
consumeQueuePromise = consumeSyncData();
try {
// it's my turn so start it up
consumeQueuePromise = consumeSyncData();
await consumeQueuePromise;
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
await consumeQueuePromise;
}
finally {
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
}
}
checkSyncIdListeners();
}
else if (message.type === 'sync-hash-check-failed') {
toastService.showError("Sync check failed!", 60000);
@ -84,7 +91,7 @@ async function handleMessage(event) {
let syncIdReachedListeners = [];
function waitForSyncId(desiredSyncId) {
if (desiredSyncId <= lastSyncId) {
if (desiredSyncId <= lastProcessedSyncId) {
return Promise.resolve();
}
@ -97,16 +104,29 @@ function waitForSyncId(desiredSyncId) {
});
}
function waitForMaxKnownSyncId() {
return waitForSyncId(server.getMaxKnownSyncId());
}
function checkSyncIdListeners() {
syncIdReachedListeners
.filter(l => l.desiredSyncId <= lastSyncId)
.filter(l => l.desiredSyncId <= lastProcessedSyncId)
.forEach(l => l.resolvePromise());
syncIdReachedListeners = syncIdReachedListeners
.filter(l => l.desiredSyncId > lastSyncId);
.filter(l => l.desiredSyncId > lastProcessedSyncId);
syncIdReachedListeners.filter(l => Date.now() > l.start - 60000)
.forEach(l => console.log(`Waiting for syncId ${l.desiredSyncId} while current is ${lastSyncId} for ${Math.floor((Date.now() - l.start) / 1000)}s`));
.forEach(l => console.log(`Waiting for syncId ${l.desiredSyncId} while current is ${lastProcessedSyncId} for ${Math.floor((Date.now() - l.start) / 1000)}s`));
}
async function runSafely(syncHandler, syncData) {
try {
return await syncHandler(syncData);
}
catch (e) {
console.log(`Sync handler failed with ${e.message}: ${e.stack}`);
}
}
async function consumeSyncData() {
@ -116,14 +136,24 @@ async function consumeSyncData() {
const outsideSyncData = allSyncData.filter(sync => sync.sourceId !== glob.sourceId);
// the update process should be synchronous as a whole but individual handlers can run in parallel
await Promise.all([
...allSyncMessageHandlers.map(syncHandler => syncHandler(allSyncData)),
...outsideSyncMessageHandlers.map(syncHandler => syncHandler(outsideSyncData))
]);
try {
// the update process should be synchronous as a whole but individual handlers can run in parallel
await Promise.all([
...allSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, allSyncData)),
...outsideSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, outsideSyncData))
]);
}
catch (e) {
logError(`Encountered error ${e.message}, reloading frontend.`);
lastSyncId = allSyncData[allSyncData.length - 1].id;
// if there's an error in updating the frontend then the easy option to recover is to reload the frontend completely
utils.reloadApp();
}
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id);
}
checkSyncIdListeners();
}
function connectWebSocket() {
@ -140,29 +170,30 @@ function connectWebSocket() {
return ws;
}
async function sendPing() {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost connection to server");
}
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastAcceptedSyncId
}));
}
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
console.log(utils.now(), "WS closed or closing, trying to reconnect");
ws = connectWebSocket();
}
}
setTimeout(() => {
ws = connectWebSocket();
lastSyncId = glob.maxSyncIdAtLoad;
lastPingTs = Date.now();
setInterval(async () => {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost connection to server");
}
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
}
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
console.log(utils.now(), "WS closed or closing, trying to reconnect");
ws = connectWebSocket();
}
}, 1000);
setInterval(sendPing, 1000);
}, 0);
subscribeToMessages(message => {
@ -185,5 +216,6 @@ export default {
subscribeToMessages,
subscribeToAllSyncMessages,
subscribeToOutsideSyncMessages,
waitForSyncId
waitForSyncId,
waitForMaxKnownSyncId
};

View file

@ -18,7 +18,7 @@ class SimilarNotesWidget extends StandardWidget {
// remember which title was when we found the similar notes
this.title = this.ctx.note.title;
const similarNotes = await server.get('similar_notes/' + this.ctx.note.noteId);
const similarNotes = await server.get('similar-notes/' + this.ctx.note.noteId);
if (similarNotes.length === 0) {
this.$body.text("No similar notes found ...");

View file

@ -18,7 +18,7 @@ body {
grid-gap: 0;
}
#container.distraction-free-mode {
#container.zen-mode {
grid-template-areas:
"tab-container" !important;
grid-template-rows: auto
@ -59,7 +59,7 @@ body {
background-color: var(--header-background-color);
display: flex;
align-items: center;
padding: 4px;
padding-top: 4px;
}
#header button {
@ -68,18 +68,26 @@ body {
margin-bottom: 2px;
margin-top: 2px;
margin-right: 8px;
border-color: transparent !important;
}
#header button.btn-sm .bx {
position: relative;
top: 1px;
}
#header button:hover {
border-color: var(--button-border-color) !important;
}
#history-navigation {
margin: 0 15px 0 5px;
position: relative;
top: 2px;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
padding: 3px 0 3px 0;
border: 1px solid var(--main-border-color);
border-radius: 7px;
margin: 5px 15px 5px 5px;
@ -116,6 +124,7 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
min-width: 15rem;
}
/* rotate caret on hover */
@ -150,7 +159,7 @@ body {
::-webkit-scrollbar-thumb {
border-radius: 3px;
border: 1px solid var(--main-border-color);
border: 1px solid var(--scrollbar-border-color);
}
::-webkit-scrollbar-corner {
@ -170,6 +179,13 @@ body {
cursor: pointer;
position: relative;
top: -1px;
border: 1px solid transparent;
padding: 2px;
border-radius: 2px;
}
.refresh-search-button:hover {
border-color: var(--button-border-color);
}
.note-title-row {
@ -220,7 +236,7 @@ body {
.note-new-tab {
position: absolute;
left: 0;
height: 32px;
height: 33px;
width: 32px;
border: 0;
margin: 0;
@ -228,6 +244,7 @@ body {
text-align: center;
font-size: 24px;
cursor: pointer;
border-bottom: 1px solid var(--button-border-color);
}
.note-new-tab:hover {
@ -235,6 +252,14 @@ body {
border-radius: 5px;
}
.tab-row-filler {
position: absolute;
left: 0;
background: linear-gradient(to right, var(--button-border-color), transparent);
height: 1px;
margin-top: 32px;
}
.note-tab-row .note-tab[active] {
z-index: 5;
}
@ -249,6 +274,7 @@ body {
top: 10px;
animation: note-tab-was-just-added 120ms forwards ease-in-out;
}
.note-tab-row .note-tab .note-tab-wrapper {
position: absolute;
display: flex;
@ -261,11 +287,14 @@ body {
border-top-right-radius: 8px;
overflow: hidden;
pointer-events: all;
background-image: linear-gradient(to bottom, var(--accented-background-color), var(--main-background-color));
background-color: var(--accented-background-color);
border-bottom: 1px solid var(--button-border-color);
}
.note-tab-row .note-tab[active] .note-tab-wrapper {
background-image: linear-gradient(to bottom, var(--more-accented-background-color), var(--main-background-color));
background-color: var(--main-background-color);
border: 1px solid var(--button-border-color);
border-bottom: 0;
font-weight: bold;
}
@ -273,6 +302,7 @@ body {
padding-left: 2px;
padding-right: 2px;
}
.note-tab-row .note-tab .note-tab-title {
flex: 1;
vertical-align: top;
@ -280,12 +310,15 @@ body {
white-space: nowrap;
color: var(--muted-text-color);
}
.note-tab-row .note-tab[is-small] .note-tab-title {
margin-left: 0;
}
.note-tab-row .note-tab[active] .note-tab-title {
color: var(--main-text-color);
}
.note-tab-row .note-tab .note-tab-drag-handle {
position: absolute;
top: 0;
@ -295,6 +328,7 @@ body {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.note-tab-row .note-tab .note-tab-close {
flex-grow: 0;
flex-shrink: 0;
@ -353,6 +387,14 @@ body {
.hide-sidebar-button {
color: var(--main-text-color);
background: none;
border: 1px solid transparent;
padding: 2px 8px 2px 8px;
border-radius: 2px;
}
.hide-sidebar-button:hover {
border-color: var(--button-border-color);
}
.note-detail-sidebar {
@ -385,6 +427,8 @@ body {
border: 0;
background: inherit;
font-weight: bold;
text-transform: uppercase;
color: var(--muted-text-color) !important;
}
.note-detail-sidebar .widget-header-action {
@ -393,7 +437,9 @@ body {
}
.note-detail-sidebar .widget-help {
color: var(--main-text-color);
color: var(--muted-text-color);
position: relative;
top: 2px;
}
.note-detail-sidebar .widget-help.no-link:hover {

View file

@ -19,7 +19,7 @@ html, body {
display: flex;
flex-shrink: 0;
justify-content: space-around;
padding: 10px 0 10px 0;
padding: 3px 0 3px 0;
margin: 0 10px 0 16px;
}

View file

@ -75,6 +75,12 @@ ul.fancytree-container {
content: "\e9f8";
}
/** some common text styling for cssClass label */
span.fancytree-node.underline .fancytree-title { text-decoration: underline; }
span.fancytree-node.dotted .fancytree-title { text-decoration: dotted; }
span.fancytree-node.bold .fancytree-title { font-weight: bold; }
span.fancytree-node.muted { opacity: 0.6; }
.note-title[readonly] {
background: inherit;
}
@ -144,9 +150,9 @@ ul.fancytree-container {
margin-top: 0;
}
/** we disable shield background when in distraction free mode because I couldn't get it to stay static
/** we disable shield background when in zen mode because I couldn't get it to stay static
(it kept growing with content) */
#container:not(.distraction-free-mode) .note-tab-content.protected {
#container:not(.zen-mode) .note-tab-content.protected {
/* DON'T COLLAPSE THE RULES INTO SINGLE ONE, BACKGROUND WON'T DISPLAY */
background: url('../images/shield.svg') no-repeat;
background-size: contain;
@ -227,9 +233,13 @@ span.fancytree-node.archived {
.icon-action:hover {
text-decoration: none;
border-color: var(--button-border-color);
}
.icon-action {
border: 1px solid transparent;
border-radius: 3px;
padding: 5px;
cursor: pointer;
font-size: 1.5em;
}
@ -419,7 +429,23 @@ div.ui-tooltip {
border-radius: var(--button-border-radius);
}
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
.btn.btn-primary {
border-color: var(--primary-button-border-color);
background-color: var(--primary-button-background-color);
color: var(--primary-button-text-color);
}
.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active {
border-color: var(--primary-button-text-color);
background-color: var(--active-item-background-color);
color: var(--active-item-text-color);
}
.btn.btn-primary kbd {
color: var(--primary-button-text-color);
}
.btn:not(.btn-primary) {
border-color: var(--button-border-color);
background-color: var(--button-background-color);
color: var(--button-text-color);
@ -427,6 +453,7 @@ div.ui-tooltip {
.btn.active:not(.btn-primary) {
background-color: var(--button-disabled-background-color) !important;
opacity: 0.4;
}
.note-path-list a.current {
@ -773,7 +800,7 @@ div[data-notify="container"] {
padding: 2px;
}
#sql-console-tables button {
#sql-console-table-schemas button {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;

View file

@ -10,26 +10,30 @@
--main-text-color: black;
--main-border-color: #ccc;
--accented-background-color: #f5f5f5;
--more-accented-background-color: #ccc;
--header-background-color: #f8f8f8;
--button-background-color: #eee;
--button-disabled-background-color: #ccc;
--more-accented-background-color: #ddd;
--header-background-color: #fff;
--button-background-color: #fff;
--button-disabled-background-color: #ddd;
--button-border-color: #ddd;
--button-text-color: black;
--button-border-radius: 5px;
--muted-text-color: #444;
--primary-button-background-color: #6c757d;
--primary-button-text-color: white;
--primary-button-border-color: #6c757d;
--muted-text-color: #666;
--input-text-color: black;
--input-background-color: white;
--hover-item-text-color: black;
--hover-item-background-color: #eee;
--active-item-text-color: black;
--active-item-background-color: #ccc;
--active-item-background-color: #ddd;
--menu-text-color: black;
--menu-background-color: white;
--tooltip-background-color: #f8f8f8;
--link-color: blue;
--modal-background-color: white;
--modal-backdrop-color: black;
--scrollbar-border-color: #ddd;
}
body.theme-black {
@ -43,6 +47,9 @@ body.theme-black {
--button-border-color: #444;
--button-text-color: white;
--button-border-radius: 5px;
--primary-button-background-color: #888;
--primary-button-text-color: white;
--primary-button-border-color: #999;
--muted-text-color: #ccc;
--input-text-color: white;
--input-background-color: black;
@ -56,6 +63,7 @@ body.theme-black {
--link-color: lightskyblue;
--modal-background-color: black;
--modal-backdrop-color: #444;
--scrollbar-border-color: #888;
}
body.theme-black .CodeMirror {
@ -65,7 +73,7 @@ body.theme-black .CodeMirror {
body.theme-dark {
--main-background-color: #333;
--main-text-color: white;
--main-border-color: #ddd;
--main-border-color: #aaa;
--accented-background-color: #555;
--more-accented-background-color: #777;
--header-background-color: #333;
@ -73,6 +81,9 @@ body.theme-dark {
--button-border-color: #444;
--button-text-color: white;
--button-border-radius: 5px;
--primary-button-background-color: #888;
--primary-button-text-color: white;
--primary-button-border-color: #999;
--muted-text-color: #ccc;
--input-text-color: white;
--input-background-color: #333;
@ -86,6 +97,7 @@ body.theme-dark {
--link-color: lightskyblue;
--modal-background-color: #333;
--modal-backdrop-color: #444;
--scrollbar-border-color: #888;
}
body.theme-dark .CodeMirror {
@ -184,7 +196,7 @@ body {
--ck-color-todo-list-checkmark-border: var(--main-border-color);
--ck-color-engine-placeholder-text: var(--main-text-color);
--ck-color-engine-placeholder-text: var(--muted-text-color);
}
body {

View file

@ -0,0 +1,15 @@
"use strict";
const fs = require('fs');
const dateUtils = require('../../services/date_utils');
const {LOG_DIR} = require('../../services/data_dir.js');
async function getBackendLog() {
const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`;
return fs.readFileSync(file, 'utf8');
}
module.exports = {
getBackendLog
};

View file

@ -2,6 +2,7 @@
const sql = require('../../services/sql');
const log = require('../../services/log');
const consistencyChecksService = require('../../services/consistency_checks');
async function vacuumDatabase() {
await sql.execute("VACUUM");
@ -9,6 +10,11 @@ async function vacuumDatabase() {
log.info("Database has been vacuumed.");
}
async function findAndFixConsistencyIssues() {
await consistencyChecksService.runOnDemandChecks(true);
}
module.exports = {
vacuumDatabase
vacuumDatabase,
findAndFixConsistencyIssues
};

View file

@ -15,7 +15,7 @@ async function findClippingNote(todayNote, pageUrl) {
const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl);
for (const note of notes) {
if (await note.getLabelValue('clipType') === 'clippings') {
if (await note.getOwnedLabelValue('clipType') === 'clippings') {
return note;
}
}
@ -31,7 +31,12 @@ async function addClipping(req) {
let clippingNote = await findClippingNote(todayNote, pageUrl);
if (!clippingNote) {
clippingNote = (await noteService.createNote(todayNote.noteId, title, '')).note;
clippingNote = (await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title: title,
content: '',
type: 'text'
})).note;
await clippingNote.setLabel('clipType', 'clippings');
await clippingNote.setLabel('pageUrl', pageUrl);
@ -51,7 +56,12 @@ async function createNote(req) {
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
const {note} = await noteService.createNote(todayNote.noteId, title, content);
const {note} = await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title,
content,
type: 'text'
});
await note.setLabel('clipType', clipType);
@ -88,7 +98,7 @@ async function addImagesToNote(images, note, content) {
noteId: note.noteId,
type: 'relation',
value: imageNote.noteId,
name: 'image-link'
name: 'imageLink'
}).save();
console.log(`Replacing ${imageId} with ${url}`);

22
src/routes/api/keys.js Normal file
View file

@ -0,0 +1,22 @@
"use strict";
const keyboardActions = require('../../services/keyboard_actions');
const sql = require('../../services/sql');
async function getKeyboardActions() {
return await keyboardActions.getKeyboardActions();
}
async function getShortcutsForNotes() {
return await sql.getMap(`
SELECT value, noteId
FROM attributes
WHERE isDeleted = 0
AND type = 'label'
AND name = 'keyboardShortcut'`);
}
module.exports = {
getKeyboardActions,
getShortcutsForNotes
};

View file

@ -112,15 +112,21 @@ async function eraseNoteRevision(req) {
}
async function getEditedNotesOnDate(req) {
const date = req.params.date;
const date = utils.sanitizeSql(req.params.date);
const notes = await repository.getEntities(`
select distinct notes.*
from notes
left join note_revisions using (noteId)
where substr(notes.dateCreated, 0, 11) = ?
or substr(notes.dateModified, 0, 11) = ?
or substr(note_revisions.dateLastEdited, 0, 11) = ?`, [date, date, date]);
SELECT notes.*
FROM notes
WHERE noteId IN (
SELECT noteId FROM notes
WHERE notes.dateCreated LIKE '${date}%'
OR notes.dateModified LIKE '${date}%'
UNION ALL
SELECT noteId FROM note_revisions
WHERE note_revisions.dateLastEdited LIKE '${date}%'
)
ORDER BY isDeleted
LIMIT 50`);
for (const note of notes) {
const notePath = noteCacheService.getNotePath(note.noteId);

View file

@ -27,36 +27,13 @@ async function getNote(req) {
return note;
}
async function getChildren(req) {
const parentNoteId = req.params.parentNoteId;
const parentNote = await repository.getNote(parentNoteId);
if (!parentNote) {
return [404, `Note ${parentNoteId} has not been found.`];
}
const ret = [];
for (const childNote of await parentNote.getChildNotes()) {
ret.push({
noteId: childNote.noteId,
title: childNote.title,
relations: (await childNote.getRelations()).map(relation => { return {
attributeId: relation.attributeId,
name: relation.name,
targetNoteId: relation.value
}; })
});
}
return ret;
}
async function createNote(req) {
const parentNoteId = req.params.parentNoteId;
const newNote = req.body;
const params = Object.assign({}, req.body); // clone
params.parentNoteId = req.params.parentNoteId;
const { note, branch } = await noteService.createNewNote(parentNoteId, newNote, req);
const { target, targetBranchId } = req.query;
const { note, branch } = await noteService.createNewNoteWithTarget(target, targetBranchId, params);
await treeService.setCssClassesToNotes([note]);
@ -128,7 +105,9 @@ async function getRelationMap(req) {
noteTitles: {},
relations: [],
// relation name => inverse relation name
inverseRelations: {}
inverseRelations: {
'internalLink': 'internalLink'
}
};
if (noteIds.length === 0) {
@ -194,7 +173,6 @@ module.exports = {
sortNotes,
protectSubtree,
setNoteTypeMime,
getChildren,
getRelationMap,
changeTitle,
duplicateNote

View file

@ -5,7 +5,7 @@ const log = require('../../services/log');
const attributes = require('../../services/attributes');
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = [
const ALLOWED_OPTIONS = new Set([
'protectedSessionTimeout',
'noteRevisionSnapshotTimeInterval',
'zoomFactor',
@ -37,23 +37,32 @@ const ALLOWED_OPTIONS = [
'spellCheckLanguageCode',
'imageMaxWidthHeight',
'imageJpegQuality'
];
]);
async function getOptions() {
return await optionService.getOptionsMap(ALLOWED_OPTIONS);
const optionMap = await optionService.getOptionsMap();
const resultMap = {};
for (const optionName in optionMap) {
if (isAllowed(optionName)) {
resultMap[optionName] = optionMap[optionName];
}
}
return resultMap;
}
async function updateOption(req) {
const {name, value} = req.params;
if (!update(name, value)) {
if (!await update(name, value)) {
return [400, "not allowed option to change"];
}
}
async function updateOptions(req) {
for (const optionName in req.body) {
if (!update(optionName, req.body[optionName])) {
if (!await update(optionName, req.body[optionName])) {
// this should be improved
// it should return 400 instead of current 500, but at least it now rollbacks transaction
throw new Error(`${optionName} is not allowed to change`);
@ -62,7 +71,7 @@ async function updateOptions(req) {
}
async function update(name, value) {
if (!ALLOWED_OPTIONS.includes(name)) {
if (!isAllowed(name)) {
return false;
}
@ -81,7 +90,7 @@ async function getUserThemes() {
const ret = [];
for (const note of notes) {
let value = await note.getLabelValue('appTheme');
let value = await note.getOwnedLabelValue('appTheme');
if (!value) {
value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-');
@ -97,6 +106,10 @@ async function getUserThemes() {
return ret;
}
function isAllowed(name) {
return ALLOWED_OPTIONS.has(name) || name.startsWith("keyboardShortcuts");
}
module.exports = {
getOptions,
updateOption,

View file

@ -26,10 +26,10 @@ async function uploadImage(req) {
async function saveNote(req) {
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
const {note, branch} = await noteService.createNewNote(parentNote.noteId, {
const {note, branch} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: req.body.title,
content: req.body.content,
target: 'into',
isProtected: false,
type: 'text',
mime: 'text/html'

View file

@ -17,12 +17,22 @@ async function getSchema() {
}
async function execute(req) {
const query = req.body.query;
const queries = req.body.query.split("\n---");
try {
const results = [];
for (const query of queries) {
if (!query.trim()) {
continue;
}
results.push(await sql.getRows(query));
}
return {
success: true,
rows: await sql.getRows(query)
results
};
}
catch (e) {

View file

@ -10,6 +10,8 @@ async function getNotesAndBranches(noteIds) {
noteIds = notes.map(n => n.noteId);
// joining child note to filter out not completely synchronised notes which would then cause errors later
// cannot do that with parent because of root note's 'none' parent
const branches = await sql.getManyRows(`
SELECT
branches.branchId,
@ -19,6 +21,7 @@ async function getNotesAndBranches(noteIds) {
branches.prefix,
branches.isExpanded
FROM branches
JOIN notes AS child ON child.noteId = branches.noteId
WHERE branches.isDeleted = 0
AND (branches.noteId IN (???) OR parentNoteId IN (???))`, noteIds);

View file

@ -12,10 +12,14 @@ function init(app) {
}
};
const respHeaders = {};
const res = {
statusCode: 200,
getHeader: () => {},
setHeader: () => {},
getHeader: name => respHeaders[name],
setHeader: (name, value) => {
respHeaders[name] = value.toString();
},
status: statusCode => {
res.statusCode = statusCode;
return res;
@ -24,6 +28,7 @@ function init(app) {
event.sender.send('server-response', {
requestId: arg.requestId,
statusCode: res.statusCode,
headers: respHeaders,
body: obj
});
}

View file

@ -34,6 +34,8 @@ const dateNotesRoute = require('./api/date_notes');
const linkMapRoute = require('./api/link_map');
const clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log');
const log = require('../services/log');
const express = require('express');
@ -42,6 +44,7 @@ const auth = require('../services/auth');
const cls = require('../services/cls');
const sql = require('../services/sql');
const protectedSessionService = require('../services/protected_session');
const syncTableService = require('../services/sync_table');
const csurf = require('csurf');
const csrfMiddleware = csurf({
@ -50,6 +53,8 @@ const csrfMiddleware = csurf({
});
function apiResultHandler(req, res, result) {
res.setHeader('trilium-max-sync-id', syncTableService.getMaxSyncId());
// if it's an array and first element is integer then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
@ -127,7 +132,6 @@ function register(app) {
apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
apiRoute(GET, '/api/notes/:parentNoteId/children', notesApiRoute.getChildren);
apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes);
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectSubtree);
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
@ -211,10 +215,11 @@ function register(app) {
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
apiRoute(POST, '/api/cleanup/cleanup-unused-images', cleanupRoute.cleanupUnusedImages);
// VACUUM requires execution outside of transaction
route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false);
route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles);
@ -242,7 +247,12 @@ function register(app) {
route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler);
route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler);
apiRoute(GET, '/api/similar_notes/:noteId', similarNotesRoute.getSimilarNotes);
apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes);
apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions);
apiRoute(GET, '/api/keyboard-shortcuts-for-notes', keysRoute.getShortcutsForNotes);
apiRoute(GET, '/api/backend-log', backendLogRoute.getBackendLog);
app.use('', router);
}

View file

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

View file

@ -21,6 +21,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'readOnly' },
{ type: 'label', name: 'cssClass' },
{ type: 'label', name: 'iconClass' },
{ type: 'label', name: 'keyboardShortcut' },
{ type: 'label', name: 'run', isDangerous: true },
{ type: 'label', name: 'customRequestHandler', isDangerous: true },
{ type: 'label', name: 'customResourceProvider', isDangerous: true },

View file

@ -170,6 +170,66 @@ function BackendScriptApi(currentNote, apiParams) {
*/
this.toggleNoteInParent = cloningService.toggleNoteInParent;
/**
* @typedef {object} CreateNoteAttribute
* @property {string} type - attribute type - label, relation etc.
* @property {string} name - attribute name
* @property {string} [value] - attribute value
*/
/**
* Create text note. See also createNewNote() for more options.
*
* @param {string} parentNoteId
* @param {string} title
* @param {string} content
* @return {Promise<{note: Note, branch: Branch}>}
*/
this.createTextNote = async (parentNoteId, title, content = '') => await noteService.createNewNote({
parentNoteId,
title,
content,
type: 'text'
});
/**
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
* JSON MIME type. See also createNewNote() for more options.
*
* @param {string} parentNoteId
* @param {string} title
* @param {object} content
* @return {Promise<{note: Note, branch: Branch}>}
*/
this.createDataNote = async (parentNoteId, title, content = {}) => await noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content),
type: 'code',
mime: 'application/json'
});
/**
* @typedef {object} CreateNewNoteParams
* @property {string} parentNoteId - MANDATORY
* @property {string} title - MANDATORY
* @property {string|buffer} content - MANDATORY
* @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY
* @property {string} mime - value is derived from default mimes for type
* @property {boolean} isProtected - default is false
* @property {boolean} isExpanded - default is false
* @property {string} prefix - default is empty string
* @property {int} notePosition - default is last existing notePosition in a parent + 10
*/
/**
* @method
*
* @param {CreateNewNoteParams} [params]
* @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNewNote = noteService.createNewNote;
/**
* @typedef {object} CreateNoteAttribute
* @property {string} type - attribute type - label, relation etc.
@ -195,25 +255,38 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNote = noteService.createNote;
this.createNote = async (parentNoteId, title, content = "", extraOptions= {}) => {
extraOptions.parentNoteId = parentNoteId;
extraOptions.title = title;
/**
* Creates new note according to given params and force all connected clients to refresh their tree.
*
* @method
*
* @param {string} parentNoteId - create new note under this parent
* @param {string} title
* @param {string} [content=""]
* @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNoteAndRefresh = async function(parentNoteId, title, content, extraOptions) {
const ret = await noteService.createNote(parentNoteId, title, content, extraOptions);
const parentNote = await repository.getNote(parentNoteId);
ws.refreshTree();
return ret;
// code note type can be inherited, otherwise text is default
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
if (extraOptions.json) {
extraOptions.content = JSON.stringify(content || {}, null, '\t');
extraOptions.type = 'code';
extraOptions.mime = 'application/json';
}
else {
extraOptions.content = content;
}
const {note, branch} = await noteService.createNewNote(extraOptions);
for (const attr of extraOptions.attributes || []) {
await attributeService.createAttribute({
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: !!attr.isInheritable
});
}
return {note, branch};
};
/**
@ -240,6 +313,14 @@ function BackendScriptApi(currentNote, apiParams) {
*/
this.getDateNote = dateNoteService.getDateNote;
/**
* Returns today's day note. If such note doesn't exist, it is created.
*
* @method
* @returns {Promise<Note|null>}
*/
this.getTodayNote = dateNoteService.getTodayNote;
/**
* Returns note for the first date of the week of the given date.
*

View file

@ -1 +1 @@
module.exports = { buildDate:"2019-12-03T22:31:20+01:00", buildRevision: "b0310e34e2c6f023cc8190310ff63d840157f6cc" };
module.exports = { buildDate:"2019-12-18T20:21:06+01:00", buildRevision: "5988776b7ea358a117556cf9aaf0e3c78cb705b4" };

View file

@ -11,7 +11,8 @@ const VIRTUAL_ATTRIBUTES = [
"content",
"type",
"mime",
"text"
"text",
"parentCount"
];
module.exports = function(filters, selectedColumns = 'notes.*') {
@ -43,6 +44,12 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
accessor = `${alias}.${property}`;
}
else if (property === 'parentCount') {
// need to cast as string for the equality operator to work
// for >= etc. it is cast again to DECIMAL
// also cannot use COUNT() in WHERE so using subquery ...
accessor = `CAST((SELECT COUNT(1) FROM branches WHERE branches.noteId = notes.noteId AND isDeleted = 0) AS STRING)`;
}
else {
accessor = "notes." + property;
}
@ -106,6 +113,11 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
condition = "NOT(" + condition + ")";
}
if (['text', 'title', 'content'].includes(filter.name)) {
// for title/content search does not make sense to search for protected notes
condition = `(${condition} AND notes.isProtected = 0)`;
}
where += condition;
}
else if ([">", ">=", "<", "<="].includes(filter.operator)) {

File diff suppressed because it is too large Load diff

View file

@ -13,12 +13,13 @@ const DATE_LABEL = 'dateNote';
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
async function createNote(parentNoteId, noteTitle, noteText) {
return (await noteService.createNewNote(parentNoteId, {
async function createNote(parentNoteId, noteTitle) {
return (await noteService.createNewNote({
parentNoteId: parentNoteId,
title: noteTitle,
content: noteText,
target: 'into',
isProtected: false
content: '',
isProtected: false,
type: 'text'
})).note;
}
@ -35,7 +36,8 @@ async function getRootCalendarNote() {
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
if (!rootNote) {
rootNote = (await noteService.createNewNote('root', {
rootNote = (await noteService.createNewNote({
parentNoteId: 'root',
title: 'Calendar',
target: 'into',
isProtected: false
@ -63,15 +65,15 @@ async function getYearNote(dateStr, rootNote) {
if (!yearNote) {
yearNote = await createNote(rootNote.noteId, yearStr);
}
await attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
await attributeService.createLabel(yearNote.noteId, 'sorted');
await attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
await attributeService.createLabel(yearNote.noteId, 'sorted');
const yearTemplateAttr = await rootNote.getOwnedAttribute('relation', 'yearTemplate');
const yearTemplateAttr = await rootNote.getOwnedAttribute('relation', 'yearTemplate');
if (yearTemplateAttr) {
await attributeService.createRelation(yearNote.noteId, 'template', yearTemplateAttr.value);
if (yearTemplateAttr) {
await attributeService.createRelation(yearNote.noteId, 'template', yearTemplateAttr.value);
}
}
}
@ -79,7 +81,7 @@ async function getYearNote(dateStr, rootNote) {
}
async function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
const pattern = await rootNote.getLabelValue("monthPattern") || "{monthNumberPadded} - {month}";
const pattern = await rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}";
const monthName = MONTHS[dateObj.getMonth()];
return pattern
@ -109,15 +111,15 @@ async function getMonthNote(dateStr, rootNote) {
const noteTitle = await getMonthNoteTitle(rootNote, monthNumber, dateObj);
monthNote = await createNote(yearNote.noteId, noteTitle);
}
await attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
await attributeService.createLabel(monthNote.noteId, 'sorted');
await attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
await attributeService.createLabel(monthNote.noteId, 'sorted');
const monthTemplateAttr = await rootNote.getOwnedAttribute('relation', 'monthTemplate');
const monthTemplateAttr = await rootNote.getOwnedAttribute('relation', 'monthTemplate');
if (monthTemplateAttr) {
await attributeService.createRelation(monthNote.noteId, 'template', monthTemplateAttr.value);
if (monthTemplateAttr) {
await attributeService.createRelation(monthNote.noteId, 'template', monthTemplateAttr.value);
}
}
}
@ -125,7 +127,7 @@ async function getMonthNote(dateStr, rootNote) {
}
async function getDateNoteTitle(rootNote, dayNumber, dateObj) {
const pattern = await rootNote.getLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const pattern = await rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = DAYS[dateObj.getDay()];
return pattern
@ -154,20 +156,24 @@ async function getDateNote(dateStr) {
const noteTitle = await getDateNoteTitle(rootNote, dayNumber, dateObj);
dateNote = await createNote(monthNote.noteId, noteTitle);
}
await attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10));
await attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10));
const dateTemplateAttr = await rootNote.getOwnedAttribute('relation', 'dateTemplate');
const dateTemplateAttr = await rootNote.getOwnedAttribute('relation', 'dateTemplate');
if (dateTemplateAttr) {
await attributeService.createRelation(dateNote.noteId, 'template', dateTemplateAttr.value);
if (dateTemplateAttr) {
await attributeService.createRelation(dateNote.noteId, 'template', dateTemplateAttr.value);
}
}
}
return dateNote;
}
async function getTodayNote() {
return await getDateNote(dateUtils.localNowDate());
}
function getStartOfTheWeek(date, startOfTheWeek) {
const day = date.getDay();
let diff;
@ -200,5 +206,6 @@ module.exports = {
getYearNote,
getMonthNote,
getWeekNote,
getDateNote
getDateNote,
getTodayNote
};

View file

@ -16,7 +16,7 @@ async function exportToOpml(taskContext, branch, version, res) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
if (!note.isStringNote() || await note.hasLabel('excludeFromExport')) {
if (!note.isStringNote() || await note.hasOwnedLabel('excludeFromExport')) {
return;
}

View file

@ -79,12 +79,17 @@ async function exportToTar(taskContext, branch, format, res) {
async function getNoteMeta(branch, parentMeta, existingFileNames) {
const note = await branch.getNote();
if (await note.hasLabel('excludeFromExport')) {
if (await note.hasOwnedLabel('excludeFromExport')) {
return;
}
const completeTitle = branch.prefix ? (branch.prefix + ' - ' + note.title) : note.title;
const baseFileName = sanitize(completeTitle);
let baseFileName = sanitize(completeTitle);
if (baseFileName.length > 200) { // actual limit is 256 bytes(!) but let's be conservative
baseFileName = baseFileName.substr(0, 200);
}
const notePath = parentMeta.notePath.concat([note.noteId]);
if (note.noteId in noteIdToMeta) {

View file

@ -27,7 +27,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
const parents = await note.getParentNotes();
for (const parent of parents) {
if (await parent.hasLabel("sorted")) {
if (await parent.hasOwnedLabel("sorted")) {
await treeService.sortNotesAlphabetically(parent.noteId);
}
}
@ -119,7 +119,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity
eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => {
await processInverseRelations(entityName, entity, async (definition, note, targetNote) => {
// if one inverse attribute is deleted then the other should be deleted as well
const relations = await targetNote.getRelations(definition.inverseRelation);
const relations = await targetNote.getOwnedRelations(definition.inverseRelation);
let deletedSomething = false;
for (const relation of relations) {

View file

@ -55,14 +55,17 @@ async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSw
const parentNote = await repository.getNote(parentNoteId);
const {note} = await noteService.createNote(parentNoteId, fileName, buffer, {
target: 'into',
const {note} = await noteService.createNewNote({
parentNoteId,
title: fileName,
content: buffer,
type: 'image',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
mime: 'image/' + imageFormat.ext.toLowerCase(),
attributes: [{ type: 'label', name: 'originalFileName', value: originalName }]
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
await note.addLabel('originalFileName', originalName);
return {
fileName,
note,

View file

@ -28,7 +28,10 @@ async function importEnex(taskContext, file, parentNote) {
: file.originalname;
// root note is new note into all ENEX/notebook's notes will be imported
const rootNote = (await noteService.createNote(parentNote.noteId, rootNoteTitle, "", {
const rootNote = (await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: rootNoteTitle,
content: "",
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
@ -212,14 +215,20 @@ async function importEnex(taskContext, file, parentNote) {
content = extractContent(content);
const noteEntity = (await noteService.createNote(rootNote.noteId, title, content, {
attributes,
const noteEntity = (await noteService.createNewNote({
parentNoteId: rootNote.noteId,
title,
content,
utcDateCreated,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
})).note;
for (const attr of attributes) {
await noteEntity.addAttribute(attr.type, attr.name, attr.value);
}
utcDateCreated = utcDateCreated || noteEntity.utcDateCreated;
// sometime date modified is not present in ENEX, then use date created
utcDateModified = utcDateModified || utcDateCreated;
@ -240,13 +249,19 @@ async function importEnex(taskContext, file, parentNote) {
}
const createFileNote = async () => {
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
attributes: resource.attributes,
const resourceNote = (await noteService.createNewNote({
parentNoteId: noteEntity.noteId,
title: resource.title,
content: resource.content,
type: 'file',
mime: resource.mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
})).note;
for (const attr of resource.attributes) {
await noteEntity.addAttribute(attr.type, attr.name, attr.value);
}
await updateDates(resourceNote.noteId, utcDateCreated, utcDateModified);
taskContext.increaseProgressCount();

View file

@ -44,8 +44,12 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
throw new Error("Unrecognized OPML version " + opmlVersion);
}
const {note} = await noteService.createNote(parentNoteId, title, content, {
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
const {note} = await noteService.createNewNote({
parentNoteId,
title,
content,
type: 'text',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
taskContext.increaseProgressCount();

View file

@ -41,16 +41,18 @@ async function importImage(file, parentNote, taskContext) {
async function importFile(taskContext, file, parentNote) {
const originalName = file.originalname;
const size = file.size;
const {note} = await noteService.createNote(parentNote.noteId, originalName, file.buffer, {
target: 'into',
const {note} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: originalName,
content: file.buffer,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: 'file',
mime: mimeService.getMime(originalName) || file.mimetype,
attributes: [{ type: "label", name: "originalFileName", value: originalName }]
mime: mimeService.getMime(originalName) || file.mimetype
});
await note.addLabel("originalFileName", originalName);
taskContext.increaseProgressCount();
return note;
@ -62,7 +64,10 @@ async function importCodeNote(taskContext, file, parentNote) {
const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
const mime = mimeService.normalizeMimeType(detectedMime);
const {note} = await noteService.createNote(parentNote.noteId, title, content, {
const {note} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content,
type: 'code',
mime: mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
@ -78,7 +83,10 @@ async function importPlainText(taskContext, file, parentNote) {
const plainTextContent = file.buffer.toString("UTF-8");
const htmlContent = convertTextToHtml(plainTextContent);
const {note} = await noteService.createNote(parentNote.noteId, title, htmlContent, {
const {note} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content: htmlContent,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
@ -118,7 +126,10 @@ async function importMarkdown(taskContext, file, parentNote) {
const title = getFileNameWithoutExtension(file.originalname);
const {note} = await noteService.createNote(parentNote.noteId, title, htmlContent, {
const {note} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content: htmlContent,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
@ -133,7 +144,10 @@ async function importHtml(taskContext, file, parentNote) {
const title = getFileNameWithoutExtension(file.originalname);
const content = file.buffer.toString("UTF-8");
const {note} = await noteService.createNote(parentNote.noteId, title, content, {
const {note} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),

View file

@ -147,7 +147,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
continue;
}
if (attr.type === 'relation' && ['internal-link', 'image-link', 'relation-map-link'].includes(attr.name)) {
if (attr.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink'].includes(attr.name)) {
// these relations are created automatically and as such don't need to be duplicated in the import
continue;
}
@ -177,8 +177,11 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return;
}
({note} = await noteService.createNote(parentNoteId, noteTitle, '', {
noteId,
({note} = await noteService.createNewNote({
parentNoteId: parentNoteId,
title: noteTitle,
content: '',
noteId: noteId,
type: noteMeta ? noteMeta.type : 'text',
mime: noteMeta ? noteMeta.mime : 'text/html',
prefix: noteMeta ? noteMeta.prefix : '',
@ -310,7 +313,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
if (type === 'relation-map' && noteMeta) {
const relationMapLinks = (noteMeta.attributes || [])
.filter(attr => attr.type === 'relation' && attr.name === 'relation-map-link');
.filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
// this will replace relation map links
for (const link of relationMapLinks) {
@ -325,7 +328,10 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
await note.setContent(content);
}
else {
({note} = await noteService.createNote(parentNoteId, noteTitle, content, {
({note} = await noteService.createNewNote({
parentNoteId: parentNoteId,
title: noteTitle,
content: content,
noteId,
type,
mime,

Some files were not shown because too many files have changed in this diff Show more