mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 04:16:17 +01:00 
			
		
		
		
	Compare commits
	
		
			56 Commits
		
	
	
		
			v0.48.1-be
			...
			v0.48.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6f60cf1a86 | ||
| 
						 | 
					930d29d64a | ||
| 
						 | 
					c345e7031b | ||
| 
						 | 
					fc46398a3c | ||
| 
						 | 
					7e41226549 | ||
| 
						 | 
					b0c0ed8512 | ||
| 
						 | 
					5978447185 | ||
| 
						 | 
					ec4d445f97 | ||
| 
						 | 
					e378435fbe | ||
| 
						 | 
					e4dca4750f | ||
| 
						 | 
					311b98ffcb | ||
| 
						 | 
					126f41ae5e | ||
| 
						 | 
					4b1678c416 | ||
| 
						 | 
					da74272f13 | ||
| 
						 | 
					9ce224d4c5 | ||
| 
						 | 
					ce57a13002 | ||
| 
						 | 
					e42357f1f8 | ||
| 
						 | 
					ab7d121290 | ||
| 
						 | 
					5209583a73 | ||
| 
						 | 
					35fc4ba9a4 | ||
| 
						 | 
					7f9019322b | ||
| 
						 | 
					8a455e83f0 | ||
| 
						 | 
					79b8d91025 | ||
| 
						 | 
					bcabe5786f | ||
| 
						 | 
					a7d3dafcf1 | ||
| 
						 | 
					e6af84df39 | ||
| 
						 | 
					674172f0b8 | ||
| 
						 | 
					7ec20f9384 | ||
| 
						 | 
					6135de8507 | ||
| 
						 | 
					63b0d30e74 | ||
| 
						 | 
					9e29fba8da | ||
| 
						 | 
					8910ae92c7 | ||
| 
						 | 
					3413074235 | ||
| 
						 | 
					33aa72eb97 | ||
| 
						 | 
					6e0a65b59c | ||
| 
						 | 
					56e49cfc19 | ||
| 
						 | 
					af40d73cee | ||
| 
						 | 
					50b7063811 | ||
| 
						 | 
					ee1b377bc2 | ||
| 
						 | 
					51dfe8bb14 | ||
| 
						 | 
					e426ee3e4f | ||
| 
						 | 
					a434aa113d | ||
| 
						 | 
					3b551e3e4d | ||
| 
						 | 
					3d98644bf6 | ||
| 
						 | 
					a22e4d60b6 | ||
| 
						 | 
					069fbee3a6 | ||
| 
						 | 
					8b21867c5c | ||
| 
						 | 
					2cc4367b37 | ||
| 
						 | 
					241d1b1035 | ||
| 
						 | 
					449081807e | ||
| 
						 | 
					dc0d6d24bd | ||
| 
						 | 
					4111a2f0e8 | ||
| 
						 | 
					8ef4b2bf50 | ||
| 
						 | 
					53875d26bc | ||
| 
						 | 
					f505f9d65a | ||
| 
						 | 
					6434889cd6 | 
							
								
								
									
										53
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
name: Publish Docker image
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags: [v*]
 | 
			
		||||
jobs:
 | 
			
		||||
  push_to_registries:
 | 
			
		||||
    name: Push Docker image to multiple registries
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write
 | 
			
		||||
      contents: read
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v1
 | 
			
		||||
      - name: Docker meta
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            zadam/trilium
 | 
			
		||||
            ghcr.io/zadam/trilium
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
            type=semver,pattern={{major}}.{{minor}}-latest
 | 
			
		||||
            type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          install: true
 | 
			
		||||
      - name: Log in to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to GitHub Docker Registry
 | 
			
		||||
        uses: docker/login-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.repository_owner }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      - name: Create server-package.json
 | 
			
		||||
        run: cat package.json | grep -v electron > server-package.json
 | 
			
		||||
      - name: Build and Push
 | 
			
		||||
        uses: docker/build-push-action@v2.7.0
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
 | 
			
		||||
          push: true
 | 
			
		||||
          cache-from: type=registry,ref=zadam/trilium:buildcache
 | 
			
		||||
          cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
@@ -2,7 +2,7 @@ image:
 | 
			
		||||
  file: .gitpod.dockerfile
 | 
			
		||||
 | 
			
		||||
tasks:
 | 
			
		||||
    - before: nvm install 14.17.6 && nvm use 14.17.6
 | 
			
		||||
    - before: nvm install 14.18.1 && nvm use 14.18.1
 | 
			
		||||
      init: npm install
 | 
			
		||||
      command: npm run start-server
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:14.17.6-alpine
 | 
			
		||||
FROM node:14.18.1-alpine
 | 
			
		||||
 | 
			
		||||
# Create app directory
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 
 | 
			
		||||
@@ -5,3 +5,18 @@ echo "Packaging debian x64 distribution..."
 | 
			
		||||
VERSION=`jq -r ".version" package.json`
 | 
			
		||||
 | 
			
		||||
./node_modules/.bin/electron-installer-debian --config bin/deb-options.json --options.version=${VERSION} --arch amd64
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# hacky stop-gag measure to produce debian compatible XZ compressed debs until this is fixed: https://github.com/electron-userland/electron-installer-debian/issues/272
 | 
			
		||||
cd dist
 | 
			
		||||
ar x trilium_${VERSION}_amd64.deb
 | 
			
		||||
rm trilium_${VERSION}_amd64.deb
 | 
			
		||||
# recompress
 | 
			
		||||
< control.tar.zst zstd -d | xz > control.tar.xz
 | 
			
		||||
< data.tar.zst zstd -d | xz > data.tar.xz
 | 
			
		||||
# create deb archive (I really do not know, what argument "sdsd" is for but something is required for ar to create the archive as desired)
 | 
			
		||||
ar -m -c -a sdsd trilium_${VERSION}_amd64.deb debian-binary control.tar.xz data.tar.xz
 | 
			
		||||
 | 
			
		||||
rm control* data* debian-binary
 | 
			
		||||
 | 
			
		||||
echo "Converted to XZ deb"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
PKG_DIR=dist/trilium-linux-x64-server
 | 
			
		||||
NODE_VERSION=14.17.6
 | 
			
		||||
NODE_VERSION=14.18.1
 | 
			
		||||
 | 
			
		||||
if [ "$1" != "DONTCOPY" ]
 | 
			
		||||
then
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
n exec 14.17.6 npm run webpack
 | 
			
		||||
n exec 14.18.1 npm run webpack
 | 
			
		||||
 | 
			
		||||
DIR=$1
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +27,7 @@ cp -r electron.js $DIR/
 | 
			
		||||
cp webpack-* $DIR/
 | 
			
		||||
 | 
			
		||||
# run in subshell (so we return to original dir)
 | 
			
		||||
(cd $DIR && n exec 14.17.6 npm install --only=prod)
 | 
			
		||||
(cd $DIR && n exec 14.18.1 npm install --only=prod)
 | 
			
		||||
 | 
			
		||||
# cleanup of useless files in dependencies
 | 
			
		||||
rm -r $DIR/node_modules/image-q/demo
 | 
			
		||||
 
 | 
			
		||||
@@ -69,13 +69,3 @@ gh release create "$TAG" \
 | 
			
		||||
    "dist/$WINDOWS_X64_BUILD" \
 | 
			
		||||
    "dist/$MAC_X64_BUILD" \
 | 
			
		||||
    "dist/$SERVER_BUILD"
 | 
			
		||||
 | 
			
		||||
echo "Building docker image"
 | 
			
		||||
 | 
			
		||||
bin/build-docker.sh $VERSION
 | 
			
		||||
 | 
			
		||||
echo "Pushing docker image to dockerhub"
 | 
			
		||||
 | 
			
		||||
bin/push-docker-image.sh $VERSION
 | 
			
		||||
 | 
			
		||||
echo "Release finished!"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										67
									
								
								images/icon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								images/icon.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   width="210mm"
 | 
			
		||||
   height="297mm"
 | 
			
		||||
   viewBox="0 0 210 297"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg2163"
 | 
			
		||||
   inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
 | 
			
		||||
   sodipodi:docname="cropped.svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs2157" />
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="base"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1.0"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pageshadow="2"
 | 
			
		||||
     inkscape:zoom="1.5099157"
 | 
			
		||||
     inkscape:cx="386.95033"
 | 
			
		||||
     inkscape:cy="314.49555"
 | 
			
		||||
     inkscape:document-units="mm"
 | 
			
		||||
     inkscape:current-layer="layer1"
 | 
			
		||||
     inkscape:document-rotation="0"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     inkscape:window-width="1245"
 | 
			
		||||
     inkscape:window-height="846"
 | 
			
		||||
     inkscape:window-x="60"
 | 
			
		||||
     inkscape:window-y="0"
 | 
			
		||||
     inkscape:window-maximized="0" />
 | 
			
		||||
  <metadata
 | 
			
		||||
     id="metadata2160">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work
 | 
			
		||||
         rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type
 | 
			
		||||
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
 | 
			
		||||
        <dc:title></dc:title>
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <g
 | 
			
		||||
     inkscape:label="Ebene 1"
 | 
			
		||||
     inkscape:groupmode="layer"
 | 
			
		||||
     id="layer1">
 | 
			
		||||
    <path
 | 
			
		||||
       d="m 139.19968,46.483949 c -5.40456,-0.16722 -8.20491,2.3622 -8.88118,2.5968 0.61524,-1.16523 1.02694,-1.89936 2.60032,-3.53766 -5.35164,-0.0561 -10.31381,1.10208 -14.09982,4.64397 -5.07788,4.75015 -4.68312,8.68574 -4.69335,8.66634 0.001,0.002 -0.80116,-2.56646 -1.04176,-5.31354 -5.52591,7.64928 -7.70784,17.78459 -3.51825,25.34532 2.60068,-6.2738 6.73029,-11.68083 12.0523,-15.9445 0.17533,-4.47393 1.9752,-8.25394 4.82,-11.04477 -2.56716,3.0727 -3.69993,6.57331 -3.58563,10.10144 6.19019,-4.61821 13.77738,-7.6962 22.11141,-8.7062 -6.03391,1.10878 -11.50091,3.51367 -16.27929,6.7938 2.99085,1.36595 6.40645,1.58009 9.99631,0.32984 -3.4417,1.6129 -7.26228,1.87678 -11.12731,0.47202 -3.43041,2.51213 -6.48124,5.4804 -9.10343,8.74254 4.65525,1.52188 9.74725,1.2072 14.81455,-1.37548 -4.65173,3.00461 -10.11661,4.13138 -15.95614,2.85185 -2.32798,3.1242 -4.2792,6.47983 -5.80426,9.93811 1.87855,1.385 10.03089,5.73052 24.01465,-4.58153 -1.9745,-0.58349 -3.5747,-1.40864 -3.57541,-1.41005 0.0445,0.0198 1.4411,0.14817 5.52556,-2.2987 3.61104,-2.11173 6.71724,-5.9369 9.0103,-8.91434 -1.95051,-0.71861 -3.46957,-1.47285 -3.47028,-1.47461 -0.001,-10e-4 2.5206,0.16968 6.4322,-2.8247 3.48897,-2.67088 7.38399,-6.54261 7.39599,-6.43325 0.0247,0.0233 -8.59226,-6.3433 -17.63748,-6.6227"
 | 
			
		||||
       style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
 | 
			
		||||
       id="path1983" />
 | 
			
		||||
    <path
 | 
			
		||||
       d="m 76.908921,60.394149 c 4.10149,-1.19228 6.7131,0.18422 7.27437,0.22948 -0.68748,-0.76706 -1.13907,-1.24548 -2.64709,-2.18406 4.08133,-1.09739 8.09089,-1.19268 11.64521,0.76111 4.7672,2.6204 5.19783,5.69818 5.20227,5.68151 -8.1e-4,0.002 0.13467,-2.11419 -0.19274,-4.25578 5.649429,4.74214 9.204969,12.03797 7.409049,18.6271 -3.15635,-4.27007 -7.320749,-7.57773 -12.183899,-9.77927 -0.96691,-3.37555 -3.04709,-5.90265 -5.74177,-7.46934 2.535,1.83627 4.0527,4.28158 4.62244,6.9933 -5.59297,-2.30039 -11.96698,-3.15163 -18.52727,-2.27895 4.82002,-0.34383 15.0298,0.35455 23.31423,7.22913 -3.27571,2.07767 -7.22761,2.84155 -11.58272,1.87121 4.116,1.37333 8.50418,1.15576 12.73066,-0.97074 2.361629,1.92282 4.478499,4.09616 6.288529,6.4319 -1.1786,1.42613 -6.602629,6.34494 -19.214749,1.23993 1.40117,-0.83377 2.47089,-1.77831 2.47129,-1.7795 -0.0302,0.0238 -1.07415,0.39703 -4.65308,-0.66304 -3.15394,-0.89808 -6.24094,-3.20205 -8.54894,-5.02006 1.35762,-0.93183 2.37896,-1.80609 2.37896,-1.80768 3.9e-4,-8.1e-4 -1.89551,0.62612 -5.44379,-0.88538 -3.16522,-1.34871 -6.86431,-3.53238 -6.85302,-3.44662 -0.0145,0.0226 5.38855,-6.52877 12.25206,-8.52425"
 | 
			
		||||
       style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.040011"
 | 
			
		||||
       id="path1985" />
 | 
			
		||||
    <path
 | 
			
		||||
       d="m 106.35028,116.78676 c -2.99089,-2.78316 -3.11891,-5.61706 -3.35465,-6.10128 -0.30328,0.94728 -0.48119,1.55657 -0.5271,3.26459 -2.902599,-2.81363 -4.935969,-6.06629 -5.042799,-9.9638 -0.14303,-5.226251 2.20774,-7.111581 2.19185,-7.106711 0.002,0 -1.82455,0.94068 -3.44692,2.27518 1.19194,-7.00825 5.529279,-13.56279 11.887189,-15.36164 -2.01438,4.72154 -2.73705,9.79389 -2.19847,14.89152 -2.33709,2.474751 -3.42573,5.443121 -3.41602,8.440321 0.29225,-2.99941 1.58705,-5.46485 3.56567,-7.282331 0.81185,5.747411 3.21074,11.417161 7.13399,16.382681 -2.63507,-3.79648 -7.03025,-12.54682 -5.34828,-22.784661 3.32507,1.66359 5.88686,4.53616 7.20241,8.603531 -0.86349,-4.070921 -3.18336,-7.574601 -7.01259,-9.996131 0.44853,-2.89963 1.2255,-5.72253 2.28632,-8.37424 1.76099,0.26123 8.49766,2.27917 10.39682,15.19844 -1.37647,-0.73844 -2.68363,-1.14912 -2.68496,-1.14868 0.0349,0.0129 0.85422,0.68654 1.7164,4.159181 0.78977,3.04292 0.37789,6.72937 -0.0102,9.53311 -1.43739,-0.65417 -2.66244,-1.05988 -2.66332,-1.059 -0.001,0 1.44445,1.24891 1.91637,4.92099 0.42071,3.27585 0.40659,7.40616 0.47236,7.35418 0.026,8.4e-4 -8.0584,-1.18846 -13.06411,-5.84525"
 | 
			
		||||
       style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0442482"
 | 
			
		||||
       id="path1987" />
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 5.4 KiB  | 
							
								
								
									
										100
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										100
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "version": "0.48.0-beta",
 | 
			
		||||
  "version": "0.48.4",
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
@@ -1606,16 +1606,16 @@
 | 
			
		||||
      "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
 | 
			
		||||
    },
 | 
			
		||||
    "browserslist": {
 | 
			
		||||
      "version": "4.17.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.3.tgz",
 | 
			
		||||
      "integrity": "sha512-59IqHJV5VGdcJZ+GZ2hU5n4Kv3YiASzW6Xk5g9tf5a/MAzGeFwgGWU39fVzNIOVcgB3+Gp+kiQu0HEfTVU/3VQ==",
 | 
			
		||||
      "version": "4.17.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.4.tgz",
 | 
			
		||||
      "integrity": "sha512-Zg7RpbZpIJRW3am9Lyckue7PLytvVxxhJj1CaJVlCWENsGEAOlnlt8X0ZxGRPp7Bt9o8tIRM5SEXy4BCPMJjLQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "caniuse-lite": "^1.0.30001264",
 | 
			
		||||
        "electron-to-chromium": "^1.3.857",
 | 
			
		||||
        "caniuse-lite": "^1.0.30001265",
 | 
			
		||||
        "electron-to-chromium": "^1.3.867",
 | 
			
		||||
        "escalade": "^3.1.1",
 | 
			
		||||
        "node-releases": "^1.1.77",
 | 
			
		||||
        "picocolors": "^0.2.1"
 | 
			
		||||
        "node-releases": "^2.0.0",
 | 
			
		||||
        "picocolors": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buffer": {
 | 
			
		||||
@@ -2188,11 +2188,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
 | 
			
		||||
      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
 | 
			
		||||
    },
 | 
			
		||||
    "colorette": {
 | 
			
		||||
      "version": "1.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
 | 
			
		||||
      "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
 | 
			
		||||
    },
 | 
			
		||||
    "colors": {
 | 
			
		||||
      "version": "1.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
 | 
			
		||||
@@ -2855,9 +2850,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "electron": {
 | 
			
		||||
      "version": "13.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/electron/-/electron-13.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-CPakwDpy5m8dL0383F5uJboQcVtn9bT/+6/wdDKo8LuTUO9aER1TF41v7feZgZW2c+UwoGPWa814ElSQ3qta2A==",
 | 
			
		||||
      "version": "13.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/electron/-/electron-13.6.1.tgz",
 | 
			
		||||
      "integrity": "sha512-rZ6Y7RberigruefQpWOiI4bA9ppyT88GQF8htY6N1MrAgal5RrBc+Mh92CcGU7zT9QO+XO3DarSgZafNTepffQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@electron/get": "^1.0.1",
 | 
			
		||||
@@ -2866,9 +2861,9 @@
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/node": {
 | 
			
		||||
          "version": "14.17.21",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz",
 | 
			
		||||
          "integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==",
 | 
			
		||||
          "version": "14.17.32",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.32.tgz",
 | 
			
		||||
          "integrity": "sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ==",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -3551,9 +3546,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "electron-to-chromium": {
 | 
			
		||||
      "version": "1.3.864",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.864.tgz",
 | 
			
		||||
      "integrity": "sha512-v4rbad8GO6/yVI92WOeU9Wgxc4NA0n4f6P1FvZTY+jyY7JHEhw3bduYu60v3Q1h81Cg6eo4ApZrFPuycwd5hGw==",
 | 
			
		||||
      "version": "1.3.867",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.867.tgz",
 | 
			
		||||
      "integrity": "sha512-WbTXOv7hsLhjJyl7jBfDkioaY++iVVZomZ4dU6TMe/SzucV6mUAs2VZn/AehBwuZMiNEQDaPuTGn22YK5o+aDw==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "electron-window-state": {
 | 
			
		||||
@@ -4944,19 +4939,19 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "jasmine": {
 | 
			
		||||
      "version": "3.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.9.0.tgz",
 | 
			
		||||
      "integrity": "sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==",
 | 
			
		||||
      "version": "3.10.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.10.0.tgz",
 | 
			
		||||
      "integrity": "sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "glob": "^7.1.6",
 | 
			
		||||
        "jasmine-core": "~3.9.0"
 | 
			
		||||
        "jasmine-core": "~3.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "jasmine-core": {
 | 
			
		||||
      "version": "3.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.9.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==",
 | 
			
		||||
      "version": "3.10.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.0.tgz",
 | 
			
		||||
      "integrity": "sha512-XWGaJ25RUdOQnjGiLoQa9QG/R4u1e9Bk4uhLdn9F4JCBco84L4SKM52bxci4vWTSUzhmhuHNAkAHFN/6Cox9wQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "jest-worker": {
 | 
			
		||||
@@ -5782,9 +5777,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "nanoid": {
 | 
			
		||||
      "version": "3.1.25",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
 | 
			
		||||
      "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q=="
 | 
			
		||||
      "version": "3.1.30",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
 | 
			
		||||
      "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "napi-build-utils": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
@@ -5844,9 +5839,9 @@
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node-releases": {
 | 
			
		||||
      "version": "1.1.77",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz",
 | 
			
		||||
      "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==",
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-aA87l0flFYMzCHpTM3DERFSYxc6lv/BltdbRTOMZuxZ0cwZCD3mejE5n9vLhSJCN++/eOqr77G1IO5uXxlQYWA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "nopt": {
 | 
			
		||||
@@ -6308,9 +6303,9 @@
 | 
			
		||||
      "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA=="
 | 
			
		||||
    },
 | 
			
		||||
    "picocolors": {
 | 
			
		||||
      "version": "0.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "picomatch": {
 | 
			
		||||
@@ -6380,13 +6375,20 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "postcss": {
 | 
			
		||||
      "version": "8.3.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz",
 | 
			
		||||
      "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==",
 | 
			
		||||
      "version": "8.3.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.9.tgz",
 | 
			
		||||
      "integrity": "sha512-f/ZFyAKh9Dnqytx5X62jgjhhzttjZS7hMsohcI7HEI5tjELX/HxCy3EFhsRxyzGvrzFF+82XPvCS8T9TFleVJw==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "colorette": "^1.2.2",
 | 
			
		||||
        "nanoid": "^3.1.23",
 | 
			
		||||
        "nanoid": "^3.1.28",
 | 
			
		||||
        "picocolors": "^0.2.1",
 | 
			
		||||
        "source-map-js": "^0.6.2"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "picocolors": {
 | 
			
		||||
          "version": "0.2.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
 | 
			
		||||
          "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "prebuild-install": {
 | 
			
		||||
@@ -6862,9 +6864,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "sanitize-html": {
 | 
			
		||||
      "version": "2.5.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.5.1.tgz",
 | 
			
		||||
      "integrity": "sha512-hUITPitQk+eFNLtr4dEkaaiAJndG2YE87IOpcfBSL1XdklWgwcNDJdr9Ppe8QKL/C3jFt1xH/Mbj20e0GZQOfg==",
 | 
			
		||||
      "version": "2.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-sJ1rO2YixFIqs2kIcEUb6PTrCjvz8DMq1XqWWuy0kjgjrn58GNLK1DKSIRybFZDO1WNgsEgD+WiEzTEYS8xEug==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "deepmerge": "^4.2.2",
 | 
			
		||||
        "escape-string-regexp": "^4.0.0",
 | 
			
		||||
@@ -8030,9 +8032,9 @@
 | 
			
		||||
      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
 | 
			
		||||
    },
 | 
			
		||||
    "webpack": {
 | 
			
		||||
      "version": "5.58.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.58.1.tgz",
 | 
			
		||||
      "integrity": "sha512-4Z/dmbTU+VmkCb2XNgW7wkE5TfEcSooclprn/UEuVeAkwHhn07OcgUsyaKHGtCY/VobjnsYBlyhKeMLiSoOqPg==",
 | 
			
		||||
      "version": "5.58.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.58.2.tgz",
 | 
			
		||||
      "integrity": "sha512-3S6e9Vo1W2ijk4F4PPWRIu6D/uGgqaPmqw+av3W3jLDujuNkdxX5h5c+RQ6GkjVR+WwIPOfgY8av+j5j4tMqJw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@types/eslint-scope": "^3.7.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "productName": "Trilium Notes",
 | 
			
		||||
  "description": "Trilium Notes",
 | 
			
		||||
  "version": "0.48.1-beta",
 | 
			
		||||
  "version": "0.48.5",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "main": "electron.js",
 | 
			
		||||
  "bin": {
 | 
			
		||||
@@ -66,7 +66,7 @@
 | 
			
		||||
    "request": "^2.88.2",
 | 
			
		||||
    "rimraf": "3.0.2",
 | 
			
		||||
    "sanitize-filename": "1.6.3",
 | 
			
		||||
    "sanitize-html": "2.5.1",
 | 
			
		||||
    "sanitize-html": "2.5.2",
 | 
			
		||||
    "sax": "1.2.4",
 | 
			
		||||
    "semver": "7.3.5",
 | 
			
		||||
    "serve-favicon": "2.5.0",
 | 
			
		||||
@@ -81,16 +81,16 @@
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "cross-env": "7.0.3",
 | 
			
		||||
    "electron": "13.5.2",
 | 
			
		||||
    "electron": "13.6.1",
 | 
			
		||||
    "electron-builder": "22.13.1",
 | 
			
		||||
    "electron-packager": "15.4.0",
 | 
			
		||||
    "electron-rebuild": "3.2.3",
 | 
			
		||||
    "esm": "3.2.25",
 | 
			
		||||
    "jasmine": "3.9.0",
 | 
			
		||||
    "jasmine": "3.10.0",
 | 
			
		||||
    "jsdoc": "3.6.7",
 | 
			
		||||
    "lorem-ipsum": "2.0.4",
 | 
			
		||||
    "rcedit": "3.0.1",
 | 
			
		||||
    "webpack": "5.58.1",
 | 
			
		||||
    "webpack": "5.58.2",
 | 
			
		||||
    "webpack-cli": "4.9.0"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ const Attribute = require('../../src/becca/entities/attribute.js');
 | 
			
		||||
const becca = require('../../src/becca/becca.js');
 | 
			
		||||
const randtoken = require('rand-token').generator({source: 'crypto'});
 | 
			
		||||
 | 
			
		||||
/** @return {Note} */
 | 
			
		||||
/** @returns {Note} */
 | 
			
		||||
function findNoteByTitle(searchResults, title) {
 | 
			
		||||
    return searchResults
 | 
			
		||||
        .map(sr => becca.notes[sr.noteId])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const sql = require("../services/sql.js");
 | 
			
		||||
const NoteRevision = require("./entities/note_revision.js");
 | 
			
		||||
const RecentNote = require("./entities/recent_note.js");
 | 
			
		||||
const NoteSet = require("../services/search/note_set");
 | 
			
		||||
 | 
			
		||||
class Becca {
 | 
			
		||||
@@ -27,7 +25,7 @@ class Becca {
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    /** @returns {Attribute[]} */
 | 
			
		||||
    findAttributes(type, name) {
 | 
			
		||||
        name = name.trim().toLowerCase();
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +36,7 @@ class Becca {
 | 
			
		||||
        return this.attributeIndex[`${type}-${name}`] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    /** @returns {Attribute[]} */
 | 
			
		||||
    findAttributesWithPrefix(type, name) {
 | 
			
		||||
        const resArr = [];
 | 
			
		||||
        const key = `${type}-${name}`;
 | 
			
		||||
@@ -102,6 +100,7 @@ class Becca {
 | 
			
		||||
    getNoteRevision(noteRevisionId) {
 | 
			
		||||
        const row = sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]);
 | 
			
		||||
 | 
			
		||||
        const NoteRevision = require("./entities/note_revision.js"); // avoiding circular dependency problems
 | 
			
		||||
        return row ? new NoteRevision(row) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -131,12 +130,14 @@ class Becca {
 | 
			
		||||
    getRecentNotesFromQuery(query, params = []) {
 | 
			
		||||
        const rows = sql.getRows(query, params);
 | 
			
		||||
 | 
			
		||||
        const RecentNote = require("./entities/recent_note.js"); // avoiding circular dependency problems
 | 
			
		||||
        return rows.map(row => new RecentNote(row));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getNoteRevisionsFromQuery(query, params = []) {
 | 
			
		||||
        const rows = sql.getRows(query, params);
 | 
			
		||||
 | 
			
		||||
        const NoteRevision = require("./entities/note_revision.js"); // avoiding circular dependency problems
 | 
			
		||||
        return rows.map(row => new NoteRevision(row));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,10 @@ function load() {
 | 
			
		||||
        new Option(row);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const noteId in becca.notes) {
 | 
			
		||||
        becca.notes[noteId].sortParents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    becca.loaded = true;
 | 
			
		||||
 | 
			
		||||
    log.info(`Becca (note cache) load took ${Date.now() - start}ms`);
 | 
			
		||||
@@ -66,7 +70,7 @@ function postProcessEntityUpdate(entityName, entity) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
eventService.subscribe([eventService.ENTITY_CHANGE_SYNCED],  ({entityName, entityRow}) => {
 | 
			
		||||
eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED],  ({entityName, entityRow}) => {
 | 
			
		||||
    if (!becca.loaded) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -89,7 +93,7 @@ eventService.subscribe([eventService.ENTITY_CHANGE_SYNCED],  ({entityName, entit
 | 
			
		||||
    postProcessEntityUpdate(entityName, entityRow);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
eventService.subscribe(eventService.ENTITY_CHANGED,  ({entityName, entity}) => {
 | 
			
		||||
eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED,  ({entityName, entity}) => {
 | 
			
		||||
    if (!becca.loaded) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -97,7 +101,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED,  ({entityName, entity}) => {
 | 
			
		||||
    postProcessEntityUpdate(entityName, entity);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
eventService.subscribe([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED],  ({entityName, entityId}) => {
 | 
			
		||||
eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED],  ({entityName, entityId}) => {
 | 
			
		||||
    if (!becca.loaded) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -151,7 +155,7 @@ function branchUpdated(branch) {
 | 
			
		||||
 | 
			
		||||
    if (childNote) {
 | 
			
		||||
        childNote.flatTextCache = null;
 | 
			
		||||
        childNote.resortParents();
 | 
			
		||||
        childNote.sortParents();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -216,7 +220,7 @@ function noteReorderingUpdated(branchIdList) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
 | 
			
		||||
eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
 | 
			
		||||
    try {
 | 
			
		||||
        becca.decryptProtectedNotes();
 | 
			
		||||
    }
 | 
			
		||||
@@ -225,7 +229,7 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, load);
 | 
			
		||||
eventService.subscribeBeccaLoader(eventService.LEAVE_PROTECTED_SESSION, load);
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    load,
 | 
			
		||||
 
 | 
			
		||||
@@ -125,7 +125,7 @@ function getNoteTitleForPath(notePathArray) {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns notePath for noteId from cache. Note hoisting is respected.
 | 
			
		||||
 * Archived notes are also returned, but non-archived paths are preferred if available
 | 
			
		||||
 * Archived (and hidden) notes are also returned, but non-archived paths are preferred if available
 | 
			
		||||
 * - this means that archived paths is returned only if there's no non-archived path
 | 
			
		||||
 * - you can check whether returned path is archived using isArchived
 | 
			
		||||
 */
 | 
			
		||||
@@ -140,10 +140,6 @@ function getSomePathInner(note, path, respectHoisting) {
 | 
			
		||||
        path.push(note.noteId);
 | 
			
		||||
        path.reverse();
 | 
			
		||||
 | 
			
		||||
        if (path.includes("hidden")) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (respectHoisting && !path.includes(cls.getHoistedNoteId())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -36,21 +36,21 @@ class Attribute extends AbstractEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.attributeId = attributeId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.type = type;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.name = name;
 | 
			
		||||
        /** @param {int} */
 | 
			
		||||
        /** @type {int} */
 | 
			
		||||
        this.position = position;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.value = value;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isInheritable = !!isInheritable;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateModified = utcDateModified;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
@@ -145,6 +145,10 @@ class Attribute extends AbstractEntity {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isDeleted() {
 | 
			
		||||
        return !(this.attributeId in this.becca.attributes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        if (!this.value) {
 | 
			
		||||
            if (this.type === 'relation') {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,19 +35,19 @@ class Branch extends AbstractEntity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.branchId = branchId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.parentNoteId = parentNoteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.prefix = prefix;
 | 
			
		||||
        /** @param {int} */
 | 
			
		||||
        /** @type {int} */
 | 
			
		||||
        this.notePosition = notePosition;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isExpanded = !!isExpanded;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateModified = utcDateModified;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
@@ -77,7 +77,7 @@ class Branch extends AbstractEntity {
 | 
			
		||||
        this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note} */
 | 
			
		||||
    /** @returns {Note} */
 | 
			
		||||
    get childNote() {
 | 
			
		||||
        if (!(this.noteId in this.becca.notes)) {
 | 
			
		||||
            // entities can come out of order in sync, create skeleton which will be filled later
 | 
			
		||||
@@ -91,7 +91,7 @@ class Branch extends AbstractEntity {
 | 
			
		||||
        return this.childNote;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note} */
 | 
			
		||||
    /** @returns {Note} */
 | 
			
		||||
    get parentNote() {
 | 
			
		||||
        if (!(this.parentNoteId in this.becca.notes)) {
 | 
			
		||||
            // entities can come out of order in sync, create skeleton which will be filled later
 | 
			
		||||
@@ -101,6 +101,10 @@ class Branch extends AbstractEntity {
 | 
			
		||||
        return this.becca.notes[this.parentNoteId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isDeleted() {
 | 
			
		||||
        return !(this.branchId in this.becca.branches);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        if (this.notePosition === undefined || this.notePosition === null) {
 | 
			
		||||
            // TODO finding new position can be refactored into becca
 | 
			
		||||
 
 | 
			
		||||
@@ -45,33 +45,33 @@ class Note extends AbstractEntity {
 | 
			
		||||
    update([noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
 | 
			
		||||
        // ------ Database persisted attributes ------
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.title = title;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isProtected = !!isProtected;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.type = type;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.mime = mime;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.dateCreated = dateCreated || dateUtils.localNowDateTime();
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.dateModified = dateModified;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateCreated = utcDateCreated || dateUtils.utcNowDateTime();
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateModified = utcDateModified;
 | 
			
		||||
 | 
			
		||||
        // ------ Derived attributes ------
 | 
			
		||||
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        this.isDecrypted = !this.isProtected;
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isDecrypted = !this.noteId || !this.isProtected;
 | 
			
		||||
 | 
			
		||||
        this.decrypt();
 | 
			
		||||
 | 
			
		||||
        /** @param {string|null} */
 | 
			
		||||
        /** @type {string|null} */
 | 
			
		||||
        this.flatTextCache = null;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
@@ -116,26 +116,32 @@ class Note extends AbstractEntity {
 | 
			
		||||
            || protectedSessionService.isProtectedSessionAvailable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {Branch[]} */
 | 
			
		||||
    getParentBranches() {
 | 
			
		||||
        return this.parentBranches;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {Branch[]} */
 | 
			
		||||
    getBranches() {
 | 
			
		||||
        return this.parentBranches;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {Note[]} */
 | 
			
		||||
    getParentNotes() {
 | 
			
		||||
        return this.parents;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {Note[]} */
 | 
			
		||||
    getChildNotes() {
 | 
			
		||||
        return this.children;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {boolean} */
 | 
			
		||||
    hasChildren() {
 | 
			
		||||
        return this.children && this.children.length > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {Branch[]} */
 | 
			
		||||
    getChildBranches() {
 | 
			
		||||
        return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
 | 
			
		||||
    }
 | 
			
		||||
@@ -370,7 +376,7 @@ class Note extends AbstractEntity {
 | 
			
		||||
        return this.__attributeCache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    /** @returns {Attribute[]} */
 | 
			
		||||
    __getInheritableAttributes(path) {
 | 
			
		||||
        if (path.includes(this.noteId)) {
 | 
			
		||||
            return [];
 | 
			
		||||
@@ -611,7 +617,7 @@ class Note extends AbstractEntity {
 | 
			
		||||
 | 
			
		||||
    // will sort the parents so that non-search & non-archived are first and archived at the end
 | 
			
		||||
    // this is done so that non-search & non-archived paths are always explored as first when looking for note path
 | 
			
		||||
    resortParents() {
 | 
			
		||||
    sortParents() {
 | 
			
		||||
        this.parentBranches.sort((a, b) =>
 | 
			
		||||
            a.branchId.startsWith('virt-')
 | 
			
		||||
            || a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1);
 | 
			
		||||
@@ -721,28 +727,38 @@ class Note extends AbstractEntity {
 | 
			
		||||
        return !!this.targetRelations.find(rel => rel.name === 'template');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note[]} */
 | 
			
		||||
    /** @returns {Note[]} */
 | 
			
		||||
    getSubtreeNotesIncludingTemplated() {
 | 
			
		||||
        const arr = [[this]];
 | 
			
		||||
        const set = new Set();
 | 
			
		||||
 | 
			
		||||
        for (const childNote of this.children) {
 | 
			
		||||
            arr.push(childNote.getSubtreeNotesIncludingTemplated());
 | 
			
		||||
        function inner(note) {
 | 
			
		||||
            if (set.has(note)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        for (const targetRelation of this.targetRelations) {
 | 
			
		||||
            set.add(note);
 | 
			
		||||
 | 
			
		||||
            for (const childNote of note.children) {
 | 
			
		||||
                inner(childNote);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const targetRelation of note.targetRelations) {
 | 
			
		||||
                if (targetRelation.name === 'template') {
 | 
			
		||||
                const note = targetRelation.note;
 | 
			
		||||
                    const targetNote = targetRelation.note;
 | 
			
		||||
 | 
			
		||||
                if (note) {
 | 
			
		||||
                    arr.push(note.getSubtreeNotesIncludingTemplated());
 | 
			
		||||
                    if (targetNote) {
 | 
			
		||||
                        inner(targetNote);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return arr.flat();
 | 
			
		||||
        inner(this);
 | 
			
		||||
 | 
			
		||||
        return Array.from(set);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note[]} */
 | 
			
		||||
    /** @returns {Note[]} */
 | 
			
		||||
    getSubtreeNotes(includeArchived = true) {
 | 
			
		||||
        const noteSet = new Set();
 | 
			
		||||
 | 
			
		||||
@@ -763,9 +779,9 @@ class Note extends AbstractEntity {
 | 
			
		||||
        return Array.from(noteSet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {String[]} */
 | 
			
		||||
    getSubtreeNoteIds() {
 | 
			
		||||
        return this.getSubtreeNotes().map(note => note.noteId);
 | 
			
		||||
    /** @returns {String[]} */
 | 
			
		||||
    getSubtreeNoteIds(includeArchived = true) {
 | 
			
		||||
        return this.getSubtreeNotes(includeArchived).map(note => note.noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDescendantNoteIds() {
 | 
			
		||||
@@ -820,6 +836,7 @@ class Note extends AbstractEntity {
 | 
			
		||||
        return this.getAttributes().length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {Note[]} */
 | 
			
		||||
    getAncestors() {
 | 
			
		||||
        if (!this.ancestorCache) {
 | 
			
		||||
            const noteIds = new Set();
 | 
			
		||||
@@ -843,11 +860,22 @@ class Note extends AbstractEntity {
 | 
			
		||||
        return this.ancestorCache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @returns {boolean} */
 | 
			
		||||
    hasAncestor(ancestorNoteId) {
 | 
			
		||||
        for (const ancestorNote of this.getAncestors()) {
 | 
			
		||||
            if (ancestorNote.noteId === ancestorNoteId) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTargetRelations() {
 | 
			
		||||
        return this.targetRelations;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note[]} - returns only notes which are templated, does not include their subtrees
 | 
			
		||||
    /** @returns {Note[]} - returns only notes which are templated, does not include their subtrees
 | 
			
		||||
     *                     in effect returns notes which are influenced by note's non-inheritable attributes */
 | 
			
		||||
    getTemplatedNotes() {
 | 
			
		||||
        const arr = [this];
 | 
			
		||||
@@ -1084,6 +1112,10 @@ class Note extends AbstractEntity {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isDeleted() {
 | 
			
		||||
        return !(this.noteId in this.becca.notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,29 +19,29 @@ class NoteRevision extends AbstractEntity {
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteRevisionId = row.noteRevisionId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.type = row.type;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.mime = row.mime;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isProtected = !!row.isProtected;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.title = row.title;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.dateLastEdited = row.dateLastEdited;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.dateCreated = row.dateCreated;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateLastEdited = row.utcDateLastEdited;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateCreated = row.utcDateCreated;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateModified = row.utcDateModified;
 | 
			
		||||
        /** @param {number} */
 | 
			
		||||
        /** @type {number} */
 | 
			
		||||
        this.contentLength = row.contentLength;
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
 
 | 
			
		||||
@@ -163,7 +163,23 @@ const TPL = `
 | 
			
		||||
    
 | 
			
		||||
    <p>
 | 
			
		||||
        To apply font changes, click on 
 | 
			
		||||
        <button class="btn btn-micro" id="reload-frontend-button">reload frontend</button>
 | 
			
		||||
        <button class="btn btn-micro reload-frontend-button">reload frontend</button>
 | 
			
		||||
    </p>
 | 
			
		||||
    
 | 
			
		||||
    <h4>Content width</h4>
 | 
			
		||||
    
 | 
			
		||||
    <p>Trilium by default limits max content width to improve readability for maximized screens on wide screens.</p>
 | 
			
		||||
 | 
			
		||||
    <div class="form-group row">
 | 
			
		||||
        <div class="col-4">
 | 
			
		||||
            <label for="max-content-width">Max content width in pixels</label>
 | 
			
		||||
            <input type="number" min="200" step="10" class="form-control" id="max-content-width">
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <p>
 | 
			
		||||
        To content width changes, click on 
 | 
			
		||||
        <button class="btn btn-micro reload-frontend-button">reload frontend</button>
 | 
			
		||||
    </p>
 | 
			
		||||
</form>`;
 | 
			
		||||
 | 
			
		||||
@@ -192,7 +208,7 @@ export default class ApperanceOptions {
 | 
			
		||||
        this.$monospaceFontSize = $("#monospace-font-size");
 | 
			
		||||
        this.$monospaceFontFamily = $("#monospace-font-family");
 | 
			
		||||
 | 
			
		||||
        $("#reload-frontend-button").on("click", () => utils.reloadFrontendApp("font changes"));
 | 
			
		||||
        $(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
 | 
			
		||||
 | 
			
		||||
        this.$body = $("body");
 | 
			
		||||
 | 
			
		||||
@@ -238,6 +254,14 @@ export default class ApperanceOptions {
 | 
			
		||||
        for (const optionName of optionsToSave) {
 | 
			
		||||
            this['$' + optionName].on('change', () => server.put(`options/${optionName}/${this['$' + optionName].val()}`));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$maxContentWidth = $("#max-content-width");
 | 
			
		||||
 | 
			
		||||
        this.$maxContentWidth.on('change', async () => {
 | 
			
		||||
            const maxContentWidth = this.$maxContentWidth.val();
 | 
			
		||||
 | 
			
		||||
            await server.put('options/maxContentWidth/' + maxContentWidth);
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggleBodyClass(prefix, value) {
 | 
			
		||||
@@ -292,6 +316,8 @@ export default class ApperanceOptions {
 | 
			
		||||
 | 
			
		||||
        this.$monospaceFontSize.val(options.monospaceFontSize);
 | 
			
		||||
        this.fillFontFamilyOptions(this.$monospaceFontFamily, options.monospaceFontFamily);
 | 
			
		||||
 | 
			
		||||
        this.$maxContentWidth.val(options.maxContentWidth);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fillFontFamilyOptions($select, currentValue) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,19 +8,19 @@ class Attribute {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update(row) {
 | 
			
		||||
        /** @param {string} attributeId */
 | 
			
		||||
        /** @type {string} attributeId */
 | 
			
		||||
        this.attributeId = row.attributeId;
 | 
			
		||||
        /** @param {string} noteId */
 | 
			
		||||
        /** @type {string} noteId */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} type */
 | 
			
		||||
        /** @type {string} type */
 | 
			
		||||
        this.type = row.type;
 | 
			
		||||
        /** @param {string} name */
 | 
			
		||||
        /** @type {string} name */
 | 
			
		||||
        this.name = row.name;
 | 
			
		||||
        /** @param {string} value */
 | 
			
		||||
        /** @type {string} value */
 | 
			
		||||
        this.value = row.value;
 | 
			
		||||
        /** @param {int} position */
 | 
			
		||||
        /** @type {int} position */
 | 
			
		||||
        this.position = row.position;
 | 
			
		||||
        /** @param {boolean} isInheritable */
 | 
			
		||||
        /** @type {boolean} isInheritable */
 | 
			
		||||
        this.isInheritable = !!row.isInheritable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,19 +7,19 @@ class Branch {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update(row) {
 | 
			
		||||
        /** @param {string} primary key */
 | 
			
		||||
        /** @type {string} primary key */
 | 
			
		||||
        this.branchId = row.branchId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.parentNoteId = row.parentNoteId;
 | 
			
		||||
        /** @param {int} */
 | 
			
		||||
        /** @type {int} */
 | 
			
		||||
        this.notePosition = row.notePosition;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.prefix = row.prefix;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isExpanded = !!row.isExpanded;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.fromSearchNote = !!row.fromSearchNote;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,35 +3,35 @@
 | 
			
		||||
 */
 | 
			
		||||
class NoteComplement {
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * @param {string} - can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
 | 
			
		||||
         * @type {string} - can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
 | 
			
		||||
         */
 | 
			
		||||
        this.content = row.content;
 | 
			
		||||
 | 
			
		||||
        /** @param {int} */
 | 
			
		||||
        /** @type {int} */
 | 
			
		||||
        this.contentLength = row.contentLength;
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.dateCreated = row.dateCreated;
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.dateModified = row.dateModified;
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateCreated = row.utcDateCreated;
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateModified = row.utcDateModified;
 | 
			
		||||
 | 
			
		||||
        // "combined" date modified give larger out of note's and note_content's dateModified
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.combinedDateModified = row.combinedDateModified;
 | 
			
		||||
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.combinedUtcDateModified = row.combinedUtcDateModified;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -53,15 +53,15 @@ class NoteShort {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update(row) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.title = row.title;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        /** @type {boolean} */
 | 
			
		||||
        this.isProtected = !!row.isProtected;
 | 
			
		||||
        /** @param {string} one of 'text', 'code', 'file' or 'render' */
 | 
			
		||||
        /** @type {string} one of 'text', 'code', 'file' or 'render' */
 | 
			
		||||
        this.type = row.type;
 | 
			
		||||
        /** @param {string} content-type, e.g. "application/json" */
 | 
			
		||||
        /** @type {string} content-type, e.g. "application/json" */
 | 
			
		||||
        this.mime = row.mime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -644,12 +644,9 @@ class NoteShort {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clear note's attributes cache to force fresh reload for next attribute request.
 | 
			
		||||
     * Cache is note instance scoped.
 | 
			
		||||
     * @deprecated NOOP
 | 
			
		||||
     */
 | 
			
		||||
    invalidateAttributeCache() {
 | 
			
		||||
        this.__attributeCache = null;
 | 
			
		||||
    }
 | 
			
		||||
    invalidateAttributeCache() {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get relations which target this note
 | 
			
		||||
@@ -681,7 +678,7 @@ class NoteShort {
 | 
			
		||||
        return await this.froca.getNoteComplement(this.noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get toString() {
 | 
			
		||||
    toString() {
 | 
			
		||||
        return `Note(noteId=${this.noteId}, title=${this.title})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -79,12 +79,12 @@ class AppContext extends Component {
 | 
			
		||||
        this.triggerEvent('initialRenderComplete');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise} */
 | 
			
		||||
    /** @returns {Promise} */
 | 
			
		||||
    triggerEvent(name, data) {
 | 
			
		||||
        return this.handleEvent(name, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise} */
 | 
			
		||||
    /** @returns {Promise} */
 | 
			
		||||
    triggerCommand(name, data = {}) {
 | 
			
		||||
        for (const executor of this.executors) {
 | 
			
		||||
            const fun = executor[name + "Command"];
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,7 @@ class ContextMenu {
 | 
			
		||||
                    .append($link)
 | 
			
		||||
                    // important to use mousedown instead of click since the former does not change focus
 | 
			
		||||
                    // (especially important for focused text for spell check)
 | 
			
		||||
                    .on('mousedown', (e) => {
 | 
			
		||||
                    .on('mousedown', e => {
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
                        this.hide();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,19 +2,19 @@ import froca from "./froca.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import ws from "./ws.js";
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function getInboxNote() {
 | 
			
		||||
    const note = await server.get('special-notes/inbox/' + dayjs().format("YYYY-MM-DD"), "date-note");
 | 
			
		||||
 | 
			
		||||
    return await froca.getNote(note.noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function getTodayNote() {
 | 
			
		||||
    return await getDateNote(dayjs().format("YYYY-MM-DD"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function getDateNote(date) {
 | 
			
		||||
    const note = await server.get('special-notes/date/' + date, "date-note");
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,16 @@ async function getDateNote(date) {
 | 
			
		||||
    return await froca.getNote(note.noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function getWeekNote(date) {
 | 
			
		||||
    const note = await server.get('special-notes/week/' + date, "date-note");
 | 
			
		||||
 | 
			
		||||
    await ws.waitForMaxKnownEntityChangeId();
 | 
			
		||||
 | 
			
		||||
    return await froca.getNote(note.noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function getMonthNote(month) {
 | 
			
		||||
    const note = await server.get('special-notes/month/' + month, "date-note");
 | 
			
		||||
 | 
			
		||||
@@ -32,7 +41,7 @@ async function getMonthNote(month) {
 | 
			
		||||
    return await froca.getNote(note.noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function getYearNote(year) {
 | 
			
		||||
    const note = await server.get('special-notes/year/' + year, "date-note");
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +50,7 @@ async function getYearNote(year) {
 | 
			
		||||
    return await froca.getNote(note.noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function createSqlConsole() {
 | 
			
		||||
    const note = await server.post('special-notes/sql-console');
 | 
			
		||||
 | 
			
		||||
@@ -50,7 +59,7 @@ async function createSqlConsole() {
 | 
			
		||||
    return await froca.getNote(note.noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {NoteShort} */
 | 
			
		||||
/** @returns {NoteShort} */
 | 
			
		||||
async function createSearchNote(opts = {}) {
 | 
			
		||||
    const note = await server.post('special-notes/search-note', opts);
 | 
			
		||||
 | 
			
		||||
@@ -63,6 +72,7 @@ export default {
 | 
			
		||||
    getInboxNote,
 | 
			
		||||
    getTodayNote,
 | 
			
		||||
    getDateNote,
 | 
			
		||||
    getWeekNote,
 | 
			
		||||
    getMonthNote,
 | 
			
		||||
    getYearNote,
 | 
			
		||||
    createSqlConsole,
 | 
			
		||||
 
 | 
			
		||||
@@ -172,6 +172,12 @@ export default class Entrypoints extends Component {
 | 
			
		||||
        utils.reloadFrontendApp("Switching to desktop version");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async switchToMobileVersionCommand() {
 | 
			
		||||
        utils.setCookie('trilium-device', 'mobile');
 | 
			
		||||
 | 
			
		||||
        utils.reloadFrontendApp("Switching to mobile version");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async openInWindowCommand({notePath, hoistedNoteId}) {
 | 
			
		||||
        if (!hoistedNoteId) {
 | 
			
		||||
            hoistedNoteId = 'root';
 | 
			
		||||
 
 | 
			
		||||
@@ -207,7 +207,7 @@ class Froca {
 | 
			
		||||
        froca.notes[note.noteId].searchResultsLoaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {NoteShort[]} */
 | 
			
		||||
    /** @returns {NoteShort[]} */
 | 
			
		||||
    getNotesFromCache(noteIds, silentNotFoundError = false) {
 | 
			
		||||
        return noteIds.map(noteId => {
 | 
			
		||||
            if (!this.notes[noteId] && !silentNotFoundError) {
 | 
			
		||||
@@ -221,7 +221,7 @@ class Froca {
 | 
			
		||||
        }).filter(note => !!note);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise<NoteShort[]>} */
 | 
			
		||||
    /** @returns {Promise<NoteShort[]>} */
 | 
			
		||||
    async getNotes(noteIds, silentNotFoundError = false) {
 | 
			
		||||
        const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
 | 
			
		||||
 | 
			
		||||
@@ -238,14 +238,14 @@ class Froca {
 | 
			
		||||
        }).filter(note => !!note);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise<boolean>} */
 | 
			
		||||
    /** @returns {Promise<boolean>} */
 | 
			
		||||
    async noteExists(noteId) {
 | 
			
		||||
        const notes = await this.getNotes([noteId], true);
 | 
			
		||||
 | 
			
		||||
        return notes.length === 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise<NoteShort>} */
 | 
			
		||||
    /** @returns {Promise<NoteShort>} */
 | 
			
		||||
    async getNote(noteId, silentNotFoundError = false) {
 | 
			
		||||
        if (noteId === 'none') {
 | 
			
		||||
            console.trace(`No 'none' note.`);
 | 
			
		||||
@@ -273,7 +273,7 @@ class Froca {
 | 
			
		||||
            .filter(b => !!b);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Branch} */
 | 
			
		||||
    /** @returns {Branch} */
 | 
			
		||||
    getBranch(branchId, silentNotFoundError = false) {
 | 
			
		||||
        if (!(branchId in this.branches)) {
 | 
			
		||||
            if (!silentNotFoundError) {
 | 
			
		||||
 
 | 
			
		||||
@@ -396,6 +396,15 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
 | 
			
		||||
     */
 | 
			
		||||
    this.getDateNote = dateNotesService.getDateNote;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns date-note for the first date of the week of the given date. If it doesn't exist, it is automatically created.
 | 
			
		||||
     *
 | 
			
		||||
     * @method
 | 
			
		||||
     * @param {string} date - e.g. "2019-04-29"
 | 
			
		||||
     * @return {Promise<NoteShort>}
 | 
			
		||||
     */
 | 
			
		||||
     this.getWeekNote = dateNotesService.getWeekNote;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns month-note. If it doesn't exist, it is automatically created.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ export default class LoadResults {
 | 
			
		||||
        this.attributes.push({attributeId, sourceId});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    /** @returns {Attribute[]} */
 | 
			
		||||
    getAttributes(sourceId = 'none') {
 | 
			
		||||
        return this.attributes
 | 
			
		||||
            .filter(row => row.sourceId !== sourceId)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ async function autocompleteSource(term, cb, options = {}) {
 | 
			
		||||
                action: 'create-note',
 | 
			
		||||
                noteTitle: term,
 | 
			
		||||
                parentNoteId: activeNoteId || 'root',
 | 
			
		||||
                highlightedNotePathTitle: `Create and link child note "${term}"`
 | 
			
		||||
                highlightedNotePathTitle: `Create and link child note "${utils.escapeHtml(term)}"`
 | 
			
		||||
            }
 | 
			
		||||
        ].concat(results);
 | 
			
		||||
    }
 | 
			
		||||
@@ -53,7 +53,7 @@ async function autocompleteSource(term, cb, options = {}) {
 | 
			
		||||
            {
 | 
			
		||||
                action: 'external-link',
 | 
			
		||||
                externalLink: term,
 | 
			
		||||
                highlightedNotePathTitle: `Insert external link to "${term}"`
 | 
			
		||||
                highlightedNotePathTitle: `Insert external link to "${utils.escapeHtml(term)}"`
 | 
			
		||||
            }
 | 
			
		||||
        ].concat(results);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -169,7 +169,7 @@ function trim(text, doTrim) {
 | 
			
		||||
        return text;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        return text.substr(0, Math.min(text.length, 1000));
 | 
			
		||||
        return text.substr(0, Math.min(text.length, 2000));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,11 @@ class NoteContext extends Component {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setEmpty() {
 | 
			
		||||
        this.notePath = null;
 | 
			
		||||
        this.noteId = null;
 | 
			
		||||
        this.parentNoteId = null;
 | 
			
		||||
        this.hoistedNoteId = 'root';
 | 
			
		||||
 | 
			
		||||
        this.triggerEvent('noteSwitched', {
 | 
			
		||||
            noteContext: this,
 | 
			
		||||
            notePath: this.notePath
 | 
			
		||||
@@ -71,8 +76,14 @@ class NoteContext extends Component {
 | 
			
		||||
 | 
			
		||||
    getMainContext() {
 | 
			
		||||
        if (this.mainNtxId) {
 | 
			
		||||
            try {
 | 
			
		||||
                return appContext.tabManager.getNoteContextById(this.mainNtxId);
 | 
			
		||||
            }
 | 
			
		||||
            catch (e) {
 | 
			
		||||
                this.mainNtxId = null;
 | 
			
		||||
                return this;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
@@ -123,7 +134,7 @@ class NoteContext extends Component {
 | 
			
		||||
        return this.notePath ? this.notePath.split('/') : [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {NoteComplement} */
 | 
			
		||||
    /** @returns {NoteComplement} */
 | 
			
		||||
    async getNoteComplement() {
 | 
			
		||||
        if (!this.noteId) {
 | 
			
		||||
            return null;
 | 
			
		||||
@@ -155,12 +166,12 @@ class NoteContext extends Component {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setHoistedNoteId(noteIdToHoist) {
 | 
			
		||||
        if (this.notePathArray && !this.notePathArray.includes(noteIdToHoist)) {
 | 
			
		||||
        this.hoistedNoteId = noteIdToHoist;
 | 
			
		||||
 | 
			
		||||
        if (!this.notePathArray?.includes(noteIdToHoist)) {
 | 
			
		||||
            await this.setNote(noteIdToHoist);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.hoistedNoteId = noteIdToHoist;
 | 
			
		||||
 | 
			
		||||
        await this.triggerEvent('hoistedNoteChanged', {
 | 
			
		||||
            noteId: noteIdToHoist,
 | 
			
		||||
            ntxId: this.ntxId
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@ class NoteListRenderer {
 | 
			
		||||
        this.parentNote = parentNote;
 | 
			
		||||
        const includedNoteIds = this.getIncludedNoteIds();
 | 
			
		||||
 | 
			
		||||
        this.noteIds = noteIds.filter(noteId => !includedNoteIds.has(noteId));
 | 
			
		||||
        this.noteIds = noteIds.filter(noteId => !includedNoteIds.has(noteId) && noteId !== 'hidden');
 | 
			
		||||
 | 
			
		||||
        if (this.noteIds.length === 0) {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -180,7 +180,7 @@ class NoteListRenderer {
 | 
			
		||||
        this.showNotePath = showNotePath;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Set<string>} list of noteIds included (images, included notes) into a parent note and which
 | 
			
		||||
    /** @returns {Set<string>} list of noteIds included (images, included notes) into a parent note and which
 | 
			
		||||
     *                        don't have to be shown in the note list. */
 | 
			
		||||
    getIncludedNoteIds() {
 | 
			
		||||
        const includedLinks = this.parentNote
 | 
			
		||||
 
 | 
			
		||||
@@ -139,7 +139,7 @@ export default class TabManager extends Component {
 | 
			
		||||
        this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {NoteContext[]} */
 | 
			
		||||
    /** @returns {NoteContext[]} */
 | 
			
		||||
    getNoteContexts() {
 | 
			
		||||
        return this.noteContexts;
 | 
			
		||||
    }
 | 
			
		||||
@@ -175,20 +175,20 @@ export default class TabManager extends Component {
 | 
			
		||||
        return activeContext ? activeContext.notePath : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {NoteShort} */
 | 
			
		||||
    /** @returns {NoteShort} */
 | 
			
		||||
    getActiveContextNote() {
 | 
			
		||||
        const activeContext = this.getActiveContext();
 | 
			
		||||
        return activeContext ? activeContext.note : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {string|null} */
 | 
			
		||||
    /** @returns {string|null} */
 | 
			
		||||
    getActiveContextNoteId() {
 | 
			
		||||
        const activeNote = this.getActiveContextNote();
 | 
			
		||||
 | 
			
		||||
        return activeNote ? activeNote.noteId : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {string|null} */
 | 
			
		||||
    /** @returns {string|null} */
 | 
			
		||||
    getActiveContextNoteType() {
 | 
			
		||||
        const activeNote = this.getActiveContextNote();
 | 
			
		||||
 | 
			
		||||
@@ -299,6 +299,17 @@ export default class TabManager extends Component {
 | 
			
		||||
    async removeNoteContext(ntxId) {
 | 
			
		||||
        const noteContextToRemove = this.getNoteContextById(ntxId);
 | 
			
		||||
 | 
			
		||||
        if (noteContextToRemove.isMainContext()) {
 | 
			
		||||
            // forbid removing last main note context
 | 
			
		||||
            // this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions
 | 
			
		||||
            const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext());
 | 
			
		||||
 | 
			
		||||
            if (mainNoteContexts.length === 1) {
 | 
			
		||||
                mainNoteContexts[0].setEmpty();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // close dangling autocompletes after closing the tab
 | 
			
		||||
        $(".aa-input").autocomplete("close");
 | 
			
		||||
 | 
			
		||||
@@ -353,7 +364,7 @@ export default class TabManager extends Component {
 | 
			
		||||
        const order = {};
 | 
			
		||||
        let i = 0;
 | 
			
		||||
 | 
			
		||||
        for (const ntxId in ntxIdsInOrder) {
 | 
			
		||||
        for (const ntxId of ntxIdsInOrder) {
 | 
			
		||||
            order[ntxId] = i++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -106,6 +106,10 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
 | 
			
		||||
 | 
			
		||||
        const someNotePathSegments = getSomeNotePathSegments(note, hoistedNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!someNotePathSegments) {
 | 
			
		||||
            throw new Error(`Did not find any path segments for ${note.toString()}, hoisted note ${hoistedNoteId}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if there isn't actually any note path with hoisted note then return the original resolved note path
 | 
			
		||||
        return someNotePathSegments.includes(hoistedNoteId) ? someNotePathSegments : effectivePathSegments;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -68,12 +68,13 @@ async function executeFrontendUpdate(entityChanges) {
 | 
			
		||||
        frontendUpdateDataQueue.push(...entityChanges);
 | 
			
		||||
 | 
			
		||||
        // we set lastAcceptedEntityChangeId even before frontend update processing and send ping so that backend can start sending more updates
 | 
			
		||||
        lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, entityChanges[entityChanges.length - 1].id);
 | 
			
		||||
 | 
			
		||||
        const lastSyncEntityChange = entityChanges.slice().reverse().find(ec => ec.isSynced);
 | 
			
		||||
        for (const entityChange of entityChanges) {
 | 
			
		||||
            lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, entityChange.id);
 | 
			
		||||
 | 
			
		||||
        if (lastSyncEntityChange) {
 | 
			
		||||
            lastAcceptedEntityChangeSyncId = Math.max(lastAcceptedEntityChangeSyncId, lastSyncEntityChange.id);
 | 
			
		||||
            if (entityChange.isSynced) {
 | 
			
		||||
                lastAcceptedEntityChangeSyncId = Math.max(lastAcceptedEntityChangeSyncId, entityChange.id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sendPing();
 | 
			
		||||
 
 | 
			
		||||
@@ -334,7 +334,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$relatedNotesList = this.$relatedNotesContainer.find('.related-notes-list');
 | 
			
		||||
        this.$relatedNotesMoreNotes = this.$relatedNotesContainer.find('.related-notes-more-notes');
 | 
			
		||||
 | 
			
		||||
        $(window).on('mouseup', e => {
 | 
			
		||||
        $(window).on('mousedown', e => {
 | 
			
		||||
            if (!$(e.target).closest(this.$widget[0]).length
 | 
			
		||||
                && !$(e.target).closest(".algolia-autocomplete").length
 | 
			
		||||
                && !$(e.target).closest("#context-menu-container").length) {
 | 
			
		||||
@@ -626,7 +626,9 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
 | 
			
		||||
                props.push('precision=' + this.$inputNumberPrecision.val());
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.attrType === 'relation-definition' && this.$inputInverseRelation.val().trim().length > 0) {
 | 
			
		||||
            props.push("inverse=" + this.$inputInverseRelation.val());
 | 
			
		||||
            const inverseRelationName = this.$inputInverseRelation.val();
 | 
			
		||||
 | 
			
		||||
            props.push("inverse=" + utils.filterAttributeName(inverseRelationName));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$rowNumberPrecision.toggle(
 | 
			
		||||
 
 | 
			
		||||
@@ -193,7 +193,8 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        this.$editor.on('keydown', async e => {
 | 
			
		||||
            if (e.which === 13) {
 | 
			
		||||
                await this.save();
 | 
			
		||||
                // allow autocomplete to fill the result textarea
 | 
			
		||||
                setTimeout(() => this.save(), 100);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.attributeDetailWidget.hide();
 | 
			
		||||
 
 | 
			
		||||
@@ -26,9 +26,17 @@ export default class ButtonWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
 | 
			
		||||
        if (this.settings.onClick) {
 | 
			
		||||
            this.$widget.on("click", () => this.settings.onClick(this));
 | 
			
		||||
            this.$widget.on("click", e => {
 | 
			
		||||
                this.$widget.tooltip("hide");
 | 
			
		||||
 | 
			
		||||
                this.settings.onClick(this, e);
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            this.$widget.on("click", () => this.triggerCommand(this.settings.command));
 | 
			
		||||
            this.$widget.on("click", () => {
 | 
			
		||||
                this.$widget.tooltip("hide");
 | 
			
		||||
 | 
			
		||||
                this.triggerCommand(this.settings.command);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$widget.attr("data-placement", this.settings.titlePlacement);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,12 @@ export default class ClosePaneButton extends ButtonWidget {
 | 
			
		||||
        this.icon("bx-x")
 | 
			
		||||
            .title("Close this pane")
 | 
			
		||||
            .titlePlacement("bottom")
 | 
			
		||||
            .onClick(widget => widget.triggerCommand("closeThisNoteSplit", { ntxId: widget.getNtxId() }));
 | 
			
		||||
            .onClick((widget, e) => {
 | 
			
		||||
                // to avoid split pane container detecting click within the pane which would try to activate this
 | 
			
		||||
                // pane (which is being removed)
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
                widget.triggerCommand("closeThisNoteSplit", { ntxId: widget.getNtxId() });
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,11 @@ const TPL = `
 | 
			
		||||
            <kbd data-command="showBackendLog"></kbd>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
 | 
			
		||||
            <span class="bx bx-empty"></span>
 | 
			
		||||
            Switch to mobile version
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a class="dropdown-item" data-trigger-command="reloadFrontendApp" 
 | 
			
		||||
            title="Reload can help with some visual glitches without restarting the whole app.">
 | 
			
		||||
            <span class="bx bx-empty"></span>
 | 
			
		||||
@@ -103,9 +108,11 @@ export default class GlobalMenuWidget extends BasicWidget {
 | 
			
		||||
        this.$widget.find(".show-about-dialog-button").on('click',
 | 
			
		||||
            () => import("../../dialogs/about.js").then(d => d.showDialog()));
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".logout-button").toggle(!utils.isElectron());
 | 
			
		||||
        const isElectron = utils.isElectron();
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".open-dev-tools-button").toggle(utils.isElectron());
 | 
			
		||||
        this.$widget.find(".logout-button").toggle(!isElectron);
 | 
			
		||||
        this.$widget.find(".open-dev-tools-button").toggle(isElectron);
 | 
			
		||||
        this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron);
 | 
			
		||||
 | 
			
		||||
        this.$widget.on('click', '.dropdown-item',
 | 
			
		||||
            () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
 | 
			
		||||
 
 | 
			
		||||
@@ -25,12 +25,12 @@ const TPL = `
 | 
			
		||||
 | 
			
		||||
    <div class="dropdown-menu dropdown-menu-right">
 | 
			
		||||
        <a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> Re-render note</a>
 | 
			
		||||
        <a data-trigger-command="findInText" class="dropdown-item">Search in note <kbd data-command="findInText"></a>
 | 
			
		||||
        <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a>
 | 
			
		||||
        <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
 | 
			
		||||
        <a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
 | 
			
		||||
        <a class="dropdown-item import-files-button">Import files</a>
 | 
			
		||||
        <a class="dropdown-item export-note-button">Export note</a>
 | 
			
		||||
        <a data-trigger-command="printActiveNote" class="dropdown-item print-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
 | 
			
		||||
        <a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +42,8 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
 | 
			
		||||
        this.$findInTextButton = this.$widget.find('.find-in-text-button');
 | 
			
		||||
        this.$printActiveNoteButton = this.$widget.find('.print-active-note-button');
 | 
			
		||||
        this.$showSourceButton = this.$widget.find('.show-source-button');
 | 
			
		||||
        this.$renderNoteButton = this.$widget.find('.render-note-button');
 | 
			
		||||
 | 
			
		||||
@@ -57,14 +59,18 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$importNoteButton = this.$widget.find('.import-files-button');
 | 
			
		||||
        this.$importNoteButton.on("click", () => import('../../dialogs/import.js').then(d => d.showDialog(this.noteId)));
 | 
			
		||||
 | 
			
		||||
        this.$widget.on('click', '.dropdown-item', () => this.$widget.find('.dropdown-menu').dropdown('toggle'));
 | 
			
		||||
        this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
 | 
			
		||||
 | 
			
		||||
        this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    refreshWithNote(note) {
 | 
			
		||||
        this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book', 'search'].includes(note.type));
 | 
			
		||||
 | 
			
		||||
        this.toggleDisabled(this.$showSourceButton, ['text', 'relation-map', 'search', 'code'].includes(note.type));
 | 
			
		||||
 | 
			
		||||
        this.toggleDisabled(this.$printActiveNoteButton, ['text', 'code'].includes(note.type));
 | 
			
		||||
 | 
			
		||||
        this.$renderNoteButton.toggle(note.type === 'render');
 | 
			
		||||
 | 
			
		||||
        this.$openNoteExternallyButton.toggle(utils.isElectron());
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ export default class Component {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise} */
 | 
			
		||||
    /** @returns {Promise} */
 | 
			
		||||
    handleEvent(name, data) {
 | 
			
		||||
        return Promise.all([
 | 
			
		||||
            this.initialized.then(() => this.callMethod(this[name + 'Event'], data)),
 | 
			
		||||
@@ -48,12 +48,12 @@ export default class Component {
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise} */
 | 
			
		||||
    /** @returns {Promise} */
 | 
			
		||||
    triggerEvent(name, data) {
 | 
			
		||||
        return this.parent.triggerEvent(name, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise} */
 | 
			
		||||
    /** @returns {Promise} */
 | 
			
		||||
    handleEventInChildren(name, data) {
 | 
			
		||||
        const promises = [];
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +64,7 @@ export default class Component {
 | 
			
		||||
        return Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Promise} */
 | 
			
		||||
    /** @returns {Promise} */
 | 
			
		||||
    triggerCommand(name, data = {}) {
 | 
			
		||||
        const fun = this[name + 'Command'];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
			
		||||
import keyboardActionsService from "../../services/keyboard_actions.js";
 | 
			
		||||
import attributeService from "../../services/attributes.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="ribbon-container">
 | 
			
		||||
@@ -166,7 +167,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggleRibbonTab($ribbonTitle) {
 | 
			
		||||
    toggleRibbonTab($ribbonTitle, refreshActiveTab = true) {
 | 
			
		||||
        const activate = !$ribbonTitle.hasClass("active");
 | 
			
		||||
 | 
			
		||||
        this.$tabContainer.find('.ribbon-tab-title').removeClass("active");
 | 
			
		||||
@@ -182,7 +183,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            const activeChild = this.getActiveRibbonWidget();
 | 
			
		||||
 | 
			
		||||
            if (activeChild) {
 | 
			
		||||
            if (activeChild && refreshActiveTab) {
 | 
			
		||||
                activeChild.handleEvent('noteSwitched', {noteContext: this.noteContext, notePath: this.notePath});
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -248,7 +249,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($ribbonTabToActivate) {
 | 
			
		||||
            $ribbonTabToActivate.trigger('click');
 | 
			
		||||
            this.toggleRibbonTab($ribbonTabToActivate, false);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            this.$bodyContainer.find('.ribbon-body').removeClass("active");
 | 
			
		||||
@@ -261,10 +262,6 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        return $ribbonComponent.hasClass("active");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    refreshRibbonContainerCommand() {
 | 
			
		||||
        this.refreshWithNote(this.note, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ensureOwnedAttributesAreOpen(ntxId) {
 | 
			
		||||
        if (this.isNoteContext(ntxId) && !this.isRibbonTabActive('ownedAttributes')) {
 | 
			
		||||
            this.toggleRibbonTabWithName('ownedAttributes', ntxId);
 | 
			
		||||
@@ -332,6 +329,9 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
        else if (loadResults.getAttributes(this.componentId).find(attr => attributeService.isAffecting(attr, this.note))) {
 | 
			
		||||
            this.refreshWithNote(this.note, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getActiveRibbonWidget() {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ export default class SplitNoteContainer extends FlexContainer {
 | 
			
		||||
        const $renderedWidget = widget.render();
 | 
			
		||||
 | 
			
		||||
        $renderedWidget.attr("data-ntx-id", noteContext.ntxId);
 | 
			
		||||
        $renderedWidget.css("flex-basis", "0"); // so that each split has same width
 | 
			
		||||
        $renderedWidget.addClass("note-split");
 | 
			
		||||
 | 
			
		||||
        $renderedWidget.on('click', () => appContext.tabManager.activateNoteContext(noteContext.ntxId));
 | 
			
		||||
 | 
			
		||||
@@ -37,6 +37,12 @@ export default class SplitNoteContainer extends FlexContainer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async openNewNoteSplitEvent({ntxId, notePath}) {
 | 
			
		||||
        if (!ntxId) {
 | 
			
		||||
            logError("empty ntxId!");
 | 
			
		||||
 | 
			
		||||
            ntxId = appContext.tabManager.getActiveMainContext().ntxId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const noteContext = await appContext.tabManager.openEmptyTab(null, 'root', appContext.tabManager.getActiveMainContext().ntxId);
 | 
			
		||||
 | 
			
		||||
        // remove the original position of newly created note context
 | 
			
		||||
@@ -46,7 +52,7 @@ export default class SplitNoteContainer extends FlexContainer {
 | 
			
		||||
        // insert the note context after the originating note context
 | 
			
		||||
        ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId);
 | 
			
		||||
 | 
			
		||||
        this.triggerCommand("noteContextReorder", ntxIds);
 | 
			
		||||
        this.triggerCommand("noteContextReorder", {ntxIdsInOrder: ntxIds});
 | 
			
		||||
 | 
			
		||||
        // move the note context rendered widget after the originating widget
 | 
			
		||||
        this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ const TPL = `<div class="note-map-widget" style="position: relative;">
 | 
			
		||||
            top: 10px; 
 | 
			
		||||
            right: 10px; 
 | 
			
		||||
            background-color: var(--accented-background-color);
 | 
			
		||||
            z-index: 1000;
 | 
			
		||||
            z-index: 10; /* should be below dropdown (note actions) */
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .map-type-switcher .bx {
 | 
			
		||||
@@ -332,9 +332,9 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        if (this.widgetMode === 'ribbon') {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                const node = this.nodes.find(node => node.id === this.noteId);
 | 
			
		||||
                const subGraphNoteIds = this.getSubGraphConnectedToCurrentNote(data);
 | 
			
		||||
 | 
			
		||||
                this.graph.centerAt(node.x, node.y, 500);
 | 
			
		||||
                this.graph.zoomToFit(400, 50, node => subGraphNoteIds.has(node.id));
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        }
 | 
			
		||||
        else if (this.widgetMode === 'type') {
 | 
			
		||||
@@ -344,6 +344,39 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSubGraphConnectedToCurrentNote(data) {
 | 
			
		||||
        function getGroupedLinksBySource(links) {
 | 
			
		||||
            const map = {};
 | 
			
		||||
 | 
			
		||||
            for (const link of links) {
 | 
			
		||||
                const key = link.source.id;
 | 
			
		||||
                map[key] = map[key] || [];
 | 
			
		||||
                map[key].push(link);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return map;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const linksBySource = getGroupedLinksBySource(data.links);
 | 
			
		||||
 | 
			
		||||
        const subGraphNoteIds = new Set();
 | 
			
		||||
 | 
			
		||||
        function traverseGraph(noteId) {
 | 
			
		||||
            if (subGraphNoteIds.has(noteId)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            subGraphNoteIds.add(noteId);
 | 
			
		||||
 | 
			
		||||
            for (const link of linksBySource[noteId] || []) {
 | 
			
		||||
                traverseGraph(link.target.id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        traverseGraph(this.noteId);
 | 
			
		||||
        return subGraphNoteIds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cleanup() {
 | 
			
		||||
        this.$container.html('');
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -681,12 +681,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
			
		||||
        return extraClasses.join(" ");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode[]} */
 | 
			
		||||
    /** @returns {FancytreeNode[]} */
 | 
			
		||||
    getSelectedNodes(stopOnParents = false) {
 | 
			
		||||
        return this.tree.getSelectedNodes(stopOnParents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode[]} */
 | 
			
		||||
    /** @returns {FancytreeNode[]} */
 | 
			
		||||
    getSelectedOrActiveNodes(node = null) {
 | 
			
		||||
        const nodes = this.getSelectedNodes(true);
 | 
			
		||||
 | 
			
		||||
@@ -771,7 +771,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode} */
 | 
			
		||||
    /** @returns {FancytreeNode} */
 | 
			
		||||
    async getNodeFromPath(notePath, expand = false, logErrors = true) {
 | 
			
		||||
        utils.assertArguments(notePath);
 | 
			
		||||
        /** @let {FancytreeNode} */
 | 
			
		||||
@@ -838,24 +838,24 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
			
		||||
        return parentNode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode} */
 | 
			
		||||
    /** @returns {FancytreeNode} */
 | 
			
		||||
    findChildNode(parentNode, childNoteId) {
 | 
			
		||||
        return parentNode.getChildren().find(childNode => childNode.data.noteId === childNoteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode} */
 | 
			
		||||
    /** @returns {FancytreeNode} */
 | 
			
		||||
    async expandToNote(notePath, logErrors = true) {
 | 
			
		||||
        return this.getNodeFromPath(notePath, true, logErrors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode[]} */
 | 
			
		||||
    /** @returns {FancytreeNode[]} */
 | 
			
		||||
    getNodesByBranch(branch) {
 | 
			
		||||
        utils.assertArguments(branch);
 | 
			
		||||
 | 
			
		||||
        return this.getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branch.branchId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {FancytreeNode[]} */
 | 
			
		||||
    /** @returns {FancytreeNode[]} */
 | 
			
		||||
    getNodesByNoteId(noteId) {
 | 
			
		||||
        utils.assertArguments(noteId);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
			
		||||
const NOTE_TYPES = [
 | 
			
		||||
    { type: "file", title: "File", selectable: false },
 | 
			
		||||
    { type: "image", title: "Image", selectable: false },
 | 
			
		||||
    { type: "search", title: "Saved search", selectable: false },
 | 
			
		||||
    { type: "search", title: "Saved Search", selectable: false },
 | 
			
		||||
    { type: "note-map", mime: '', title: "Note Map", selectable: false },
 | 
			
		||||
 | 
			
		||||
    { type: "text", mime: "text/html", title: "Text", selectable: true },
 | 
			
		||||
    { type: "relation-map", mime: "application/json", title: "Relation Map", selectable: true },
 | 
			
		||||
@@ -15,6 +16,8 @@ const NOTE_TYPES = [
 | 
			
		||||
    { type: "code", mime: 'text/plain', title: "Code", selectable: true }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter(nt => !nt.selectable).map(nt => nt.type);
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="dropdown note-type-widget">
 | 
			
		||||
    <style>
 | 
			
		||||
@@ -48,7 +51,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note) {
 | 
			
		||||
        this.$noteTypeButton.prop("disabled",
 | 
			
		||||
            () => ["file", "image", "search"].includes(note.type));
 | 
			
		||||
            () => NOT_SELECTABLE_NOTE_TYPES.includes(note.type));
 | 
			
		||||
 | 
			
		||||
        this.$noteTypeDesc.text(await this.findTypeTitle(note.type, note.mime));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ const TPL = `
 | 
			
		||||
    <style>
 | 
			
		||||
        .note-update-status-widget {
 | 
			
		||||
            margin: 10px;
 | 
			
		||||
            contain: none;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
    
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,11 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
 | 
			
		||||
        /** @type {AttributeDetailWidget} */
 | 
			
		||||
        this.attributeDetailWidget = new AttributeDetailWidget()
 | 
			
		||||
            .contentSized()
 | 
			
		||||
            .setParent(this);
 | 
			
		||||
 | 
			
		||||
        this.child(this.attributeDetailWidget);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -65,7 +69,9 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        for (const attribute of inheritedAttributes) {
 | 
			
		||||
            const $attr = (await attributeRenderer.renderAttribute(attribute, false))
 | 
			
		||||
                .on('click', e => this.attributeDetailWidget.showAttributeDetail({
 | 
			
		||||
                .on('click', e => {
 | 
			
		||||
                    setTimeout(() =>
 | 
			
		||||
                        this.attributeDetailWidget.showAttributeDetail({
 | 
			
		||||
                            attribute: {
 | 
			
		||||
                                noteId: attribute.noteId,
 | 
			
		||||
                                type: attribute.type,
 | 
			
		||||
@@ -76,7 +82,8 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
 | 
			
		||||
                            isOwned: false,
 | 
			
		||||
                            x: e.pageX,
 | 
			
		||||
                            y: e.pageY
 | 
			
		||||
                }));
 | 
			
		||||
                        }), 100);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            this.$container
 | 
			
		||||
                .append($attr)
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$notePathList.empty();
 | 
			
		||||
 | 
			
		||||
        if (this.noteId === 'root') {
 | 
			
		||||
            await this.addPath('root');
 | 
			
		||||
            await this.getRenderedPath('root');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -83,22 +83,18 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
            this.$notePathIntro.text("This note is not yet placed into the note tree.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const printedNotePaths = new Set();
 | 
			
		||||
        const renderedPaths = [];
 | 
			
		||||
 | 
			
		||||
        for (const notePathRecord of sortedNotePaths) {
 | 
			
		||||
            const notePath = notePathRecord.notePath.join('/');
 | 
			
		||||
 | 
			
		||||
            if (printedNotePaths.has(notePath)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            renderedPaths.push(await this.getRenderedPath(notePath, notePathRecord));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            printedNotePaths.add(notePath);
 | 
			
		||||
 | 
			
		||||
            await this.addPath(notePath, notePathRecord);
 | 
			
		||||
        }
 | 
			
		||||
        this.$notePathList.empty().append(...renderedPaths);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async addPath(notePath, notePathRecord) {
 | 
			
		||||
    async getRenderedPath(notePath, notePathRecord) {
 | 
			
		||||
        const title = await treeService.getNotePathTitle(notePath);
 | 
			
		||||
 | 
			
		||||
        const $noteLink = await linkService.createNoteLink(notePath, {title});
 | 
			
		||||
@@ -136,7 +132,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
            $noteLink.append(` ${icons.join(' ')}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$notePathList.append($("<li>").append($noteLink));
 | 
			
		||||
        return $("<li>").append($noteLink);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({loadResults}) {
 | 
			
		||||
 
 | 
			
		||||
@@ -297,9 +297,6 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
 | 
			
		||||
    entitiesReloadedEvent({loadResults}) {
 | 
			
		||||
        if (loadResults.getAttributes(this.componentId).find(attr => attributeService.isAffecting(attr, this.note))) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
 | 
			
		||||
            this.getTitle(this.note);
 | 
			
		||||
            this.triggerCommand('refreshRibbonContainer');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -226,8 +226,8 @@ div.ui-tooltip {
 | 
			
		||||
    background: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.CodeMirror * {
 | 
			
		||||
    font-family: var(--monospace-font-family) !important;
 | 
			
		||||
body .CodeMirror {
 | 
			
		||||
    font-size: var(--monospace-font-size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.CodeMirror-gutters {
 | 
			
		||||
@@ -950,3 +950,9 @@ input {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-split {
 | 
			
		||||
    flex-basis: 0; /* so that each split has same width */
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -125,11 +125,13 @@ function updateNoteAttributes(req) {
 | 
			
		||||
    for (const incAttr of incomingAttributes) {
 | 
			
		||||
        position += 10;
 | 
			
		||||
 | 
			
		||||
        const value = incAttr.value || "";
 | 
			
		||||
 | 
			
		||||
        const perfectMatchAttr = existingAttrs.find(attr =>
 | 
			
		||||
            attr.type === incAttr.type &&
 | 
			
		||||
            attr.name === incAttr.name &&
 | 
			
		||||
            attr.isInheritable === incAttr.isInheritable &&
 | 
			
		||||
            attr.value === incAttr.value);
 | 
			
		||||
            attr.value === value);
 | 
			
		||||
 | 
			
		||||
        if (perfectMatchAttr) {
 | 
			
		||||
            existingAttrs = existingAttrs.filter(attr => attr.attributeId !== perfectMatchAttr.attributeId);
 | 
			
		||||
@@ -145,7 +147,7 @@ function updateNoteAttributes(req) {
 | 
			
		||||
        if (incAttr.type === 'relation') {
 | 
			
		||||
            const targetNote = becca.getNote(incAttr.value);
 | 
			
		||||
 | 
			
		||||
            if (!targetNote || targetNote.isDeleted) {
 | 
			
		||||
            if (!targetNote) {
 | 
			
		||||
                log.error(`Target note of relation ${JSON.stringify(incAttr)} does not exist or is deleted`);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ const htmlSanitizer = require('../../services/html_sanitizer');
 | 
			
		||||
const {formatAttrForSearch} = require("../../services/attribute_formatter");
 | 
			
		||||
 | 
			
		||||
function findClippingNote(todayNote, pageUrl) {
 | 
			
		||||
    const notes = todayNote.searchNoteInSubtree(
 | 
			
		||||
    const notes = todayNote.searchNotesInSubtree(
 | 
			
		||||
        formatAttrForSearch({
 | 
			
		||||
            type: 'label',
 | 
			
		||||
            name: "pageUrl",
 | 
			
		||||
@@ -59,6 +59,7 @@ function addClipping(req) {
 | 
			
		||||
 | 
			
		||||
        clippingNote.setLabel('clipType', 'clippings');
 | 
			
		||||
        clippingNote.setLabel('pageUrl', pageUrl);
 | 
			
		||||
        clippingNote.setLabel('iconClass', 'bx bx-globe');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rewrittenContent = processContent(images, clippingNote, content);
 | 
			
		||||
@@ -92,6 +93,7 @@ function createNote(req) {
 | 
			
		||||
 | 
			
		||||
    if (pageUrl) {
 | 
			
		||||
        note.setLabel('pageUrl', pageUrl);
 | 
			
		||||
        note.setLabel('iconClass', 'bx bx-globe');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rewrittenContent = processContent(images, note, content);
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ function getFontCss(req, res) {
 | 
			
		||||
    style += `--main-font-size: ${optionsMap.mainFontSize}%;`;
 | 
			
		||||
    style += `--tree-font-size: ${optionsMap.treeFontSize}%;`;
 | 
			
		||||
    style += `--detail-font-size: ${optionsMap.detailFontSize}%;`;
 | 
			
		||||
    style += `--monospace-font-size: ${optionsMap.monospaceFontSize};`;
 | 
			
		||||
    style += `--monospace-font-size: ${optionsMap.monospaceFontSize}%;`;
 | 
			
		||||
 | 
			
		||||
    style += '}';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,23 +24,54 @@ function buildDescendantCountMap() {
 | 
			
		||||
    return noteIdToCountMap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNeighbors(note, depth) {
 | 
			
		||||
    if (depth === 0) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const retNoteIds = [];
 | 
			
		||||
 | 
			
		||||
    for (const relation of note.getRelations()) {
 | 
			
		||||
        if (['relationMapLink', 'template', 'image'].includes(relation.name)) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const targetNote = relation.getTargetNote();
 | 
			
		||||
        retNoteIds.push(targetNote.noteId);
 | 
			
		||||
 | 
			
		||||
        for (const noteId of getNeighbors(targetNote, depth - 1)) {
 | 
			
		||||
            retNoteIds.push(noteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return retNoteIds;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getLinkMap(req) {
 | 
			
		||||
    const mapRootNote = becca.getNote(req.params.noteId);
 | 
			
		||||
    // if the map root itself has ignore (journal typically) then there wouldn't be anything to display so
 | 
			
		||||
    // we'll just ignore it
 | 
			
		||||
    const ignoreExcludeFromNoteMap = mapRootNote.hasLabel('excludeFromNoteMap');
 | 
			
		||||
 | 
			
		||||
    const noteIds = new Set();
 | 
			
		||||
 | 
			
		||||
    const notes = mapRootNote.getSubtreeNotes(false)
 | 
			
		||||
    const noteIds = new Set(
 | 
			
		||||
        mapRootNote.getSubtreeNotes(false)
 | 
			
		||||
            .filter(note => ignoreExcludeFromNoteMap || !note.hasLabel('excludeFromNoteMap'))
 | 
			
		||||
        .map(note => [
 | 
			
		||||
            .map(note => note.noteId)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (const noteId of getNeighbors(mapRootNote, 3)) {
 | 
			
		||||
        noteIds.add(noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const notes = Array.from(noteIds).map(noteId => {
 | 
			
		||||
        const note = becca.getNote(noteId);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            note.noteId,
 | 
			
		||||
            note.isContentAvailable() ? note.title : '[protected]',
 | 
			
		||||
            note.type
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
    notes.forEach(([noteId]) => noteIds.add(noteId));
 | 
			
		||||
        ];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const links = Object.values(becca.attributes).filter(rel => {
 | 
			
		||||
        if (rel.type !== 'relation' || rel.name === 'relationMapLink' || rel.name === 'template') {
 | 
			
		||||
 
 | 
			
		||||
@@ -54,6 +54,7 @@ const ALLOWED_OPTIONS = new Set([
 | 
			
		||||
    'dailyBackupEnabled',
 | 
			
		||||
    'weeklyBackupEnabled',
 | 
			
		||||
    'monthlyBackupEnabled',
 | 
			
		||||
    'maxContentWidth'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
function getOptions() {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,11 @@ const attributeService = require('../../services/attributes');
 | 
			
		||||
const becca = require('../../becca/becca');
 | 
			
		||||
const syncService = require('../../services/sync');
 | 
			
		||||
 | 
			
		||||
function exec(req) {
 | 
			
		||||
async function exec(req) {
 | 
			
		||||
    try {
 | 
			
		||||
        const {body} = req;
 | 
			
		||||
 | 
			
		||||
        const result = scriptService.executeScript(
 | 
			
		||||
        const result = await scriptService.executeScript(
 | 
			
		||||
            body.script,
 | 
			
		||||
            body.params,
 | 
			
		||||
            body.startNoteId,
 | 
			
		||||
 
 | 
			
		||||
@@ -247,6 +247,12 @@ function getRelatedNotes(req) {
 | 
			
		||||
 | 
			
		||||
    const allResults = matchingNameAndValue.concat(matchingName);
 | 
			
		||||
 | 
			
		||||
    const allResultNoteIds = new Set();
 | 
			
		||||
 | 
			
		||||
    for (const record of allResults) {
 | 
			
		||||
        allResultNoteIds.add(record.noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const record of allResults) {
 | 
			
		||||
        if (results.length >= 20) {
 | 
			
		||||
            break;
 | 
			
		||||
@@ -260,7 +266,7 @@ function getRelatedNotes(req) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        count: allResults.length,
 | 
			
		||||
        count: allResultNoteIds.size,
 | 
			
		||||
        results
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,10 @@ function getDateNote(req) {
 | 
			
		||||
    return dateNoteService.getDateNote(req.params.date);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getWeekNote(req) {
 | 
			
		||||
    return dateNoteService.getWeekNote(req.params.date);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getMonthNote(req) {
 | 
			
		||||
    return dateNoteService.getMonthNote(req.params.month);
 | 
			
		||||
}
 | 
			
		||||
@@ -65,6 +69,7 @@ function getHoistedNote() {
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getInboxNote,
 | 
			
		||||
    getDateNote,
 | 
			
		||||
    getWeekNote,
 | 
			
		||||
    getMonthNote,
 | 
			
		||||
    getYearNote,
 | 
			
		||||
    getDateNotesForMonth,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,15 @@ const config = require('../services/config');
 | 
			
		||||
const optionService = require('../services/options');
 | 
			
		||||
const log = require('../services/log');
 | 
			
		||||
const env = require('../services/env');
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const protectedSessionService = require("../services/protected_session");
 | 
			
		||||
 | 
			
		||||
function index(req, res) {
 | 
			
		||||
    const options = optionService.getOptionsMap();
 | 
			
		||||
 | 
			
		||||
    let view = req.cookies['trilium-device'] === 'mobile' ? 'mobile' : 'desktop';
 | 
			
		||||
    let view = (!utils.isElectron() && req.cookies['trilium-device'] === 'mobile')
 | 
			
		||||
        ? 'mobile'
 | 
			
		||||
        : 'desktop';
 | 
			
		||||
 | 
			
		||||
    const csrfToken = req.csrfToken();
 | 
			
		||||
    log.info(`Generated CSRF token ${csrfToken} with secret ${res.getHeader('set-cookie')}`);
 | 
			
		||||
@@ -32,7 +35,8 @@ function index(req, res) {
 | 
			
		||||
        isDev: env.isDev(),
 | 
			
		||||
        isMainWindow: !req.query.extra,
 | 
			
		||||
        extraHoistedNoteId: req.query.extraHoistedNoteId,
 | 
			
		||||
        isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable()
 | 
			
		||||
        isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
 | 
			
		||||
        maxContentWidth: parseInt(options.maxContentWidth)
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -262,6 +262,7 @@ function register(app) {
 | 
			
		||||
 | 
			
		||||
    apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
 | 
			
		||||
    apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);
 | 
			
		||||
    apiRoute(GET, '/api/special-notes/week/:date', specialNotesRoute.getWeekNote);
 | 
			
		||||
    apiRoute(GET, '/api/special-notes/month/:month', specialNotesRoute.getMonthNote);
 | 
			
		||||
    apiRoute(GET, '/api/special-notes/year/:year', specialNotesRoute.getYearNote);
 | 
			
		||||
    apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDateNotesForMonth);
 | 
			
		||||
 
 | 
			
		||||
@@ -58,21 +58,21 @@ function BackendScriptApi(currentNote, apiParams) {
 | 
			
		||||
     * @param {string} noteId
 | 
			
		||||
     * @returns {Note|null}
 | 
			
		||||
     */
 | 
			
		||||
    this.getNote = becca.getNote;
 | 
			
		||||
    this.getNote = noteId => becca.getNote(noteId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @method
 | 
			
		||||
     * @param {string} branchId
 | 
			
		||||
     * @returns {Branch|null}
 | 
			
		||||
     */
 | 
			
		||||
    this.getBranch = becca.getBranch;
 | 
			
		||||
    this.getBranch = branchId => becca.getBranch(branchId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @method
 | 
			
		||||
     * @param {string} attributeId
 | 
			
		||||
     * @returns {Attribute|null}
 | 
			
		||||
     */
 | 
			
		||||
    this.getAttribute = becca.getAttribute;
 | 
			
		||||
    this.getAttribute = attributeId => becca.getAttribute(attributeId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This is a powerful search method - you can search by attributes and their values, e.g.:
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
module.exports = { buildDate:"2021-10-12T22:42:48+02:00", buildRevision: "64a3a8b561f614a1940551bb3feb9e035f7cb475" };
 | 
			
		||||
module.exports = { buildDate:"2021-11-01T09:19:28+01:00", buildRevision: "930d29d64adb90707ad8854739a4628b02765bc5" };
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ function getNoteStartingWith(parentNoteId, startsWith) {
 | 
			
		||||
    return becca.getNote(noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {Note} */
 | 
			
		||||
/** @returns {Note} */
 | 
			
		||||
function getRootCalendarNote() {
 | 
			
		||||
    let rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +57,7 @@ function getRootCalendarNote() {
 | 
			
		||||
    return rootNote;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {Note} */
 | 
			
		||||
/** @returns {Note} */
 | 
			
		||||
function getYearNote(dateStr, rootNote) {
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
        rootNote = getRootCalendarNote();
 | 
			
		||||
@@ -97,7 +97,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
 | 
			
		||||
        .replace(/{month}/g, monthName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {Note} */
 | 
			
		||||
/** @returns {Note} */
 | 
			
		||||
function getMonthNote(dateStr, rootNote) {
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
        rootNote = getRootCalendarNote();
 | 
			
		||||
@@ -152,7 +152,7 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
 | 
			
		||||
        .replace(/{weekDay2}/g, weekDay.substr(0, 2));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @return {Note} */
 | 
			
		||||
/** @returns {Note} */
 | 
			
		||||
function getDateNote(dateStr) {
 | 
			
		||||
    let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,19 @@ function subscribe(eventTypes, listener) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function subscribeBeccaLoader(eventTypes, listener) {
 | 
			
		||||
    if (!Array.isArray(eventTypes)) {
 | 
			
		||||
        eventTypes = [ eventTypes ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const eventType of eventTypes) {
 | 
			
		||||
        eventListeners[eventType] = eventListeners[eventType] || [];
 | 
			
		||||
        // becca loader should be the first listener so that other listeners can already work
 | 
			
		||||
        // with updated becca
 | 
			
		||||
        eventListeners[eventType] = [listener, ...eventListeners[eventType]];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function emit(eventType, data) {
 | 
			
		||||
    const listeners = eventListeners[eventType];
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +58,7 @@ function emit(eventType, data) {
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    subscribe,
 | 
			
		||||
    subscribeBeccaLoader,
 | 
			
		||||
    emit,
 | 
			
		||||
    // event types:
 | 
			
		||||
    NOTE_TITLE_CHANGED,
 | 
			
		||||
 
 | 
			
		||||
@@ -138,7 +138,8 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) =>
 | 
			
		||||
                isInheritable: entity.isInheritable
 | 
			
		||||
            }).save();
 | 
			
		||||
 | 
			
		||||
            targetNote.invalidateAttributeCache();
 | 
			
		||||
            // becca will not be updated before we'll check from the other side which would create infinite relation creation (#2269)
 | 
			
		||||
            targetNote.invalidateThisCache();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -150,9 +151,6 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) =>
 | 
			
		||||
 | 
			
		||||
        for (const relation of relations) {
 | 
			
		||||
            if (relation.value === note.noteId) {
 | 
			
		||||
                note.invalidateAttributeCache();
 | 
			
		||||
                targetNote.invalidateAttributeCache();
 | 
			
		||||
 | 
			
		||||
                relation.markAsDeleted();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -393,7 +393,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {string} path without leading or trailing slash and backslashes converted to forward ones*/
 | 
			
		||||
    /** @returns {string} path without leading or trailing slash and backslashes converted to forward ones*/
 | 
			
		||||
    function normalizeFilePath(filePath) {
 | 
			
		||||
        filePath = filePath.replace(/\\/g, "/");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,8 @@ const defaultOptions = [
 | 
			
		||||
    { name: 'autoReadonlySizeCode', value: '30000', isSynced: false },
 | 
			
		||||
    { name: 'dailyBackupEnabled', value: 'true', isSynced: false },
 | 
			
		||||
    { name: 'weeklyBackupEnabled', value: 'true', isSynced: false },
 | 
			
		||||
    { name: 'monthlyBackupEnabled', value: 'true', isSynced: false }
 | 
			
		||||
    { name: 'monthlyBackupEnabled', value: 'true', isSynced: false },
 | 
			
		||||
    { name: 'maxContentWidth', value: '1200', isSynced: false },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function initStartupOptions() {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ function parse(value) {
 | 
			
		||||
        else if (token.startsWith('inverse')) {
 | 
			
		||||
            const chunks = token.split('=');
 | 
			
		||||
 | 
			
		||||
            defObj.inverseRelation = chunks[1];
 | 
			
		||||
            defObj.inverseRelation = chunks[1].replace(/[^\p{L}\p{N}_:]/ug, "")
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            console.log("Unrecognized attribute definition token:", token);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
const lex = require('./lex');
 | 
			
		||||
const handleParens = require('./handle_parens');
 | 
			
		||||
const parse = require('./parse');
 | 
			
		||||
const NoteSet = require("../note_set");
 | 
			
		||||
const SearchResult = require("../search_result");
 | 
			
		||||
const SearchContext = require("../search_context");
 | 
			
		||||
const becca = require('../../../becca/becca');
 | 
			
		||||
 
 | 
			
		||||
@@ -137,7 +137,15 @@ function saveSqlConsole(sqlConsoleNoteId) {
 | 
			
		||||
        attributeService.getNoteWithLabel('sqlConsoleHome')
 | 
			
		||||
        || dateNoteService.getDateNote(today);
 | 
			
		||||
 | 
			
		||||
    return sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
 | 
			
		||||
    const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
 | 
			
		||||
 | 
			
		||||
    for (const parentBranch of sqlConsoleNote.getParentBranches()) {
 | 
			
		||||
        if (parentBranch.parentNote.hasAncestor("hidden")) {
 | 
			
		||||
            parentBranch.markAsDeleted();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createSearchNote(searchString, ancestorNoteId) {
 | 
			
		||||
@@ -158,25 +166,34 @@ function createSearchNote(searchString, ancestorNoteId) {
 | 
			
		||||
    return note;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveSearchNote(searchNoteId) {
 | 
			
		||||
    const searchNote = becca.getNote(searchNoteId);
 | 
			
		||||
 | 
			
		||||
function getSearchHome() {
 | 
			
		||||
    const hoistedNote = getHoistedNote();
 | 
			
		||||
    let searchHome;
 | 
			
		||||
 | 
			
		||||
    if (!hoistedNote.isRoot()) {
 | 
			
		||||
        searchHome = hoistedNote.searchNoteInSubtree('#hoistedSearchHome')
 | 
			
		||||
        return hoistedNote.searchNoteInSubtree('#hoistedSearchHome')
 | 
			
		||||
            || hoistedNote.searchNoteInSubtree('#searchHome')
 | 
			
		||||
            || hoistedNote;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
    } else {
 | 
			
		||||
        const today = dateUtils.localNowDate();
 | 
			
		||||
 | 
			
		||||
        searchHome = hoistedNote.searchNoteInSubtree('#searchHome')
 | 
			
		||||
        return hoistedNote.searchNoteInSubtree('#searchHome')
 | 
			
		||||
            || dateNoteService.getDateNote(today);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    return searchNote.cloneTo(searchHome.noteId);
 | 
			
		||||
function saveSearchNote(searchNoteId) {
 | 
			
		||||
    const searchNote = becca.getNote(searchNoteId);
 | 
			
		||||
    const searchHome = getSearchHome();
 | 
			
		||||
 | 
			
		||||
    const result = searchNote.cloneTo(searchHome.noteId);
 | 
			
		||||
 | 
			
		||||
    for (const parentBranch of searchNote.getParentBranches()) {
 | 
			
		||||
        if (parentBranch.parentNote.hasAncestor("hidden")) {
 | 
			
		||||
            parentBranch.markAsDeleted();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getHoistedNote() {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ class TaskContext {
 | 
			
		||||
        this.increaseProgressCount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {TaskContext} */
 | 
			
		||||
    /** @returns {TaskContext} */
 | 
			
		||||
    static getInstance(taskId, taskType, data) {
 | 
			
		||||
        if (!taskContexts[taskId]) {
 | 
			
		||||
            taskContexts[taskId] = new TaskContext(taskId, taskType, data);
 | 
			
		||||
 
 | 
			
		||||
@@ -298,6 +298,10 @@ function normalize(str) {
 | 
			
		||||
    return removeDiacritic(str).toLowerCase();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function filterAttributeName(name) {
 | 
			
		||||
    return name.replace(/[^\p{L}\p{N}_:]/ug, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    randomSecureToken,
 | 
			
		||||
    randomString,
 | 
			
		||||
@@ -331,5 +335,6 @@ module.exports = {
 | 
			
		||||
    timeLimit,
 | 
			
		||||
    deferred,
 | 
			
		||||
    removeDiacritic,
 | 
			
		||||
    normalize
 | 
			
		||||
    normalize,
 | 
			
		||||
    filterAttributeName
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,12 @@
 | 
			
		||||
    };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .note-split {
 | 
			
		||||
        max-width: <%= maxContentWidth %>px;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<!-- Required for correct loading of scripts in Electron -->
 | 
			
		||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
<div id="note-revisions-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
 | 
			
		||||
    <style>
 | 
			
		||||
        #note-revision-content-wrapper {
 | 
			
		||||
            flex-grow: 1;
 | 
			
		||||
            margin-left: 20px;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            min-width: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #note-revision-content {
 | 
			
		||||
            overflow: auto;
 | 
			
		||||
            word-break: break-word;
 | 
			
		||||
@@ -9,6 +17,12 @@
 | 
			
		||||
            max-width: 100%;
 | 
			
		||||
            object-fit: contain;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #note-revision-content pre {
 | 
			
		||||
            max-width: 100%;
 | 
			
		||||
            word-break: break-all;
 | 
			
		||||
            white-space: pre-wrap;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <div class="modal-dialog modal-xl" role="document">
 | 
			
		||||
@@ -34,7 +48,7 @@
 | 
			
		||||
                    <div id="note-revision-list" style="position: static; height: 100%; overflow: auto;" class="dropdown-menu"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div id="note-revision-content-wrapper" style="flex-grow: 1; margin-left: 20px; display: flex; flex-direction: column;">
 | 
			
		||||
                <div id="note-revision-content-wrapper">
 | 
			
		||||
                    <div style="flex-grow: 0; display: flex; justify-content: space-between;">
 | 
			
		||||
                        <h3 id="note-revision-title" style="margin: 3px; flex-grow: 100;"></h3>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -134,12 +134,12 @@
 | 
			
		||||
            <p>This setup needs to be initiated from the desktop instance:</p>
 | 
			
		||||
 | 
			
		||||
            <ol>
 | 
			
		||||
                <li>please open your desktop instance of Trilium Notes</li>
 | 
			
		||||
                <li>click on Options button in the top right</li>
 | 
			
		||||
                <li>click on Sync tab</li>
 | 
			
		||||
                <li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
 | 
			
		||||
                <li>click on "Test sync" button</li>
 | 
			
		||||
                <li>once you've done all this, click <a href="/">here</a></li>
 | 
			
		||||
                <li>Open your desktop instance of Trilium Notes.</li>
 | 
			
		||||
                <li>From the Trilium Menu, click Options.</li>
 | 
			
		||||
                <li>Click on Sync tab.</li>
 | 
			
		||||
                <li>Change server instance address to: <span id="current-host"></span> and click save.</li>
 | 
			
		||||
                <li>Click "Test sync" button to verify connection is successfull.</li>
 | 
			
		||||
                <li>Once you've completed these steps, click <a href="/">here</a>.</li>
 | 
			
		||||
            </ol>
 | 
			
		||||
 | 
			
		||||
            <button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user