mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			kev/share-
			...
			fix/resolv
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c0a55fec60 | 
| @@ -1,6 +1,6 @@ | |||||||
| root = true | root = true | ||||||
|  |  | ||||||
| [*.{js,ts,tsx}] | [*.{js,ts}] | ||||||
| charset = utf-8 | charset = utf-8 | ||||||
| end_of_line = lf | end_of_line = lf | ||||||
| indent_size = 4 | indent_size = 4 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,5 +2,3 @@ | |||||||
|  |  | ||||||
| github: [eliandoran] | github: [eliandoran] | ||||||
| custom: ["https://paypal.me/eliandoran"] | custom: ["https://paypal.me/eliandoran"] | ||||||
| liberapay: ElianDoran |  | ||||||
| buy_me_a_coffee: eliandoran |  | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -74,7 +74,7 @@ runs: | |||||||
|  |  | ||||||
|   - name: Update build info |   - name: Update build info | ||||||
|     shell: ${{ inputs.shell }} |     shell: ${{ inputs.shell }} | ||||||
|     run: pnpm run chore:update-build-info |     run: npm run chore:update-build-info | ||||||
|  |  | ||||||
|   # Critical debugging configuration |   # Critical debugging configuration | ||||||
|   - name: Run electron-forge build with enhanced logging |   - name: Run electron-forge build with enhanced logging | ||||||
| @@ -86,8 +86,7 @@ runs: | |||||||
|       APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }} |       APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }} | ||||||
|       WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }} |       WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }} | ||||||
|       TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }} |       TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }} | ||||||
|       TARGET_ARCH: ${{ inputs.arch }} |     run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }} | ||||||
|     run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }} |  | ||||||
|  |  | ||||||
|   # Add DMG signing step |   # Add DMG signing step | ||||||
|   - name: Sign DMG |   - name: Sign DMG | ||||||
| @@ -163,25 +162,3 @@ runs: | |||||||
|         echo "Found ZIP: $zip_file" |         echo "Found ZIP: $zip_file" | ||||||
|         echo "Note: ZIP files are not code signed, but their contents should be" |         echo "Note: ZIP files are not code signed, but their contents should be" | ||||||
|       fi |       fi | ||||||
|  |  | ||||||
|   - name: Sign the RPM |  | ||||||
|     if: inputs.os == 'linux' |  | ||||||
|     shell: ${{ inputs.shell }} |  | ||||||
|     run: | |  | ||||||
|       echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import |  | ||||||
|  |  | ||||||
|       # Import the key into RPM for verification |  | ||||||
|       gpg --export -a > pubkey |  | ||||||
|       rpm --import pubkey |  | ||||||
|       rm pubkey |  | ||||||
|  |  | ||||||
|       # Sign the RPM |  | ||||||
|       rpm_file=$(find ./apps/desktop/upload -name "*.rpm" -print -quit) |  | ||||||
|       rpmsign --define "_gpg_name Trilium Notes Signing Key <triliumnotes@outlook.com>" --addsign "$rpm_file" |  | ||||||
|       rpm -Kv "$rpm_file" |  | ||||||
|  |  | ||||||
|       # Validate code signing |  | ||||||
|       if ! rpm -K "$rpm_file" | grep -q "digests signatures OK"; then |  | ||||||
|         echo .rpm file not signed |  | ||||||
|         exit 1 |  | ||||||
|       fi |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,7 +10,7 @@ runs: | |||||||
|   steps: |   steps: | ||||||
|   - uses: pnpm/action-setup@v4 |   - uses: pnpm/action-setup@v4 | ||||||
|   - name: Set up node & dependencies |   - name: Set up node & dependencies | ||||||
|     uses: actions/setup-node@v5 |     uses: actions/setup-node@v4 | ||||||
|     with: |     with: | ||||||
|       node-version: 22 |       node-version: 22 | ||||||
|       cache: "pnpm" |       cache: "pnpm" | ||||||
| @@ -23,7 +23,7 @@ runs: | |||||||
|     shell: bash |     shell: bash | ||||||
|     run: | |     run: | | ||||||
|       pnpm run chore:update-build-info |       pnpm run chore:update-build-info | ||||||
|       pnpm run --filter server package |       pnpm nx --project=server package | ||||||
|   - name: Prepare artifacts |   - name: Prepare artifacts | ||||||
|     shell: bash |     shell: bash | ||||||
|     run: | |     run: | | ||||||
|   | |||||||
| @@ -1,103 +0,0 @@ | |||||||
| name: "Deploy to Cloudflare Pages" |  | ||||||
| description: "Deploys to Cloudflare Pages on either a temporary branch with preview comment, or on the production version if on the main branch." |  | ||||||
| inputs: |  | ||||||
|   project_name: |  | ||||||
|     description: "CloudFlare Pages project name" |  | ||||||
|   comment_body: |  | ||||||
|     description: "The message to display when deployment is ready" |  | ||||||
|     default: "Deployment is ready." |  | ||||||
|     required: false |  | ||||||
|   production_url: |  | ||||||
|     description: "The URL to mention as the production URL." |  | ||||||
|     required: true |  | ||||||
|   deploy_dir: |  | ||||||
|     description: "The directory from which to deploy." |  | ||||||
|     required: true |  | ||||||
|   cloudflare_api_token: |  | ||||||
|     description: "The Cloudflare API token to use for deployment." |  | ||||||
|     required: true |  | ||||||
|   cloudflare_account_id: |  | ||||||
|     description: "The Cloudflare account ID to use for deployment." |  | ||||||
|     required: true |  | ||||||
|   github_token: |  | ||||||
|     description: "The GitHub token to use for posting PR comments." |  | ||||||
|     required: true |  | ||||||
| runs: |  | ||||||
|   using: composite |  | ||||||
|   steps: |  | ||||||
|     # Install wrangler globally to avoid workspace issues |  | ||||||
|     - name: Install Wrangler |  | ||||||
|       shell: bash |  | ||||||
|       run: npm install -g wrangler |  | ||||||
|  |  | ||||||
|     # Deploy using Wrangler (use pre-installed wrangler) |  | ||||||
|     - name: Deploy to Cloudflare Pages |  | ||||||
|       id: deploy |  | ||||||
|       if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' |  | ||||||
|       uses: cloudflare/wrangler-action@v3 |  | ||||||
|       with: |  | ||||||
|         apiToken: ${{ inputs.cloudflare_api_token }} |  | ||||||
|         accountId: ${{ inputs.cloudflare_account_id }} |  | ||||||
|         command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=${{ github.ref_name }} |  | ||||||
|         wranglerVersion: ''  # Use pre-installed version |  | ||||||
|  |  | ||||||
|     # Deploy preview for PRs |  | ||||||
|     - name: Deploy Preview to Cloudflare Pages |  | ||||||
|       id: preview-deployment |  | ||||||
|       if: github.event_name == 'pull_request' |  | ||||||
|       uses: cloudflare/wrangler-action@v3 |  | ||||||
|       with: |  | ||||||
|         apiToken: ${{ inputs.cloudflare_api_token }} |  | ||||||
|         accountId: ${{ inputs.cloudflare_account_id }} |  | ||||||
|         command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=pr-${{ github.event.pull_request.number }} |  | ||||||
|         wranglerVersion: ''  # Use pre-installed version |  | ||||||
|  |  | ||||||
|     # Post deployment URL as PR comment |  | ||||||
|     - name: Comment PR with Preview URL |  | ||||||
|       if: github.event_name == 'pull_request' |  | ||||||
|       uses: actions/github-script@v8 |  | ||||||
|       env: |  | ||||||
|         COMMENT_BODY: ${{ inputs.comment_body }} |  | ||||||
|         PRODUCTION_URL: ${{ inputs.production_url }} |  | ||||||
|         PROJECT_NAME: ${{ inputs.project_name }} |  | ||||||
|       with: |  | ||||||
|         github-token: ${{ inputs.github_token }} |  | ||||||
|         script: | |  | ||||||
|           const prNumber = context.issue.number; |  | ||||||
|           // Construct preview URL based on Cloudflare Pages pattern |  | ||||||
|           const projectName = process.env.PROJECT_NAME; |  | ||||||
|           const previewUrl = `https://pr-${prNumber}.${projectName}.pages.dev`; |  | ||||||
|  |  | ||||||
|           // Check if we already commented |  | ||||||
|           const comments = await github.rest.issues.listComments({ |  | ||||||
|             owner: context.repo.owner, |  | ||||||
|             repo: context.repo.repo, |  | ||||||
|             issue_number: prNumber |  | ||||||
|           }); |  | ||||||
|  |  | ||||||
|           const customMessage = process.env.COMMENT_BODY; |  | ||||||
|           const botComment = comments.data.find(comment => |  | ||||||
|             comment.user.type === 'Bot' && |  | ||||||
|             comment.body.includes(customMessage) |  | ||||||
|           ); |  | ||||||
|  |  | ||||||
|           const mainUrl = process.env.PRODUCTION_URL; |  | ||||||
|           const commentBody = `${customMessage}!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`; |  | ||||||
|  |  | ||||||
|           if (botComment) { |  | ||||||
|             // Update existing comment |  | ||||||
|             await github.rest.issues.updateComment({ |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               comment_id: botComment.id, |  | ||||||
|               body: commentBody |  | ||||||
|             }); |  | ||||||
|           } else { |  | ||||||
|             // Create new comment |  | ||||||
|             await github.rest.issues.createComment({ |  | ||||||
|               issue_number: prNumber, |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               body: commentBody |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
							
								
								
									
										2
									
								
								.github/actions/report-size/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/report-size/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -44,7 +44,7 @@ runs: | |||||||
|   steps: |   steps: | ||||||
|     # Checkout branch to compare to [required] |     # Checkout branch to compare to [required] | ||||||
|     - name: Checkout base branch |     - name: Checkout base branch | ||||||
|       uses: actions/checkout@v5 |       uses: actions/checkout@v4 | ||||||
|       with: |       with: | ||||||
|         ref: ${{ inputs.branch }} |         ref: ${{ inputs.branch }} | ||||||
|         path: br-base |         path: br-base | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								.github/workflows/checks.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/checks.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,18 +0,0 @@ | |||||||
| name: Checks |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|   pull_request_target: |  | ||||||
|     types: [synchronize] |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   main: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     permissions: |  | ||||||
|       contents: write |  | ||||||
|     steps: |  | ||||||
|       - name: Check if PRs have conflicts |  | ||||||
|         uses: eps1lon/actions-label-merge-conflict@v3 |  | ||||||
|         if: github.repository == ${{ vars.REPO_MAIN }} |  | ||||||
|         with: |  | ||||||
|           dirtyLabel: "merge-conflicts" |  | ||||||
|           repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}" |  | ||||||
							
								
								
									
										2
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							| @@ -57,7 +57,7 @@ jobs: | |||||||
|         # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages |         # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages | ||||||
|     steps: |     steps: | ||||||
|     - name: Checkout repository |     - name: Checkout repository | ||||||
|       uses: actions/checkout@v5 |       uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|     # Add any setup steps before running the `github/codeql-action/init` action. |     # Add any setup steps before running the `github/codeql-action/init` action. | ||||||
|     # This includes steps like installing compilers or runtimes (`actions/setup-node` |     # This includes steps like installing compilers or runtimes (`actions/setup-node` | ||||||
|   | |||||||
							
								
								
									
										128
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										128
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,128 +0,0 @@ | |||||||
| # GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages |  | ||||||
| # This workflow builds and deploys your MkDocs site when changes are pushed to main |  | ||||||
| name: Deploy MkDocs Documentation |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   # Trigger on push to main branch |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|       - master  # Also support master branch |  | ||||||
|     # Only run when docs files change |  | ||||||
|     paths: |  | ||||||
|       - 'docs/**' |  | ||||||
|       - 'README.md'  # README is synced to docs/index.md |  | ||||||
|       - 'mkdocs.yml' |  | ||||||
|       - 'requirements-docs.txt' |  | ||||||
|       - '.github/workflows/deploy-docs.yml' |  | ||||||
|       - 'scripts/fix-mkdocs-structure.ts' |  | ||||||
|  |  | ||||||
|   # Allow manual triggering from Actions tab |  | ||||||
|   workflow_dispatch: |  | ||||||
|  |  | ||||||
|   # Run on pull requests for preview deployments |  | ||||||
|   pull_request: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|       - master |  | ||||||
|     paths: |  | ||||||
|       - 'docs/**' |  | ||||||
|       - 'README.md'  # README is synced to docs/index.md |  | ||||||
|       - 'mkdocs.yml' |  | ||||||
|       - 'requirements-docs.txt' |  | ||||||
|       - '.github/workflows/deploy-docs.yml' |  | ||||||
|       - 'scripts/fix-mkdocs-structure.ts' |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   build-and-deploy: |  | ||||||
|     name: Build and Deploy MkDocs |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     timeout-minutes: 10 |  | ||||||
|  |  | ||||||
|     # Required permissions for deployment |  | ||||||
|     permissions: |  | ||||||
|       contents: read |  | ||||||
|       deployments: write |  | ||||||
|       pull-requests: write # For PR preview comments |  | ||||||
|       id-token: write # For OIDC authentication (if needed) |  | ||||||
|  |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout Repository |  | ||||||
|         uses: actions/checkout@v5 |  | ||||||
|         with: |  | ||||||
|           fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin |  | ||||||
|  |  | ||||||
|       - name: Setup Python |  | ||||||
|         uses: actions/setup-python@v6 |  | ||||||
|         with: |  | ||||||
|           python-version: '3.13' |  | ||||||
|           cache: 'pip' |  | ||||||
|           cache-dependency-path: 'requirements-docs.txt' |  | ||||||
|  |  | ||||||
|       - name: Install MkDocs and Dependencies |  | ||||||
|         run: | |  | ||||||
|           pip install --upgrade pip |  | ||||||
|           pip install -r requirements-docs.txt |  | ||||||
|         env: |  | ||||||
|           PIP_DISABLE_PIP_VERSION_CHECK: 1 |  | ||||||
|  |  | ||||||
|       # Setup pnpm before fixing docs structure |  | ||||||
|       - name: Setup pnpm |  | ||||||
|         uses: pnpm/action-setup@v4 |  | ||||||
|  |  | ||||||
|       # Setup Node.js with pnpm |  | ||||||
|       - name: Setup Node.js |  | ||||||
|         uses: actions/setup-node@v5 |  | ||||||
|         with: |  | ||||||
|           node-version: '22' |  | ||||||
|           cache: 'pnpm' |  | ||||||
|  |  | ||||||
|       # Install Node.js dependencies for the TypeScript script |  | ||||||
|       - name: Install Dependencies |  | ||||||
|         run: | |  | ||||||
|           pnpm install --frozen-lockfile |  | ||||||
|  |  | ||||||
|       - name: Fix Documentation Structure |  | ||||||
|         run: | |  | ||||||
|           # Fix duplicate navigation entries by moving overview pages to index.md |  | ||||||
|           pnpm run chore:fix-mkdocs-structure |  | ||||||
|  |  | ||||||
|       - name: Build MkDocs Site |  | ||||||
|         run: | |  | ||||||
|           # Build with strict mode but allow expected warnings |  | ||||||
|           mkdocs build --verbose || { |  | ||||||
|             EXIT_CODE=$? |  | ||||||
|             # Check if the only issue is expected warnings |  | ||||||
|             if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \ |  | ||||||
|                [ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then |  | ||||||
|               echo "✅ Build succeeded with expected warnings" |  | ||||||
|               mkdocs build --verbose |  | ||||||
|             else |  | ||||||
|               echo "❌ Build failed with unexpected errors" |  | ||||||
|               exit $EXIT_CODE |  | ||||||
|             fi |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|       - name: Fix HTML Links |  | ||||||
|         run: | |  | ||||||
|           # Remove .md extensions from links in generated HTML |  | ||||||
|           pnpm tsx ./scripts/fix-html-links.ts site |  | ||||||
|  |  | ||||||
|       - name: Validate Built Site |  | ||||||
|         run: | |  | ||||||
|           # Basic validation that important files exist |  | ||||||
|           test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1) |  | ||||||
|           test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1) |  | ||||||
|           test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1) |  | ||||||
|           echo "✅ Site validation passed" |  | ||||||
|  |  | ||||||
|       - name: Deploy |  | ||||||
|         uses: ./.github/actions/deploy-to-cloudflare-pages |  | ||||||
|         with: |  | ||||||
|           project_name: "trilium-docs" |  | ||||||
|           comment_body: "📚 Documentation preview is ready" |  | ||||||
|           production_url: "https://docs.triliumnotes.org" |  | ||||||
|           deploy_dir: "site" |  | ||||||
|           cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }} |  | ||||||
|           cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |  | ||||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
							
								
								
									
										47
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,24 +19,45 @@ permissions: | |||||||
|   pull-requests: write  # for PR comments |   pull-requests: write  # for PR comments | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   test_dev: |   check-affected: | ||||||
|     name: Test development |     name: Check affected jobs (NX) | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v4 | ||||||
|  |         with: | ||||||
|  |           fetch-depth: 0  # needed for https://github.com/marketplace/actions/nx-set-shas | ||||||
|  |  | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
|         uses: actions/setup-node@v5 |         uses: actions/setup-node@v4 | ||||||
|  |         with: | ||||||
|  |           node-version: 22 | ||||||
|  |           cache: 'pnpm' | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: pnpm install --frozen-lockfile | ||||||
|  |  | ||||||
|  |       - uses: nrwl/nx-set-shas@v4 | ||||||
|  |       - name: Check affected | ||||||
|  |         run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build | ||||||
|  |  | ||||||
|  |   test_dev: | ||||||
|  |     name: Test development | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - check-affected | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout the repository | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - uses: pnpm/action-setup@v4 | ||||||
|  |       - name: Set up node & dependencies | ||||||
|  |         uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22 |           node-version: 22 | ||||||
|           cache: "pnpm" |           cache: "pnpm" | ||||||
|       - run: pnpm install --frozen-lockfile |       - run: pnpm install --frozen-lockfile | ||||||
|  |  | ||||||
|       - name: Typecheck |  | ||||||
|         run: pnpm typecheck |  | ||||||
|  |  | ||||||
|       - name: Run the unit tests |       - name: Run the unit tests | ||||||
|         run: pnpm run test:all |         run: pnpm run test:all | ||||||
|  |  | ||||||
| @@ -45,15 +66,16 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     needs: |     needs: | ||||||
|       - test_dev |       - test_dev | ||||||
|  |       - check-affected | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pnpm install --frozen-lockfile |         run: pnpm install --frozen-lockfile | ||||||
|       - name: Update build info |       - name: Update build info | ||||||
|         run: pnpm run chore:update-build-info |         run: pnpm run chore:update-build-info | ||||||
|       - name: Trigger client build |       - name: Trigger client build | ||||||
|         run: pnpm client:build |         run: pnpm nx run client:build | ||||||
|       - name: Send client bundle stats to RelativeCI |       - name: Send client bundle stats to RelativeCI | ||||||
|         if: false |         if: false | ||||||
|         uses: relative-ci/agent-action@v3 |         uses: relative-ci/agent-action@v3 | ||||||
| @@ -61,7 +83,7 @@ jobs: | |||||||
|           webpackStatsFile: ./apps/client/dist/webpack-stats.json |           webpackStatsFile: ./apps/client/dist/webpack-stats.json | ||||||
|           key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }} |           key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }} | ||||||
|       - name: Trigger server build |       - name: Trigger server build | ||||||
|         run: pnpm run server:build |         run: pnpm nx run server:build | ||||||
|       - uses: docker/setup-buildx-action@v3 |       - uses: docker/setup-buildx-action@v3 | ||||||
|       - uses: docker/build-push-action@v6 |       - uses: docker/build-push-action@v6 | ||||||
|         with: |         with: | ||||||
| @@ -73,6 +95,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     needs: |     needs: | ||||||
|       - build_docker |       - build_docker | ||||||
|  |       - check-affected | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         include: |         include: | ||||||
| @@ -80,7 +103,7 @@ jobs: | |||||||
|           - dockerfile: Dockerfile |           - dockerfile: Dockerfile | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
| @@ -89,7 +112,7 @@ jobs: | |||||||
|       - name: Update build info |       - name: Update build info | ||||||
|         run: pnpm run chore:update-build-info |         run: pnpm run chore:update-build-info | ||||||
|       - name: Trigger build |       - name: Trigger build | ||||||
|         run: pnpm server:build |         run: pnpm nx run server:build | ||||||
|  |  | ||||||
|       - name: Set IMAGE_NAME to lowercase |       - name: Set IMAGE_NAME to lowercase | ||||||
|         run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV |         run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								.github/workflows/main-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/main-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -32,7 +32,7 @@ jobs: | |||||||
|           - dockerfile: Dockerfile |           - dockerfile: Dockerfile | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|       - name: Set IMAGE_NAME to lowercase |       - name: Set IMAGE_NAME to lowercase | ||||||
|         run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV |         run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV | ||||||
| @@ -44,7 +44,7 @@ jobs: | |||||||
|  |  | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
|         uses: actions/setup-node@v5 |         uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22 |           node-version: 22 | ||||||
|           cache: "pnpm" |           cache: "pnpm" | ||||||
| @@ -82,7 +82,7 @@ jobs: | |||||||
|           require-healthy: true |           require-healthy: true | ||||||
|  |  | ||||||
|       - name: Run Playwright tests |       - name: Run Playwright tests | ||||||
|         run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server-e2e e2e |         run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e | ||||||
|  |  | ||||||
|       - name: Upload Playwright trace |       - name: Upload Playwright trace | ||||||
|         if: failure() |         if: failure() | ||||||
| @@ -141,10 +141,10 @@ jobs: | |||||||
|         run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV |         run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV | ||||||
|  |  | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v4 | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
|         uses: actions/setup-node@v5 |         uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22 |           node-version: 22 | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
| @@ -152,12 +152,12 @@ jobs: | |||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pnpm install --frozen-lockfile |         run: pnpm install --frozen-lockfile | ||||||
|  |  | ||||||
|       - name: Update build info |  | ||||||
|         run: pnpm run chore:update-build-info |  | ||||||
|  |  | ||||||
|       - name: Run the TypeScript build |       - name: Run the TypeScript build | ||||||
|         run: pnpm run server:build |         run: pnpm run server:build | ||||||
|  |  | ||||||
|  |       - name: Update build info | ||||||
|  |         run: pnpm run chore:update-build-info | ||||||
|  |  | ||||||
|       - name: Docker meta |       - name: Docker meta | ||||||
|         id: meta |         id: meta | ||||||
|         uses: docker/metadata-action@v5 |         uses: docker/metadata-action@v5 | ||||||
| @@ -211,7 +211,7 @@ jobs: | |||||||
|       - name: Upload digest |       - name: Upload digest | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }} |           name: digests-${{ env.PLATFORM_PAIR }} | ||||||
|           path: /tmp/digests/* |           path: /tmp/digests/* | ||||||
|           if-no-files-found: error |           if-no-files-found: error | ||||||
|           retention-days: 1 |           retention-days: 1 | ||||||
| @@ -223,7 +223,7 @@ jobs: | |||||||
|       - build |       - build | ||||||
|     steps: |     steps: | ||||||
|       - name: Download digests |       - name: Download digests | ||||||
|         uses: actions/download-artifact@v5 |         uses: actions/download-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           path: /tmp/digests |           path: /tmp/digests | ||||||
|           pattern: digests-* |           pattern: digests-* | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,6 +19,7 @@ concurrency: | |||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| env: | env: | ||||||
|  |   GITHUB_UPLOAD_URL: https://uploads.github.com/repos/TriliumNext/Notes/releases/179589950/assets{?name,label} | ||||||
|   GITHUB_RELEASE_ID: 179589950 |   GITHUB_RELEASE_ID: 179589950 | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
| @@ -26,7 +27,6 @@ permissions: | |||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   nightly-electron: |   nightly-electron: | ||||||
|     if: github.repository == ${{ vars.REPO_MAIN }} |  | ||||||
|     name: Deploy nightly |     name: Deploy nightly | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
| @@ -47,15 +47,16 @@ jobs: | |||||||
|             forge_platform: win32 |             forge_platform: win32 | ||||||
|     runs-on: ${{ matrix.os.image }} |     runs-on: ${{ matrix.os.image }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
|         uses: actions/setup-node@v5 |         uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22 |           node-version: 22 | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pnpm install --frozen-lockfile |         run: pnpm install --frozen-lockfile | ||||||
|  |       - uses: nrwl/nx-set-shas@v4 | ||||||
|       - name: Update nightly version |       - name: Update nightly version | ||||||
|         run: npm run chore:ci-update-nightly-version |         run: npm run chore:ci-update-nightly-version | ||||||
|       - name: Run the build |       - name: Run the build | ||||||
| @@ -74,10 +75,9 @@ jobs: | |||||||
|           APPLE_ID: ${{ secrets.APPLE_ID }} |           APPLE_ID: ${{ secrets.APPLE_ID }} | ||||||
|           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} |           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | ||||||
|           WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} |           WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} | ||||||
|           GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }} |  | ||||||
|  |  | ||||||
|       - name: Publish release |       - name: Publish release | ||||||
|         uses: softprops/action-gh-release@v2.3.4 |         uses: softprops/action-gh-release@v2.3.2 | ||||||
|         if: ${{ github.event_name != 'pull_request' }} |         if: ${{ github.event_name != 'pull_request' }} | ||||||
|         with: |         with: | ||||||
|           make_latest: false |           make_latest: false | ||||||
| @@ -96,7 +96,6 @@ jobs: | |||||||
|           path: apps/desktop/upload |           path: apps/desktop/upload | ||||||
|  |  | ||||||
|   nightly-server: |   nightly-server: | ||||||
|     if: github.repository == ${{ vars.REPO_MAIN }} |  | ||||||
|     name: Deploy server nightly |     name: Deploy server nightly | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
| @@ -109,7 +108,7 @@ jobs: | |||||||
|             runs-on: ubuntu-24.04-arm |             runs-on: ubuntu-24.04-arm | ||||||
|     runs-on: ${{ matrix.runs-on }} |     runs-on: ${{ matrix.runs-on }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|       - name: Run the build |       - name: Run the build | ||||||
|         uses: ./.github/actions/build-server |         uses: ./.github/actions/build-server | ||||||
| @@ -118,7 +117,7 @@ jobs: | |||||||
|           arch: ${{ matrix.arch }} |           arch: ${{ matrix.arch }} | ||||||
|  |  | ||||||
|       - name: Publish release |       - name: Publish release | ||||||
|         uses: softprops/action-gh-release@v2.3.4 |         uses: softprops/action-gh-release@v2.3.2 | ||||||
|         if: ${{ github.event_name != 'pull_request' }} |         if: ${{ github.event_name != 'pull_request' }} | ||||||
|         with: |         with: | ||||||
|           make_latest: false |           make_latest: false | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,8 +4,6 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|     paths-ignore: |  | ||||||
|       - "apps/website/**" |  | ||||||
|   pull_request: |   pull_request: | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
| @@ -16,13 +14,19 @@ jobs: | |||||||
|   main: |   main: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
|           filter: tree:0 |           filter: tree:0 | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|  |  | ||||||
|  |       # This enables task distribution via Nx Cloud | ||||||
|  |       # Run this command as early as possible, before dependencies are installed | ||||||
|  |       # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun | ||||||
|  |       # Connect your workspace by running "nx connect" and uncomment this line to enable task distribution | ||||||
|  |       # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci" | ||||||
|  |  | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - uses: actions/setup-node@v5 |       - uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22 |           node-version: 22 | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
| @@ -30,12 +34,10 @@ jobs: | |||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pnpm install --frozen-lockfile |         run: pnpm install --frozen-lockfile | ||||||
|       - run: pnpm exec playwright install --with-deps |       - run: pnpm exec playwright install --with-deps | ||||||
|  |       - uses: nrwl/nx-set-shas@v4 | ||||||
|  |  | ||||||
|       - run: pnpm --filter server-e2e e2e |       # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud | ||||||
|  |       # - run: npx nx-cloud record -- echo Hello World | ||||||
|       - name: Upload test report |       # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected | ||||||
|         if: failure() |       # When you enable task distribution, run the e2e-ci task instead of e2e | ||||||
|         uses: actions/upload-artifact@v4 |       - run: pnpm exec nx affected -t e2e --exclude desktop-e2e | ||||||
|         with: |  | ||||||
|           name: e2e report |  | ||||||
|           path: apps/server-e2e/test-output |  | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,30 +30,18 @@ jobs: | |||||||
|             image: win-signing |             image: win-signing | ||||||
|             shell: cmd |             shell: cmd | ||||||
|             forge_platform: win32 |             forge_platform: win32 | ||||||
|         # Exclude ARM64 Linux from default matrix to use native runner |  | ||||||
|         exclude: |  | ||||||
|           - arch: arm64 |  | ||||||
|             os: |  | ||||||
|               name: linux |  | ||||||
|         # Add ARM64 Linux with native ubuntu-24.04-arm runner for better-sqlite3 compatibility |  | ||||||
|         include: |  | ||||||
|           - arch: arm64 |  | ||||||
|             os: |  | ||||||
|               name: linux |  | ||||||
|               image: ubuntu-24.04-arm |  | ||||||
|               shell: bash |  | ||||||
|               forge_platform: linux |  | ||||||
|     runs-on: ${{ matrix.os.image }} |     runs-on: ${{ matrix.os.image }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|       - uses: pnpm/action-setup@v4 |       - uses: pnpm/action-setup@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
|         uses: actions/setup-node@v5 |         uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22 |           node-version: 22 | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pnpm install --frozen-lockfile |         run: pnpm install --frozen-lockfile | ||||||
|  |       - uses: nrwl/nx-set-shas@v4 | ||||||
|       - name: Run the build |       - name: Run the build | ||||||
|         uses: ./.github/actions/build-electron |         uses: ./.github/actions/build-electron | ||||||
|         with: |         with: | ||||||
| @@ -70,7 +58,6 @@ jobs: | |||||||
|           APPLE_ID: ${{ secrets.APPLE_ID }} |           APPLE_ID: ${{ secrets.APPLE_ID }} | ||||||
|           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} |           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | ||||||
|           WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} |           WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} | ||||||
|           GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }} |  | ||||||
|  |  | ||||||
|       - name: Upload the artifact |       - name: Upload the artifact | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v4 | ||||||
| @@ -91,7 +78,7 @@ jobs: | |||||||
|             runs-on: ubuntu-24.04-arm |             runs-on: ubuntu-24.04-arm | ||||||
|     runs-on: ${{ matrix.runs-on }} |     runs-on: ${{ matrix.runs-on }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|       - name: Run the build |       - name: Run the build | ||||||
|         uses: ./.github/actions/build-server |         uses: ./.github/actions/build-server | ||||||
| @@ -114,20 +101,20 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - run: mkdir upload |       - run: mkdir upload | ||||||
|  |  | ||||||
|       - uses: actions/checkout@v5 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
|           sparse-checkout: | |           sparse-checkout: | | ||||||
|             docs/Release Notes |             docs/Release Notes | ||||||
|  |  | ||||||
|       - name: Download all artifacts |       - name: Download all artifacts | ||||||
|         uses: actions/download-artifact@v5 |         uses: actions/download-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           merge-multiple: true |           merge-multiple: true | ||||||
|           pattern: release-* |           pattern: release-* | ||||||
|           path: upload |           path: upload | ||||||
|  |  | ||||||
|       - name: Publish stable release |       - name: Publish stable release | ||||||
|         uses: softprops/action-gh-release@v2.3.4 |         uses: softprops/action-gh-release@v2.3.2 | ||||||
|         with: |         with: | ||||||
|           draft: false |           draft: false | ||||||
|           body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md |           body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								.github/workflows/unblock_signing.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/unblock_signing.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +0,0 @@ | |||||||
| name: Unblock signing |  | ||||||
| on: |  | ||||||
|   workflow_dispatch: |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   unblock-win-signing: |  | ||||||
|     runs-on: win-signing |  | ||||||
|     steps: |  | ||||||
|       - run: | |  | ||||||
|           cat ${{ vars.WINDOWS_SIGN_ERROR_LOG }} |  | ||||||
|           rm ${{ vars.WINDOWS_SIGN_ERROR_LOG }} |  | ||||||
							
								
								
									
										48
									
								
								.github/workflows/website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/website.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,48 +0,0 @@ | |||||||
| name: Deploy website |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|     paths: |  | ||||||
|       - "apps/website/**" |  | ||||||
|  |  | ||||||
|   pull_request: |  | ||||||
|     paths: |  | ||||||
|       - "apps/website/**" |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   build-and-deploy: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     name: Build & deploy website |  | ||||||
|  |  | ||||||
|     permissions: |  | ||||||
|       contents: read |  | ||||||
|       deployments: write |  | ||||||
|       pull-requests: write # For PR preview comments |  | ||||||
|  |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v5 |  | ||||||
|       - uses: pnpm/action-setup@v4 |  | ||||||
|       - name: Set up node & dependencies |  | ||||||
|         uses: actions/setup-node@v5 |  | ||||||
|         with: |  | ||||||
|           node-version: 22 |  | ||||||
|           cache: "pnpm" |  | ||||||
|  |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: pnpm install --filter website --frozen-lockfile |  | ||||||
|  |  | ||||||
|       - name: Build the website |  | ||||||
|         run: pnpm website:build |  | ||||||
|  |  | ||||||
|       - name: Deploy |  | ||||||
|         uses: ./.github/actions/deploy-to-cloudflare-pages |  | ||||||
|         with: |  | ||||||
|           project_name: "trilium-homepage" |  | ||||||
|           comment_body: "📚 Website preview is ready" |  | ||||||
|           production_url: "https://triliumnotes.org" |  | ||||||
|           deploy_dir: "apps/website/dist" |  | ||||||
|           cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }} |  | ||||||
|           cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |  | ||||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,4 @@ | |||||||
| # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. | # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. | ||||||
| /.cache |  | ||||||
|  |  | ||||||
| # compiled output | # compiled output | ||||||
| dist | dist | ||||||
| @@ -11,7 +10,6 @@ node_modules | |||||||
|  |  | ||||||
| # IDEs and editors | # IDEs and editors | ||||||
| /.idea | /.idea | ||||||
| .idea |  | ||||||
| .project | .project | ||||||
| .classpath | .classpath | ||||||
| .c9/ | .c9/ | ||||||
| @@ -33,11 +31,14 @@ testem.log | |||||||
| .DS_Store | .DS_Store | ||||||
| Thumbs.db | Thumbs.db | ||||||
|  |  | ||||||
|  | .nx/cache | ||||||
|  | .nx/workspace-data | ||||||
|  |  | ||||||
| vite.config.*.timestamp* | vite.config.*.timestamp* | ||||||
| vitest.config.*.timestamp* | vitest.config.*.timestamp* | ||||||
| test-output | test-output | ||||||
|  |  | ||||||
| apps/*/data* | apps/*/data | ||||||
| apps/*/out | apps/*/out | ||||||
| upload | upload | ||||||
|  |  | ||||||
| @@ -46,6 +47,3 @@ upload | |||||||
|  |  | ||||||
| /result | /result | ||||||
| .svelte-kit | .svelte-kit | ||||||
|  |  | ||||||
| # docs |  | ||||||
| site/ |  | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | # Default ignored files | ||||||
|  | /workspace.xml | ||||||
|  |  | ||||||
|  | # Datasource local storage ignored files | ||||||
|  | /dataSources.local.xml | ||||||
|  | /dataSources/ | ||||||
							
								
								
									
										15
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <component name="ProjectCodeStyleConfiguration"> | ||||||
|  |   <code_scheme name="Project" version="173"> | ||||||
|  |     <option name="OTHER_INDENT_OPTIONS"> | ||||||
|  |       <value> | ||||||
|  |         <option name="INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="TAB_SIZE" value="2" /> | ||||||
|  |       </value> | ||||||
|  |     </option> | ||||||
|  |     <codeStyleSettings language="JSON"> | ||||||
|  |       <indentOptions> | ||||||
|  |         <option name="INDENT_SIZE" value="4" /> | ||||||
|  |       </indentOptions> | ||||||
|  |     </codeStyleSettings> | ||||||
|  |   </code_scheme> | ||||||
|  | </component> | ||||||
							
								
								
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <component name="ProjectCodeStyleConfiguration"> | ||||||
|  |   <state> | ||||||
|  |     <option name="USE_PER_PROJECT_SETTINGS" value="true" /> | ||||||
|  |   </state> | ||||||
|  | </component> | ||||||
							
								
								
									
										12
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> | ||||||
|  |     <data-source source="LOCAL" name="document.db" uuid="2a4ac1e6-b828-4a2a-8e4a-3f59f10aff26"> | ||||||
|  |       <driver-ref>sqlite.xerial</driver-ref> | ||||||
|  |       <synchronize>true</synchronize> | ||||||
|  |       <jdbc-driver>org.sqlite.JDBC</jdbc-driver> | ||||||
|  |       <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data/document.db</jdbc-url> | ||||||
|  |       <working-dir>$ProjectFileDir$</working-dir> | ||||||
|  |     </data-source> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										4
									
								
								.idea/encodings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.idea/encodings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="Encoding" addBOMForNewFiles="with NO BOM" /> | ||||||
|  | </project> | ||||||
							
								
								
									
										15
									
								
								.idea/git_toolbox_prj.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.idea/git_toolbox_prj.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="GitToolBoxProjectSettings"> | ||||||
|  |     <option name="commitMessageIssueKeyValidationOverride"> | ||||||
|  |       <BoolValueOverride> | ||||||
|  |         <option name="enabled" value="true" /> | ||||||
|  |       </BoolValueOverride> | ||||||
|  |     </option> | ||||||
|  |     <option name="commitMessageValidationEnabledOverride"> | ||||||
|  |       <BoolValueOverride> | ||||||
|  |         <option name="enabled" value="true" /> | ||||||
|  |       </BoolValueOverride> | ||||||
|  |     </option> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										11
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <profile version="1.0"> | ||||||
|  |     <option name="myName" value="Project Default" /> | ||||||
|  |     <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> | ||||||
|  |       <option name="processCode" value="true" /> | ||||||
|  |       <option name="processLiterals" value="true" /> | ||||||
|  |       <option name="processComments" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |   </profile> | ||||||
|  | </component> | ||||||
							
								
								
									
										6
									
								
								.idea/jsLibraryMappings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/jsLibraryMappings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="JavaScriptLibraryMappings"> | ||||||
|  |     <includedPredefinedLibrary name="Node.js Core" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="JSLintConfiguration"> | ||||||
|  |     <option devel="true" /> | ||||||
|  |     <option es6="true" /> | ||||||
|  |     <option maxerr="50" /> | ||||||
|  |     <option node="true" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <project version="4"> | ||||||
|  |   <component name="JavaScriptSettings"> | ||||||
|  |     <option name="languageLevel" value="ES6" /> | ||||||
|  |   </component> | ||||||
|  |   <component name="ProjectRootManager" version="2" languageLevel="JDK_16" default="true" project-jdk-name="openjdk-16" project-jdk-type="JavaSDK"> | ||||||
|  |     <output url="file://$PROJECT_DIR$/out" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/trilium.iml" filepath="$PROJECT_DIR$/trilium.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										7
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="SqlDialectMappings"> | ||||||
|  |     <file url="file://$PROJECT_DIR$" dialect="SQLite" /> | ||||||
|  |     <file url="PROJECT" dialect="SQLite" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="VcsDirectoryMappings"> | ||||||
|  |     <mapping directory="" vcs="Git" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										4
									
								
								.mailmap
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.mailmap
									
									
									
									
									
								
							| @@ -1,2 +1,2 @@ | |||||||
| zadam <adam.zivner@gmail.com> | Adam Zivner <adam.zivner@gmail.com> | ||||||
| zadam <zadam.apps@gmail.com> | Adam Zivner <zadam.apps@gmail.com> | ||||||
							
								
								
									
										1
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,7 @@ | |||||||
|     "lokalise.i18n-ally", |     "lokalise.i18n-ally", | ||||||
|     "ms-azuretools.vscode-docker", |     "ms-azuretools.vscode-docker", | ||||||
|     "ms-playwright.playwright", |     "ms-playwright.playwright", | ||||||
|  |     "nrwl.angular-console", | ||||||
|     "redhat.vscode-yaml", |     "redhat.vscode-yaml", | ||||||
|     "tobermory.es6-string-html", |     "tobermory.es6-string-html", | ||||||
|     "vitest.explorer", |     "vitest.explorer", | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.vscode/i18n-ally-custom-framework.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/i18n-ally-custom-framework.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,6 @@ | |||||||
| languageIds: | languageIds: | ||||||
|   - javascript |   - javascript | ||||||
|   - typescript |   - typescript | ||||||
|   - typescriptreact |  | ||||||
|   - html |   - html | ||||||
|  |  | ||||||
| # An array of RegExes to find the key usage. **The key should be captured in the first match group**. | # An array of RegExes to find the key usage. **The key should be captured in the first match group**. | ||||||
| @@ -26,7 +25,6 @@ scopeRangeRegex: "useTranslation\\(\\s*\\[?\\s*['\"`](.*?)['\"`]" | |||||||
| # The "$1" will be replaced by the keypath specified. | # The "$1" will be replaced by the keypath specified. | ||||||
| refactorTemplates: | refactorTemplates: | ||||||
|   - t("$1") |   - t("$1") | ||||||
|   - {t("$1")} |  | ||||||
|   - ${t("$1")} |   - ${t("$1")} | ||||||
|   - <%= t("$1") %> |   - <%= t("$1") %> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -28,12 +28,5 @@ | |||||||
|     "typescript.validate.enable": true, |     "typescript.validate.enable": true, | ||||||
|     "typescript.tsserver.experimental.enableProjectDiagnostics": true, |     "typescript.tsserver.experimental.enableProjectDiagnostics": true, | ||||||
|     "typescript.tsdk": "node_modules/typescript/lib", |     "typescript.tsdk": "node_modules/typescript/lib", | ||||||
|     "typescript.enablePromptUseWorkspaceTsdk": true, |     "typescript.enablePromptUseWorkspaceTsdk": true | ||||||
|     "search.exclude": { |  | ||||||
|         "**/node_modules": true, |  | ||||||
|         "docs/**/*.html": true, |  | ||||||
|         "docs/**/*.png": true, |  | ||||||
|         "apps/server/src/assets/doc_notes/**": true, |  | ||||||
|         "apps/edit-docs/demo/**": true |  | ||||||
|     } |  | ||||||
| } | } | ||||||
							
								
								
									
										5
									
								
								.vscode/snippets.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.vscode/snippets.code-snippets
									
									
									
									
										vendored
									
									
								
							| @@ -20,10 +20,5 @@ | |||||||
|         "scope": "typescript", |         "scope": "typescript", | ||||||
|         "prefix": "jqf", |         "prefix": "jqf", | ||||||
|         "body": ["private $${1:name}!: JQuery<HTMLElement>;"] |         "body": ["private $${1:name}!: JQuery<HTMLElement>;"] | ||||||
|     }, |  | ||||||
|     "region": { |  | ||||||
|         "scope": "css", |  | ||||||
|         "prefix": "region", |  | ||||||
|         "body": ["/* #region ${1:name} */\n$0\n/* #endregion */"] |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										152
									
								
								CLAUDE.md
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								CLAUDE.md
									
									
									
									
									
								
							| @@ -1,152 +0,0 @@ | |||||||
| # CLAUDE.md |  | ||||||
|  |  | ||||||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |  | ||||||
|  |  | ||||||
| ## Overview |  | ||||||
|  |  | ||||||
| Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages. |  | ||||||
|  |  | ||||||
| ## Development Commands |  | ||||||
|  |  | ||||||
| ### Setup |  | ||||||
| - `pnpm install` - Install all dependencies |  | ||||||
| - `corepack enable` - Enable pnpm if not available |  | ||||||
|  |  | ||||||
| ### Running Applications |  | ||||||
| - `pnpm run server:start` - Start development server (http://localhost:8080) |  | ||||||
| - `pnpm run server:start-prod` - Run server in production mode |  | ||||||
|  |  | ||||||
| ### Building |  | ||||||
| - `pnpm run client:build` - Build client application |  | ||||||
| - `pnpm run server:build` - Build server application |  | ||||||
| - `pnpm run electron:build` - Build desktop application |  | ||||||
|  |  | ||||||
| ### Testing |  | ||||||
| - `pnpm test:all` - Run all tests (parallel + sequential) |  | ||||||
| - `pnpm test:parallel` - Run tests that can run in parallel |  | ||||||
| - `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math) |  | ||||||
| - `pnpm coverage` - Generate coverage reports |  | ||||||
|  |  | ||||||
| ## Architecture Overview |  | ||||||
|  |  | ||||||
| ### Monorepo Structure |  | ||||||
| - **apps/**: Runnable applications |  | ||||||
|   - `client/` - Frontend application (shared by server and desktop) |  | ||||||
|   - `server/` - Node.js server with web interface |  | ||||||
|   - `desktop/` - Electron desktop application |  | ||||||
|   - `web-clipper/` - Browser extension for saving web content |  | ||||||
|   - Additional tools: `db-compare`, `dump-db`, `edit-docs` |  | ||||||
|  |  | ||||||
| - **packages/**: Shared libraries |  | ||||||
|   - `commons/` - Shared interfaces and utilities |  | ||||||
|   - `ckeditor5/` - Custom rich text editor with Trilium-specific plugins |  | ||||||
|   - `codemirror/` - Code editor customizations |  | ||||||
|   - `highlightjs/` - Syntax highlighting |  | ||||||
|   - Custom CKEditor plugins: `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` |  | ||||||
|  |  | ||||||
| ### Core Architecture Patterns |  | ||||||
|  |  | ||||||
| #### Three-Layer Cache System |  | ||||||
| - **Becca** (Backend Cache): Server-side entity cache (`apps/server/src/becca/`) |  | ||||||
| - **Froca** (Frontend Cache): Client-side mirror of backend data (`apps/client/src/services/froca.ts`) |  | ||||||
| - **Shaca** (Share Cache): Optimized cache for shared/published notes (`apps/server/src/share/`) |  | ||||||
|  |  | ||||||
| #### Entity System |  | ||||||
| Core entities are defined in `apps/server/src/becca/entities/`: |  | ||||||
| - `BNote` - Notes with content and metadata |  | ||||||
| - `BBranch` - Hierarchical relationships between notes (allows multiple parents) |  | ||||||
| - `BAttribute` - Key-value metadata attached to notes |  | ||||||
| - `BRevision` - Note version history |  | ||||||
| - `BOption` - Application configuration |  | ||||||
|  |  | ||||||
| #### Widget-Based UI |  | ||||||
| Frontend uses a widget system (`apps/client/src/widgets/`): |  | ||||||
| - `BasicWidget` - Base class for all UI components |  | ||||||
| - `NoteContextAwareWidget` - Widgets that respond to note changes |  | ||||||
| - `RightPanelWidget` - Widgets displayed in the right panel |  | ||||||
| - Type-specific widgets in `type_widgets/` directory |  | ||||||
|  |  | ||||||
| #### API Architecture |  | ||||||
| - **Internal API**: REST endpoints in `apps/server/src/routes/api/` |  | ||||||
| - **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`) |  | ||||||
| - **WebSocket**: Real-time synchronization (`apps/server/src/services/ws.ts`) |  | ||||||
|  |  | ||||||
| ### Key Files for Understanding Architecture |  | ||||||
|  |  | ||||||
| 1. **Application Entry Points**: |  | ||||||
|    - `apps/server/src/main.ts` - Server startup |  | ||||||
|    - `apps/client/src/desktop.ts` - Client initialization |  | ||||||
|  |  | ||||||
| 2. **Core Services**: |  | ||||||
|    - `apps/server/src/becca/becca.ts` - Backend data management |  | ||||||
|    - `apps/client/src/services/froca.ts` - Frontend data synchronization |  | ||||||
|    - `apps/server/src/services/backend_script_api.ts` - Scripting API |  | ||||||
|  |  | ||||||
| 3. **Database Schema**: |  | ||||||
|    - `apps/server/src/assets/db/schema.sql` - Core database structure |  | ||||||
|  |  | ||||||
| 4. **Configuration**: |  | ||||||
|    - `package.json` - Project dependencies and scripts |  | ||||||
|  |  | ||||||
| ## Note Types and Features |  | ||||||
|  |  | ||||||
| Trilium supports multiple note types, each with specialized widgets: |  | ||||||
| - **Text**: Rich text with CKEditor5 (markdown import/export) |  | ||||||
| - **Code**: Syntax-highlighted code editing with CodeMirror |  | ||||||
| - **File**: Binary file attachments |  | ||||||
| - **Image**: Image display with editing capabilities |  | ||||||
| - **Canvas**: Drawing/diagramming with Excalidraw |  | ||||||
| - **Mermaid**: Diagram generation |  | ||||||
| - **Relation Map**: Visual note relationship mapping |  | ||||||
| - **Web View**: Embedded web pages |  | ||||||
| - **Doc/Book**: Hierarchical documentation structure |  | ||||||
|  |  | ||||||
| ## Development Guidelines |  | ||||||
|  |  | ||||||
| ### Testing Strategy |  | ||||||
| - Server tests run sequentially due to shared database |  | ||||||
| - Client tests can run in parallel |  | ||||||
| - E2E tests use Playwright for both server and desktop apps |  | ||||||
| - Build validation tests check artifact integrity |  | ||||||
|  |  | ||||||
| ### Scripting System |  | ||||||
| Trilium provides powerful user scripting capabilities: |  | ||||||
| - Frontend scripts run in browser context |  | ||||||
| - Backend scripts run in Node.js context with full API access |  | ||||||
| - Script API documentation available in `docs/Script API/` |  | ||||||
|  |  | ||||||
| ### Internationalization |  | ||||||
| - Translation files in `apps/client/src/translations/` |  | ||||||
| - Supported languages: English, German, Spanish, French, Romanian, Chinese |  | ||||||
|  |  | ||||||
| ### Security Considerations |  | ||||||
| - Per-note encryption with granular protected sessions |  | ||||||
| - CSRF protection for API endpoints |  | ||||||
| - OpenID and TOTP authentication support |  | ||||||
| - Sanitization of user-generated content |  | ||||||
|  |  | ||||||
| ## Common Development Tasks |  | ||||||
|  |  | ||||||
| ### Adding New Note Types |  | ||||||
| 1. Create widget in `apps/client/src/widgets/type_widgets/` |  | ||||||
| 2. Register in `apps/client/src/services/note_types.ts` |  | ||||||
| 3. Add backend handling in `apps/server/src/services/notes.ts` |  | ||||||
|  |  | ||||||
| ### Extending Search |  | ||||||
| - Search expressions handled in `apps/server/src/services/search/` |  | ||||||
| - Add new search operators in search context files |  | ||||||
|  |  | ||||||
| ### Custom CKEditor Plugins |  | ||||||
| - Create new package in `packages/` following existing plugin structure |  | ||||||
| - Register in `packages/ckeditor5/src/plugins.ts` |  | ||||||
|  |  | ||||||
| ### Database Migrations |  | ||||||
| - Add migration scripts in `apps/server/src/migrations/` |  | ||||||
| - Update schema in `apps/server/src/assets/db/schema.sql` |  | ||||||
|  |  | ||||||
| ## Build System Notes |  | ||||||
| - Uses pnpm for monorepo management |  | ||||||
| - Vite for fast development builds |  | ||||||
| - ESBuild for production optimization |  | ||||||
| - pnpm workspaces for dependency management |  | ||||||
| - Docker support with multi-stage builds |  | ||||||
							
								
								
									
										126
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										126
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,11 +1,11 @@ | |||||||
| # Trilium Notes | # Trilium Notes | ||||||
|  |  | ||||||
|     |  | ||||||
|  |  | ||||||
|    |  | ||||||
| [](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/) | [](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) | ||||||
|  |  | ||||||
| [English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md) | [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) | [Spanish](./docs/README-es.md) | [English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md) | ||||||
|  |  | ||||||
| Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. | Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. | ||||||
|  |  | ||||||
| @@ -13,23 +13,6 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q | |||||||
|  |  | ||||||
| <a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a> | <a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a> | ||||||
|  |  | ||||||
| ## 📚 Documentation |  | ||||||
|  |  | ||||||
| **Visit our comprehensive documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)** |  | ||||||
|  |  | ||||||
| Our documentation is available in multiple formats: |  | ||||||
| - **Online Documentation**: Browse the full documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/) |  | ||||||
| - **In-App Help**: Press `F1` within Trilium to access the same documentation directly in the application |  | ||||||
| - **GitHub**: Navigate through the [User Guide](./docs/User%20Guide/User%20Guide/) in this repository |  | ||||||
|  |  | ||||||
| ### Quick Links |  | ||||||
| - [Getting Started Guide](https://docs.triliumnotes.org/) |  | ||||||
| - [Installation Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md) |  | ||||||
| - [Docker Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md) |  | ||||||
| - [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md) |  | ||||||
| - [Basic Concepts and Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md) |  | ||||||
| - [Patterns of Personal Knowledge Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge) |  | ||||||
|  |  | ||||||
| ## 🎁 Features | ## 🎁 Features | ||||||
|  |  | ||||||
| * Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes)) | * Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes)) | ||||||
| @@ -63,15 +46,28 @@ Our documentation is available in multiple formats: | |||||||
| - [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more. | - [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more. | ||||||
| - [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more. | - [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more. | ||||||
|  |  | ||||||
| ## ❓Why TriliumNext? | ## ⚠️ Why TriliumNext? | ||||||
|  |  | ||||||
| The original Trilium developer ([Zadam](https://github.com/zadam)) has graciously given the Trilium repository to the community project which resides at https://github.com/TriliumNext | [The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620). | ||||||
|  |  | ||||||
| ### ⬆️Migrating from Zadam/Trilium? | ### Migrating from Trilium? | ||||||
|  |  | ||||||
| There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database. | There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database. | ||||||
|  |  | ||||||
| Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration. | Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented. | ||||||
|  |  | ||||||
|  | ## 📖 Documentation | ||||||
|  |  | ||||||
|  | We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation.  | ||||||
|  |  | ||||||
|  | Below are some quick links for your convenience to navigate the documentation: | ||||||
|  | - [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md) | ||||||
|  |   - [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md) | ||||||
|  | - [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md) | ||||||
|  | - [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md) | ||||||
|  | - [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge) | ||||||
|  |  | ||||||
|  | Until we finish reorganizing the documentation, you may also want to [browse the old documentation](https://triliumnext.github.io/Docs). | ||||||
|  |  | ||||||
| ## 💬 Discuss with us | ## 💬 Discuss with us | ||||||
|  |  | ||||||
| @@ -79,14 +75,14 @@ Feel free to join our official conversations. We would love to hear what feature | |||||||
|  |  | ||||||
| - [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.) | - [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.) | ||||||
|   - The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join) |   - The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join) | ||||||
| - [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.) | - [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.) | ||||||
| - [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.) | - [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.) | ||||||
|  |  | ||||||
| ## 🏗 Installation | ## 🏗 Installation | ||||||
|  |  | ||||||
| ### Windows / MacOS | ### Windows / MacOS | ||||||
|  |  | ||||||
| Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package and run the `trilium` executable. | Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable. | ||||||
|  |  | ||||||
| ### Linux | ### Linux | ||||||
|  |  | ||||||
| @@ -94,7 +90,7 @@ If your distribution is listed in the table below, use your distribution's packa | |||||||
|  |  | ||||||
| [](https://repology.org/project/triliumnext/versions) | [](https://repology.org/project/triliumnext/versions) | ||||||
|  |  | ||||||
| You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package and run the `trilium` executable. | You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable. | ||||||
|  |  | ||||||
| TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. | TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. | ||||||
|  |  | ||||||
| @@ -108,33 +104,23 @@ Currently only the latest versions of Chrome & Firefox are supported (and tested | |||||||
|  |  | ||||||
| To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below). | To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below). | ||||||
|  |  | ||||||
| See issue https://github.com/TriliumNext/Trilium/issues/4962 for more information on mobile app support. | If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid). | ||||||
|  |  | ||||||
| If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). | See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support. | ||||||
| Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid). |  | ||||||
| Note: It is best to disable automatic updates on your server installation (see below) when using TriliumDroid since the sync version must match between Trilium and TriliumDroid. |  | ||||||
|  |  | ||||||
| ### Server | ### Server | ||||||
|  |  | ||||||
| To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). | To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). | ||||||
|  |  | ||||||
|  |  | ||||||
| ## 💻 Contribute | ## 💻 Contribute | ||||||
|  |  | ||||||
| ### Translations |  | ||||||
|  |  | ||||||
| If you are a native speaker, help us translate Trilium by heading over to our [Weblate page](https://hosted.weblate.org/engage/trilium/). |  | ||||||
|  |  | ||||||
| Here's the language coverage we have so far: |  | ||||||
|  |  | ||||||
| [](https://hosted.weblate.org/engage/trilium/) |  | ||||||
|  |  | ||||||
| ### Code | ### Code | ||||||
|  |  | ||||||
| Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080): | Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080): | ||||||
| ```shell | ```shell | ||||||
| git clone https://github.com/TriliumNext/Trilium.git | git clone https://github.com/TriliumNext/Notes.git | ||||||
| cd Trilium | cd Notes | ||||||
| pnpm install | pnpm install | ||||||
| pnpm run server:start | pnpm run server:start | ||||||
| ``` | ``` | ||||||
| @@ -143,57 +129,39 @@ pnpm run server:start | |||||||
|  |  | ||||||
| Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation: | Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation: | ||||||
| ```shell | ```shell | ||||||
| git clone https://github.com/TriliumNext/Trilium.git | git clone https://github.com/TriliumNext/Notes.git | ||||||
| cd Trilium | cd Notes | ||||||
| pnpm install | pnpm install | ||||||
| pnpm edit-docs:edit-docs | pnpm nx run edit-docs:edit-docs | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Building the Executable | ### Building the Executable | ||||||
| Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: | Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: | ||||||
| ```shell | ```shell | ||||||
| git clone https://github.com/TriliumNext/Trilium.git | git clone https://github.com/TriliumNext/Notes.git | ||||||
| cd Trilium | cd Notes | ||||||
| pnpm install | pnpm install | ||||||
| pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32 | pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide). | For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md). | ||||||
|  |  | ||||||
| ### Developer Documentation | ### Developer Documentation | ||||||
|  |  | ||||||
| Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above. | Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above. | ||||||
|  |  | ||||||
| ## 👏 Shoutouts | ## 👏 Shoutouts | ||||||
|  |  | ||||||
| * [zadam](https://github.com/zadam) for the original concept and implementation of the application. | * [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team | ||||||
| * [Larsa](https://github.com/LarsaSara) for designing the application icon. | * [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it. | ||||||
| * [nriver](https://github.com/nriver) for his work on internationalization. | * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages | ||||||
| * [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas. | * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map) | ||||||
| * [antoniotejada](https://github.com/nriver) for the original syntax highlight widget. |  | ||||||
| * [Dosu](https://dosu.dev/) for providing us with the automated responses to GitHub issues and discussions. |  | ||||||
| * [Tabler Icons](https://tabler.io/icons) for the system tray icons. |  | ||||||
|  |  | ||||||
| Trilium would not be possible without the technologies behind it: |  | ||||||
|  |  | ||||||
| * [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind text notes. We are grateful for being offered a set of the premium features. |  | ||||||
| * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages. |  | ||||||
| * [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite whiteboard used in Canvas notes. |  | ||||||
| * [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the mind map functionality. |  | ||||||
| * [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps. |  | ||||||
| * [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections. |  | ||||||
| * [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library without real competition.  |  | ||||||
| * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map) |  | ||||||
|  |  | ||||||
| ## 🤝 Support | ## 🤝 Support | ||||||
|  |  | ||||||
| Trilium is built and maintained with [hundreds of hours of work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your support keeps it open-source, improves features, and covers costs such as hosting. | Support for the TriliumNext organization will be possible in the near future. For now, you can: | ||||||
|  | - Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list) | ||||||
| Consider supporting the main developer ([eliandoran](https://github.com/eliandoran)) of the application via: | - Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2). | ||||||
|  |  | ||||||
| - [GitHub Sponsors](https://github.com/sponsors/eliandoran) |  | ||||||
| - [PayPal](https://paypal.me/eliandoran) |  | ||||||
| - [Buy Me a Coffee](https://buymeacoffee.com/eliandoran) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## 🔑 License | ## 🔑 License | ||||||
|   | |||||||
| @@ -35,13 +35,13 @@ | |||||||
|     "chore:generate-openapi": "tsx bin/generate-openapi.js" |     "chore:generate-openapi": "tsx bin/generate-openapi.js" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": {     |   "devDependencies": {     | ||||||
|     "@playwright/test": "1.55.1", |     "@playwright/test": "1.53.2", | ||||||
|     "@stylistic/eslint-plugin": "5.4.0",         |     "@stylistic/eslint-plugin": "5.1.0",         | ||||||
|     "@types/express": "5.0.3",     |     "@types/express": "5.0.3",     | ||||||
|     "@types/node": "22.18.8",     |     "@types/node": "22.16.2",     | ||||||
|     "@types/yargs": "17.0.33", |     "@types/yargs": "17.0.33", | ||||||
|     "@vitest/coverage-v8": "3.2.4", |     "@vitest/coverage-v8": "3.2.4", | ||||||
|     "eslint": "9.37.0", |     "eslint": "9.30.1", | ||||||
|     "eslint-plugin-simple-import-sort": "12.1.1", |     "eslint-plugin-simple-import-sort": "12.1.1", | ||||||
|     "esm": "3.2.25", |     "esm": "3.2.25", | ||||||
|     "jsdoc": "4.0.4", |     "jsdoc": "4.0.4", | ||||||
| @@ -49,8 +49,8 @@ | |||||||
|     "rcedit": "4.0.1", |     "rcedit": "4.0.1", | ||||||
|     "rimraf": "6.0.1",     |     "rimraf": "6.0.1",     | ||||||
|     "tslib": "2.8.1",     |     "tslib": "2.8.1",     | ||||||
|     "typedoc": "0.28.13", |     "typedoc": "0.28.7", | ||||||
|     "typedoc-plugin-missing-exports": "4.1.0" |     "typedoc-plugin-missing-exports": "4.0.0" | ||||||
|   }, |   }, | ||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "appdmg": "0.6.6" |     "appdmg": "0.6.6" | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| # The development license key for premium CKEditor features. | # The development license key for premium CKEditor features. | ||||||
| # Note: This key must only be used for the Trilium Notes project. | # Note: This key must only be used for the Trilium Notes project. | ||||||
| VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3ODcyNzA0MDAsImp0aSI6IjkyMWE1MWNlLTliNDMtNGRlMC1iOTQwLTc5ZjM2MDBkYjg1NyIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOiJ0cmlsaXVtIiwiZmVhdHVyZXMiOlsiVFJJTElVTSJdLCJ2YyI6ImU4YzRhMjBkIn0.hny77p-U4-jTkoqbwPytrEar5ylGCWBN7Ez3SlB8i6_mJCBIeCSTOlVQk_JMiOEq3AGykUMHzWXzjdMFwgniOw | # Expires on: 2025-09-13 | ||||||
|  | VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w | ||||||
| VITE_CKEDITOR_ENABLE_INSPECTOR=false | VITE_CKEDITOR_ENABLE_INSPECTOR=false | ||||||
| @@ -1,30 +1,24 @@ | |||||||
| { | { | ||||||
|   "name": "@triliumnext/client", |   "name": "@triliumnext/client", | ||||||
|   "version": "0.99.1", |   "version": "0.96.0", | ||||||
|   "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", |   "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "author": { |   "author": { | ||||||
|     "name": "Trilium Notes Team", |     "name": "Trilium Notes Team", | ||||||
|     "email": "contact@eliandoran.me", |     "email": "contact@eliandoran.me", | ||||||
|     "url": "https://github.com/TriliumNext/Trilium" |     "url": "https://github.com/TriliumNext/Notes" | ||||||
|   }, |  | ||||||
|   "scripts": { |  | ||||||
|     "build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build", |  | ||||||
|     "test": "vitest", |  | ||||||
|     "circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular" |  | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@eslint/js": "9.37.0", |     "@eslint/js": "9.30.1", | ||||||
|     "@excalidraw/excalidraw": "0.18.0", |     "@excalidraw/excalidraw": "0.18.0", | ||||||
|     "@fullcalendar/core": "6.1.19", |     "@fullcalendar/core": "6.1.18", | ||||||
|     "@fullcalendar/daygrid": "6.1.19", |     "@fullcalendar/daygrid": "6.1.18", | ||||||
|     "@fullcalendar/interaction": "6.1.19", |     "@fullcalendar/interaction": "6.1.18", | ||||||
|     "@fullcalendar/list": "6.1.19", |     "@fullcalendar/list": "6.1.18", | ||||||
|     "@fullcalendar/multimonth": "6.1.19", |     "@fullcalendar/multimonth": "6.1.18", | ||||||
|     "@fullcalendar/timegrid": "6.1.19", |     "@fullcalendar/timegrid": "6.1.18", | ||||||
|     "@maplibre/maplibre-gl-leaflet": "0.1.3", |     "@mermaid-js/layout-elk": "0.1.8", | ||||||
|     "@mermaid-js/layout-elk": "0.2.0", |  | ||||||
|     "@mind-elixir/node-menu": "5.0.0", |     "@mind-elixir/node-menu": "5.0.0", | ||||||
|     "@popperjs/core": "2.11.8", |     "@popperjs/core": "2.11.8", | ||||||
|     "@triliumnext/ckeditor5": "workspace:*", |     "@triliumnext/ckeditor5": "workspace:*", | ||||||
| @@ -33,48 +27,60 @@ | |||||||
|     "@triliumnext/highlightjs": "workspace:*", |     "@triliumnext/highlightjs": "workspace:*", | ||||||
|     "@triliumnext/share-theme": "workspace:*", |     "@triliumnext/share-theme": "workspace:*", | ||||||
|     "autocomplete.js": "0.38.1", |     "autocomplete.js": "0.38.1", | ||||||
|     "bootstrap": "5.3.8", |     "bootstrap": "5.3.7", | ||||||
|     "boxicons": "2.1.4", |     "boxicons": "2.1.4", | ||||||
|     "dayjs": "1.11.18", |     "dayjs": "1.11.13", | ||||||
|     "dayjs-plugin-utc": "0.1.2", |     "dayjs-plugin-utc": "0.1.2", | ||||||
|     "debounce": "2.2.0", |     "debounce": "2.2.0", | ||||||
|     "draggabilly": "3.0.0", |     "draggabilly": "3.0.0", | ||||||
|     "force-graph": "1.51.0", |     "force-graph": "1.50.1", | ||||||
|     "globals": "16.4.0", |     "globals": "16.3.0", | ||||||
|     "i18next": "25.5.3", |     "i18next": "25.3.2", | ||||||
|     "i18next-http-backend": "3.0.2", |     "i18next-http-backend": "3.0.2", | ||||||
|     "jquery": "3.7.1", |     "jquery": "3.7.1", | ||||||
|  |     "jquery-hotkeys": "0.2.2", | ||||||
|     "jquery.fancytree": "2.38.5", |     "jquery.fancytree": "2.38.5", | ||||||
|     "jsplumb": "2.15.6", |     "jsplumb": "2.15.6", | ||||||
|     "katex": "0.16.23", |     "katex": "0.16.22", | ||||||
|     "knockout": "3.5.1", |     "knockout": "3.5.1", | ||||||
|     "leaflet": "1.9.4", |     "leaflet": "1.9.4", | ||||||
|     "leaflet-gpx": "2.2.0", |     "leaflet-gpx": "2.2.0", | ||||||
|     "mark.js": "8.11.1", |     "mark.js": "8.11.1", | ||||||
|     "marked": "16.3.0", |     "marked": "16.0.0", | ||||||
|     "mermaid": "11.12.0", |     "mermaid": "11.8.1", | ||||||
|     "mind-elixir": "5.1.1", |     "mind-elixir": "5.0.1", | ||||||
|     "normalize.css": "8.0.1", |     "normalize.css": "8.0.1", | ||||||
|     "panzoom": "9.4.3", |     "panzoom": "9.4.3", | ||||||
|     "preact": "10.27.2", |     "preact": "10.26.9", | ||||||
|     "react-i18next": "16.0.0", |  | ||||||
|     "split.js": "1.6.5", |     "split.js": "1.6.5", | ||||||
|     "svg-pan-zoom": "3.6.2", |     "svg-pan-zoom": "3.6.2", | ||||||
|     "tabulator-tables": "6.3.1", |     "tabulator-tables": "6.3.1", | ||||||
|     "vanilla-js-wheel-zoom": "9.0.4" |     "vanilla-js-wheel-zoom": "9.0.4" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@ckeditor/ckeditor5-inspector": "5.0.0", |     "@ckeditor/ckeditor5-inspector": "4.1.0", | ||||||
|     "@preact/preset-vite": "2.10.2", |  | ||||||
|     "@types/bootstrap": "5.2.10", |     "@types/bootstrap": "5.2.10", | ||||||
|     "@types/jquery": "3.5.33", |     "@types/jquery": "3.5.32", | ||||||
|     "@types/leaflet": "1.9.20", |     "@types/leaflet": "1.9.20", | ||||||
|     "@types/leaflet-gpx": "1.3.8", |     "@types/leaflet-gpx": "1.3.7", | ||||||
|     "@types/mark.js": "8.11.12", |     "@types/mark.js": "8.11.12", | ||||||
|     "@types/tabulator-tables": "6.2.11", |     "@types/tabulator-tables": "6.2.7", | ||||||
|     "copy-webpack-plugin": "13.0.1", |     "copy-webpack-plugin": "13.0.0", | ||||||
|     "happy-dom": "19.0.2", |     "happy-dom": "18.0.1", | ||||||
|     "script-loader": "0.7.2", |     "script-loader": "0.7.2", | ||||||
|     "vite-plugin-static-copy": "3.1.3" |     "vite-plugin-static-copy": "3.1.0" | ||||||
|  |   }, | ||||||
|  |   "nx": { | ||||||
|  |     "name": "client", | ||||||
|  |     "targets": { | ||||||
|  |       "serve": { | ||||||
|  |         "dependsOn": [ | ||||||
|  |           "^build" | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       "circular-deps": { | ||||||
|  |         "command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -7,9 +7,6 @@ | |||||||
|     "display": "standalone", |     "display": "standalone", | ||||||
|     "scope": "/", |     "scope": "/", | ||||||
|     "start_url": "/", |     "start_url": "/", | ||||||
|     "display_override": [ |  | ||||||
|         "window-controls-overlay" |  | ||||||
|     ], |  | ||||||
|     "icons": [ |     "icons": [ | ||||||
|         { |         { | ||||||
|             "src": "icon.png", |             "src": "icon.png", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import froca from "../services/froca.js"; | import froca from "../services/froca.js"; | ||||||
| import RootCommandExecutor from "./root_command_executor.js"; | import RootCommandExecutor from "./root_command_executor.js"; | ||||||
| import Entrypoints from "./entrypoints.js"; | import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; | ||||||
| import options from "../services/options.js"; | import options from "../services/options.js"; | ||||||
| import utils, { hasTouchBar } from "../services/utils.js"; | import utils, { hasTouchBar } from "../services/utils.js"; | ||||||
| import zoomComponent from "./zoom.js"; | import zoomComponent from "./zoom.js"; | ||||||
| @@ -28,17 +28,16 @@ import TouchBarComponent from "./touch_bar.js"; | |||||||
| import type { CKTextEditor } from "@triliumnext/ckeditor5"; | import type { CKTextEditor } from "@triliumnext/ckeditor5"; | ||||||
| import type CodeMirror from "@triliumnext/codemirror"; | import type CodeMirror from "@triliumnext/codemirror"; | ||||||
| import { StartupChecks } from "./startup_checks.js"; | import { StartupChecks } from "./startup_checks.js"; | ||||||
| import type { CreateNoteOpts } from "../services/note_create.js"; |  | ||||||
| import { ColumnComponent } from "tabulator-tables"; |  | ||||||
| import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx"; |  | ||||||
| import type RootContainer from "../widgets/containers/root_container.js"; |  | ||||||
| import { SqlExecuteResults } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| interface Layout { | interface Layout { | ||||||
|     getRootWidget: (appContext: AppContext) => RootContainer; |     getRootWidget: (appContext: AppContext) => RootWidget; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface BeforeUploadListener extends Component { | interface RootWidget extends Component { | ||||||
|  |     render: () => JQuery<HTMLElement>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface BeforeUploadListener extends Component { | ||||||
|     beforeUnloadEvent(): boolean; |     beforeUnloadEvent(): boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -83,6 +82,7 @@ export type CommandMappings = { | |||||||
|     focusTree: CommandData; |     focusTree: CommandData; | ||||||
|     focusOnTitle: CommandData; |     focusOnTitle: CommandData; | ||||||
|     focusOnDetail: CommandData; |     focusOnDetail: CommandData; | ||||||
|  |     focusOnSearchDefinition: Required<CommandData>; | ||||||
|     searchNotes: CommandData & { |     searchNotes: CommandData & { | ||||||
|         searchString?: string; |         searchString?: string; | ||||||
|         ancestorNoteId?: string | null; |         ancestorNoteId?: string | null; | ||||||
| @@ -90,14 +90,7 @@ export type CommandMappings = { | |||||||
|     closeTocCommand: CommandData; |     closeTocCommand: CommandData; | ||||||
|     closeHlt: CommandData; |     closeHlt: CommandData; | ||||||
|     showLaunchBarSubtree: CommandData; |     showLaunchBarSubtree: CommandData; | ||||||
|     showHiddenSubtree: CommandData; |     showRevisions: CommandData; | ||||||
|     showSQLConsoleHistory: CommandData; |  | ||||||
|     logout: CommandData; |  | ||||||
|     switchToMobileVersion: CommandData; |  | ||||||
|     switchToDesktopVersion: CommandData; |  | ||||||
|     showRevisions: CommandData & { |  | ||||||
|         noteId?: string | null; |  | ||||||
|     }; |  | ||||||
|     showLlmChat: CommandData; |     showLlmChat: CommandData; | ||||||
|     createAiChat: CommandData; |     createAiChat: CommandData; | ||||||
|     showOptions: CommandData & { |     showOptions: CommandData & { | ||||||
| @@ -116,7 +109,7 @@ export type CommandMappings = { | |||||||
|     openedFileUpdated: CommandData & { |     openedFileUpdated: CommandData & { | ||||||
|         entityType: string; |         entityType: string; | ||||||
|         entityId: string; |         entityId: string; | ||||||
|         lastModifiedMs?: number; |         lastModifiedMs: number; | ||||||
|         filePath: string; |         filePath: string; | ||||||
|     }; |     }; | ||||||
|     focusAndSelectTitle: CommandData & { |     focusAndSelectTitle: CommandData & { | ||||||
| @@ -138,9 +131,6 @@ export type CommandMappings = { | |||||||
|     hideLeftPane: CommandData; |     hideLeftPane: CommandData; | ||||||
|     showCpuArchWarning: CommandData; |     showCpuArchWarning: CommandData; | ||||||
|     showLeftPane: CommandData; |     showLeftPane: CommandData; | ||||||
|     showAttachments: CommandData; |  | ||||||
|     showSearchHistory: CommandData; |  | ||||||
|     showShareSubtree: CommandData; |  | ||||||
|     hoistNote: CommandData & { noteId: string }; |     hoistNote: CommandData & { noteId: string }; | ||||||
|     leaveProtectedSession: CommandData; |     leaveProtectedSession: CommandData; | ||||||
|     enterProtectedSession: CommandData; |     enterProtectedSession: CommandData; | ||||||
| @@ -181,7 +171,7 @@ export type CommandMappings = { | |||||||
|     deleteNotes: ContextMenuCommandData; |     deleteNotes: ContextMenuCommandData; | ||||||
|     importIntoNote: ContextMenuCommandData; |     importIntoNote: ContextMenuCommandData; | ||||||
|     exportNote: ContextMenuCommandData; |     exportNote: ContextMenuCommandData; | ||||||
|     searchInSubtree: CommandData & { notePath: string; }; |     searchInSubtree: ContextMenuCommandData; | ||||||
|     moveNoteUp: ContextMenuCommandData; |     moveNoteUp: ContextMenuCommandData; | ||||||
|     moveNoteDown: ContextMenuCommandData; |     moveNoteDown: ContextMenuCommandData; | ||||||
|     moveNoteUpInHierarchy: ContextMenuCommandData; |     moveNoteUpInHierarchy: ContextMenuCommandData; | ||||||
| @@ -270,74 +260,6 @@ export type CommandMappings = { | |||||||
|     closeThisNoteSplit: CommandData; |     closeThisNoteSplit: CommandData; | ||||||
|     moveThisNoteSplit: CommandData & { isMovingLeft: boolean }; |     moveThisNoteSplit: CommandData & { isMovingLeft: boolean }; | ||||||
|     jumpToNote: CommandData; |     jumpToNote: CommandData; | ||||||
|     commandPalette: CommandData; |  | ||||||
|  |  | ||||||
|     // Keyboard shortcuts |  | ||||||
|     backInNoteHistory: CommandData; |  | ||||||
|     forwardInNoteHistory: CommandData; |  | ||||||
|     forceSaveRevision: CommandData; |  | ||||||
|     scrollToActiveNote: CommandData; |  | ||||||
|     quickSearch: CommandData; |  | ||||||
|     collapseTree: CommandData; |  | ||||||
|     createNoteAfter: CommandData; |  | ||||||
|     createNoteInto: CommandData; |  | ||||||
|     addNoteAboveToSelection: CommandData; |  | ||||||
|     addNoteBelowToSelection: CommandData; |  | ||||||
|     openNewTab: CommandData; |  | ||||||
|     activateNextTab: CommandData; |  | ||||||
|     activatePreviousTab: CommandData; |  | ||||||
|     openNewWindow: CommandData; |  | ||||||
|     toggleTray: CommandData; |  | ||||||
|     firstTab: CommandData; |  | ||||||
|     secondTab: CommandData; |  | ||||||
|     thirdTab: CommandData; |  | ||||||
|     fourthTab: CommandData; |  | ||||||
|     fifthTab: CommandData; |  | ||||||
|     sixthTab: CommandData; |  | ||||||
|     seventhTab: CommandData; |  | ||||||
|     eigthTab: CommandData; |  | ||||||
|     ninthTab: CommandData; |  | ||||||
|     lastTab: CommandData; |  | ||||||
|     showNoteSource: CommandData; |  | ||||||
|     showSQLConsole: CommandData; |  | ||||||
|     showBackendLog: CommandData; |  | ||||||
|     showCheatsheet: CommandData; |  | ||||||
|     showHelp: CommandData; |  | ||||||
|     addLinkToText: CommandData; |  | ||||||
|     followLinkUnderCursor: CommandData; |  | ||||||
|     insertDateTimeToText: CommandData; |  | ||||||
|     pasteMarkdownIntoText: CommandData; |  | ||||||
|     cutIntoNote: CommandData; |  | ||||||
|     addIncludeNoteToText: CommandData; |  | ||||||
|     editReadOnlyNote: CommandData; |  | ||||||
|     toggleRibbonTabClassicEditor: CommandData; |  | ||||||
|     toggleRibbonTabBasicProperties: CommandData; |  | ||||||
|     toggleRibbonTabBookProperties: CommandData; |  | ||||||
|     toggleRibbonTabFileProperties: CommandData; |  | ||||||
|     toggleRibbonTabImageProperties: CommandData; |  | ||||||
|     toggleRibbonTabOwnedAttributes: CommandData; |  | ||||||
|     toggleRibbonTabInheritedAttributes: CommandData; |  | ||||||
|     toggleRibbonTabPromotedAttributes: CommandData; |  | ||||||
|     toggleRibbonTabNoteMap: CommandData; |  | ||||||
|     toggleRibbonTabNoteInfo: CommandData; |  | ||||||
|     toggleRibbonTabNotePaths: CommandData; |  | ||||||
|     toggleRibbonTabSimilarNotes: CommandData; |  | ||||||
|     toggleRightPane: CommandData; |  | ||||||
|     printActiveNote: CommandData; |  | ||||||
|     exportAsPdf: CommandData; |  | ||||||
|     openNoteExternally: CommandData; |  | ||||||
|     openNoteCustom: CommandData; |  | ||||||
|     renderActiveNote: CommandData; |  | ||||||
|     unhoist: CommandData; |  | ||||||
|     reloadFrontendApp: CommandData; |  | ||||||
|     openDevTools: CommandData; |  | ||||||
|     findInText: CommandData; |  | ||||||
|     toggleLeftPane: CommandData; |  | ||||||
|     toggleFullscreen: CommandData; |  | ||||||
|     zoomOut: CommandData; |  | ||||||
|     zoomIn: CommandData; |  | ||||||
|     zoomReset: CommandData; |  | ||||||
|     copyWithoutFormatting: CommandData; |  | ||||||
|  |  | ||||||
|     // Geomap |     // Geomap | ||||||
|     deleteFromMap: { noteId: string }; |     deleteFromMap: { noteId: string }; | ||||||
| @@ -354,30 +276,12 @@ export type CommandMappings = { | |||||||
|  |  | ||||||
|     geoMapCreateChildNote: CommandData; |     geoMapCreateChildNote: CommandData; | ||||||
|  |  | ||||||
|     // Table view |  | ||||||
|     addNewRow: CommandData & { |  | ||||||
|         customOpts: CreateNoteOpts; |  | ||||||
|         parentNotePath?: string; |  | ||||||
|     }; |  | ||||||
|     addNewTableColumn: CommandData & { |  | ||||||
|         columnToEdit?: ColumnComponent; |  | ||||||
|         referenceColumn?: ColumnComponent; |  | ||||||
|         direction?: "before" | "after"; |  | ||||||
|         type?: "label" | "relation"; |  | ||||||
|     }; |  | ||||||
|     deleteTableColumn: CommandData & { |  | ||||||
|         columnToDelete?: ColumnComponent; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     buildTouchBar: CommandData & { |     buildTouchBar: CommandData & { | ||||||
|         TouchBar: typeof TouchBar; |         TouchBar: typeof TouchBar; | ||||||
|         buildIcon(name: string): NativeImage; |         buildIcon(name: string): NativeImage; | ||||||
|     }; |     }; | ||||||
|     refreshTouchBar: CommandData; |     refreshTouchBar: CommandData; | ||||||
|     reloadTextEditor: CommandData; |     reloadTextEditor: CommandData; | ||||||
|     chooseNoteType: CommandData & { |  | ||||||
|         callback: ChooseNoteTypeCallback |  | ||||||
|     } |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| type EventMappings = { | type EventMappings = { | ||||||
| @@ -530,7 +434,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp | |||||||
| export class AppContext extends Component { | export class AppContext extends Component { | ||||||
|     isMainWindow: boolean; |     isMainWindow: boolean; | ||||||
|     components: Component[]; |     components: Component[]; | ||||||
|     beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[]; |     beforeUnloadListeners: WeakRef<BeforeUploadListener>[]; | ||||||
|     tabManager!: TabManager; |     tabManager!: TabManager; | ||||||
|     layout?: Layout; |     layout?: Layout; | ||||||
|     noteTreeWidget?: NoteTreeWidget; |     noteTreeWidget?: NoteTreeWidget; | ||||||
| @@ -623,7 +527,7 @@ export class AppContext extends Component { | |||||||
|             component.triggerCommand(commandName, { $el: $(this) }); |             component.triggerCommand(commandName, { $el: $(this) }); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         this.child(rootWidget as Component); |         this.child(rootWidget); | ||||||
|  |  | ||||||
|         this.triggerEvent("initialRenderComplete", {}); |         this.triggerEvent("initialRenderComplete", {}); | ||||||
|     } |     } | ||||||
| @@ -650,20 +554,16 @@ export class AppContext extends Component { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     getComponentByEl(el: HTMLElement) { |     getComponentByEl(el: HTMLElement) { | ||||||
|         return $(el).closest("[data-component-id]").prop("component"); |         return $(el).closest(".component").prop("component"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) { |     addBeforeUnloadListener(obj: BeforeUploadListener) { | ||||||
|         if (typeof WeakRef !== "function") { |         if (typeof WeakRef !== "function") { | ||||||
|             // older browsers don't support WeakRef |             // older browsers don't support WeakRef | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (typeof obj === "object") { |         this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj)); | ||||||
|             this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj)); |  | ||||||
|         } else { |  | ||||||
|             this.beforeUnloadListeners.push(obj); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -673,29 +573,25 @@ const appContext = new AppContext(window.glob.isMainWindow); | |||||||
| $(window).on("beforeunload", () => { | $(window).on("beforeunload", () => { | ||||||
|     let allSaved = true; |     let allSaved = true; | ||||||
|  |  | ||||||
|     appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref()); |     appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref()); | ||||||
|  |  | ||||||
|     for (const listener of appContext.beforeUnloadListeners) { |     for (const weakRef of appContext.beforeUnloadListeners) { | ||||||
|         if (typeof listener === "object") { |         const component = weakRef.deref(); | ||||||
|             const component = listener.deref(); |  | ||||||
|  |  | ||||||
|             if (!component) { |         if (!component) { | ||||||
|                 continue; |             continue; | ||||||
|             } |         } | ||||||
|  |  | ||||||
|             if (!component.beforeUnloadEvent()) { |         if (!component.beforeUnloadEvent()) { | ||||||
|                 console.log(`Component ${component.componentId} is not finished saving its state.`); |             console.log(`Component ${component.componentId} is not finished saving its state.`); | ||||||
|                 allSaved = false; |  | ||||||
|             } |             toast.showMessage(t("app_context.please_wait_for_save"), 10000); | ||||||
|         } else { |  | ||||||
|             if (!listener()) { |             allSaved = false; | ||||||
|                 allSaved = false; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!allSaved) { |     if (!allSaved) { | ||||||
|         toast.showMessage(t("app_context.please_wait_for_save"), 10000); |  | ||||||
|         return "some string"; |         return "some string"; | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
| import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js"; | import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js"; | ||||||
|  |  | ||||||
| type EventHandler = ((data: any) => void); |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Abstract class for all components in the Trilium's frontend. |  * Abstract class for all components in the Trilium's frontend. | ||||||
|  * |  * | ||||||
| @@ -21,7 +19,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> { | |||||||
|     initialized: Promise<void> | null; |     initialized: Promise<void> | null; | ||||||
|     parent?: TypedComponent<any>; |     parent?: TypedComponent<any>; | ||||||
|     _position!: number; |     _position!: number; | ||||||
|     private listeners: Record<string, EventHandler[]> | null = {}; |  | ||||||
|  |  | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; |         this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; | ||||||
| @@ -79,14 +76,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> { | |||||||
|     handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null { |     handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null { | ||||||
|         const promises: Promise<unknown>[] = []; |         const promises: Promise<unknown>[] = []; | ||||||
|  |  | ||||||
|         // Handle React children. |  | ||||||
|         if (this.listeners?.[name]) { |  | ||||||
|             for (const listener of this.listeners[name]) { |  | ||||||
|                 listener(data); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Handle legacy children. |  | ||||||
|         for (const child of this.children) { |         for (const child of this.children) { | ||||||
|             const ret = child.handleEvent(name, data) as Promise<void>; |             const ret = child.handleEvent(name, data) as Promise<void>; | ||||||
|  |  | ||||||
| @@ -131,35 +120,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> { | |||||||
|  |  | ||||||
|         return promise; |         return promise; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     registerHandler<T extends EventNames>(name: T, handler: EventHandler) { |  | ||||||
|         if (!this.listeners) { |  | ||||||
|             this.listeners = {}; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!this.listeners[name]) { |  | ||||||
|             this.listeners[name] = []; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.listeners[name].includes(handler)) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.listeners[name].push(handler); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     removeHandler<T extends EventNames>(name: T, handler: EventHandler) { |  | ||||||
|         if (!this.listeners?.[name]?.includes(handler)) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.listeners[name] = this.listeners[name] |  | ||||||
|             .filter(listener => listener !== handler); |  | ||||||
|  |  | ||||||
|         if (!this.listeners[name].length) { |  | ||||||
|             delete this.listeners[name]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export default class Component extends TypedComponent<Component> {} | export default class Component extends TypedComponent<Component> {} | ||||||
|   | |||||||
| @@ -10,16 +10,38 @@ import bundleService from "../services/bundle.js"; | |||||||
| import froca from "../services/froca.js"; | import froca from "../services/froca.js"; | ||||||
| import linkService from "../services/link.js"; | import linkService from "../services/link.js"; | ||||||
| import { t } from "../services/i18n.js"; | import { t } from "../services/i18n.js"; | ||||||
| import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; | import type FNote from "../entities/fnote.js"; | ||||||
|  |  | ||||||
|  | // TODO: Move somewhere else nicer. | ||||||
|  | export type SqlExecuteResults = string[][][]; | ||||||
|  |  | ||||||
|  | // TODO: Deduplicate with server. | ||||||
|  | interface SqlExecuteResponse { | ||||||
|  |     success: boolean; | ||||||
|  |     error?: string; | ||||||
|  |     results: SqlExecuteResults; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TODO: Deduplicate with server. | ||||||
|  | interface CreateChildrenResponse { | ||||||
|  |     note: FNote; | ||||||
|  | } | ||||||
|  |  | ||||||
| export default class Entrypoints extends Component { | export default class Entrypoints extends Component { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
|  |  | ||||||
|  |         if (jQuery.hotkeys) { | ||||||
|  |             // hot keys are active also inside inputs and content editables | ||||||
|  |             jQuery.hotkeys.options.filterInputAcceptingElements = false; | ||||||
|  |             jQuery.hotkeys.options.filterContentEditable = false; | ||||||
|  |             jQuery.hotkeys.options.filterTextInputs = false; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     openDevToolsCommand() { |     openDevToolsCommand() { | ||||||
|         if (utils.isElectron()) { |         if (utils.isElectron()) { | ||||||
|             utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools(); |             utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -91,9 +113,7 @@ export default class Entrypoints extends Component { | |||||||
|             if (win.isFullScreenable()) { |             if (win.isFullScreenable()) { | ||||||
|                 win.setFullScreen(!win.isFullScreen()); |                 win.setFullScreen(!win.isFullScreen()); | ||||||
|             } |             } | ||||||
|         } else { |         } // outside of electron this is handled by the browser | ||||||
|             document.documentElement.requestFullscreen(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     reloadFrontendAppCommand() { |     reloadFrontendAppCommand() { | ||||||
| @@ -109,7 +129,7 @@ export default class Entrypoints extends Component { | |||||||
|         if (utils.isElectron()) { |         if (utils.isElectron()) { | ||||||
|             // standard JS version does not work completely correctly in electron |             // standard JS version does not work completely correctly in electron | ||||||
|             const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); |             const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); | ||||||
|             const activeIndex = webContents.navigationHistory.getActiveIndex(); |             const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex()); | ||||||
|  |  | ||||||
|             webContents.goToIndex(activeIndex - 1); |             webContents.goToIndex(activeIndex - 1); | ||||||
|         } else { |         } else { | ||||||
| @@ -121,7 +141,7 @@ export default class Entrypoints extends Component { | |||||||
|         if (utils.isElectron()) { |         if (utils.isElectron()) { | ||||||
|             // standard JS version does not work completely correctly in electron |             // standard JS version does not work completely correctly in electron | ||||||
|             const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); |             const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); | ||||||
|             const activeIndex = webContents.navigationHistory.getActiveIndex(); |             const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex()); | ||||||
|  |  | ||||||
|             webContents.goToIndex(activeIndex + 1); |             webContents.goToIndex(activeIndex + 1); | ||||||
|         } else { |         } else { | ||||||
|   | |||||||
| @@ -325,9 +325,8 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> | |||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Collections must always display a note list, even if no children. |         // Some book types must always display a note list, even if no children. | ||||||
|         const viewType = note.getLabelValue("viewType") ?? "grid"; |         if (["calendar", "table", "geoMap"].includes(note.getLabelValue("viewType") ?? "")) { | ||||||
|         if (!["list", "grid"].includes(viewType)) { |  | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,6 +43,8 @@ export default class RootCommandExecutor extends Component { | |||||||
|         const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, { |         const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, { | ||||||
|             activate: true |             activate: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) { |     async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) { | ||||||
|   | |||||||
| @@ -433,9 +433,6 @@ export default class TabManager extends Component { | |||||||
|                 $autocompleteEl.autocomplete("close"); |                 $autocompleteEl.autocomplete("close"); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // close dangling tooltips |  | ||||||
|             $("body > div.tooltip").remove(); |  | ||||||
|  |  | ||||||
|             const noteContextsToRemove = noteContextToRemove.getSubContexts(); |             const noteContextsToRemove = noteContextToRemove.getSubContexts(); | ||||||
|             const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); |             const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); | ||||||
|  |  | ||||||
| @@ -603,18 +600,18 @@ export default class TabManager extends Component { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) { |     async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) { | ||||||
|         const { notePath, hoistedNoteId, viewScope } = this.getNoteContextById(ntxId); |         const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); | ||||||
|  |  | ||||||
|         const removed = await this.removeNoteContext(ntxId); |         const removed = await this.removeNoteContext(ntxId); | ||||||
|  |  | ||||||
|         if (removed) { |         if (removed) { | ||||||
|             this.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); |             this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) { |     async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) { | ||||||
|         const { notePath, hoistedNoteId, viewScope } = this.getNoteContextById(ntxId); |         const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); | ||||||
|         this.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); |         this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async reopenLastTabCommand() { |     async reopenLastTabCommand() { | ||||||
|   | |||||||
| @@ -23,11 +23,11 @@ export default class TouchBarComponent extends Component { | |||||||
|         this.$widget = $("<div>"); |         this.$widget = $("<div>"); | ||||||
|  |  | ||||||
|         $(window).on("focusin", async (e) => { |         $(window).on("focusin", async (e) => { | ||||||
|             const focusedEl = e.target as unknown as HTMLElement; |             const $target = $(e.target); | ||||||
|             const $target = $(focusedEl); |  | ||||||
|  |  | ||||||
|             this.$activeModal = $target.closest(".modal-dialog"); |             this.$activeModal = $target.closest(".modal-dialog"); | ||||||
|             this.lastFocusedComponent = appContext.getComponentByEl(focusedEl); |             const parentComponentEl = $target.closest(".component"); | ||||||
|  |             this.lastFocusedComponent = appContext.getComponentByEl(parentComponentEl[0]); | ||||||
|             this.#refreshTouchBar(); |             this.#refreshTouchBar(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -8,10 +8,12 @@ import electronContextMenu from "./menus/electron_context_menu.js"; | |||||||
| import glob from "./services/glob.js"; | import glob from "./services/glob.js"; | ||||||
| import { t } from "./services/i18n.js"; | import { t } from "./services/i18n.js"; | ||||||
| import options from "./services/options.js"; | import options from "./services/options.js"; | ||||||
|  | import server from "./services/server.js"; | ||||||
| import type ElectronRemote from "@electron/remote"; | import type ElectronRemote from "@electron/remote"; | ||||||
| import type Electron from "electron"; | import type Electron from "electron"; | ||||||
| import "bootstrap/dist/css/bootstrap.min.css"; | import "./stylesheets/bootstrap.scss"; | ||||||
| import "boxicons/css/boxicons.min.css"; | import "boxicons/css/boxicons.min.css"; | ||||||
|  | import "jquery-hotkeys"; | ||||||
| import "autocomplete.js/index_jquery.js"; | import "autocomplete.js/index_jquery.js"; | ||||||
|  |  | ||||||
| await appContext.earlyInit(); | await appContext.earlyInit(); | ||||||
| @@ -45,10 +47,6 @@ if (utils.isElectron()) { | |||||||
|     electronContextMenu.setupContextMenu(); |     electronContextMenu.setupContextMenu(); | ||||||
| } | } | ||||||
|  |  | ||||||
| if (utils.isPWA()) { |  | ||||||
|     initPWATopbarColor(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function initOnElectron() { | function initOnElectron() { | ||||||
|     const electron: typeof Electron = utils.dynamicRequire("electron"); |     const electron: typeof Electron = utils.dynamicRequire("electron"); | ||||||
|     electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); |     electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); | ||||||
| @@ -117,20 +115,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) { | |||||||
|     const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote; |     const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote; | ||||||
|     nativeTheme.themeSource = themeSource; |     nativeTheme.themeSource = themeSource; | ||||||
| } | } | ||||||
|  |  | ||||||
| function initPWATopbarColor() { |  | ||||||
|     const tracker = $("#background-color-tracker"); |  | ||||||
|  |  | ||||||
|     if (tracker.length) { |  | ||||||
|         const applyThemeColor = () => { |  | ||||||
|             let meta = $("meta[name='theme-color']"); |  | ||||||
|             if (!meta.length) { |  | ||||||
|                 meta = $(`<meta name="theme-color">`).appendTo($("head")); |  | ||||||
|             } |  | ||||||
|             meta.attr("content", tracker.css("color")); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         tracker.on("transitionend", applyThemeColor); |  | ||||||
|         applyThemeColor(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ export interface NoteMetaData { | |||||||
| /** | /** | ||||||
|  * Note is the main node and concept in Trilium. |  * Note is the main node and concept in Trilium. | ||||||
|  */ |  */ | ||||||
| export default class FNote { | class FNote { | ||||||
|     private froca: Froca; |     private froca: Froca; | ||||||
|  |  | ||||||
|     noteId!: string; |     noteId!: string; | ||||||
| @@ -256,22 +256,6 @@ export default class FNote { | |||||||
|         return this.children; |         return this.children; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getSubtreeNoteIds(includeArchived = false) { |  | ||||||
|         let noteIds: (string | string[])[] = []; |  | ||||||
|         for (const child of await this.getChildNotes()) { |  | ||||||
|             if (child.isArchived && !includeArchived) continue; |  | ||||||
|  |  | ||||||
|             noteIds.push(child.noteId); |  | ||||||
|             noteIds.push(await child.getSubtreeNoteIds(includeArchived)); |  | ||||||
|         } |  | ||||||
|         return noteIds.flat(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async getSubtreeNotes() { |  | ||||||
|         const noteIds = await this.getSubtreeNoteIds(); |  | ||||||
|         return (await this.froca.getNotes(noteIds)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async getChildNotes() { |     async getChildNotes() { | ||||||
|         return await this.froca.getNotes(this.children); |         return await this.froca.getNotes(this.children); | ||||||
|     } |     } | ||||||
| @@ -907,8 +891,8 @@ export default class FNote { | |||||||
|         return this.getBlob(); |         return this.getBlob(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getBlob() { |     async getBlob() { | ||||||
|         return this.froca.getBlob("notes", this.noteId); |         return await this.froca.getBlob("notes", this.noteId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     toString() { |     toString() { | ||||||
| @@ -1022,14 +1006,6 @@ export default class FNote { | |||||||
|         return this.noteId.startsWith("_options"); |         return this.noteId.startsWith("_options"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     isTriliumSqlite() { |  | ||||||
|         return this.mime === "text/x-sqlite;schema=trilium"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     isTriliumScript() { |  | ||||||
|         return this.mime.startsWith("application/javascript"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Provides note's date metadata. |      * Provides note's date metadata. | ||||||
|      */ |      */ | ||||||
| @@ -1037,3 +1013,5 @@ export default class FNote { | |||||||
|         return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`); |         return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export default FNote; | ||||||
|   | |||||||
| @@ -1,47 +1,78 @@ | |||||||
| import FlexContainer from "../widgets/containers/flex_container.js"; | import FlexContainer from "../widgets/containers/flex_container.js"; | ||||||
|  | import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; | ||||||
| import TabRowWidget from "../widgets/tab_row.js"; | import TabRowWidget from "../widgets/tab_row.js"; | ||||||
|  | import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js"; | ||||||
| import LeftPaneContainer from "../widgets/containers/left_pane_container.js"; | import LeftPaneContainer from "../widgets/containers/left_pane_container.js"; | ||||||
| import NoteTreeWidget from "../widgets/note_tree.js"; | import NoteTreeWidget from "../widgets/note_tree.js"; | ||||||
| import NoteTitleWidget from "../widgets/note_title.jsx"; | import NoteTitleWidget from "../widgets/note_title.js"; | ||||||
|  | import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js"; | ||||||
|  | import NoteActionsWidget from "../widgets/buttons/note_actions.js"; | ||||||
| import NoteDetailWidget from "../widgets/note_detail.js"; | import NoteDetailWidget from "../widgets/note_detail.js"; | ||||||
| import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; | import RibbonContainer from "../widgets/containers/ribbon_container.js"; | ||||||
| import NoteIconWidget from "../widgets/note_icon.jsx"; | import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; | ||||||
|  | import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js"; | ||||||
|  | import NoteListWidget from "../widgets/note_list.js"; | ||||||
|  | import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js"; | ||||||
|  | import SqlResultWidget from "../widgets/sql_result.js"; | ||||||
|  | import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js"; | ||||||
|  | import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; | ||||||
|  | import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js"; | ||||||
|  | import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js"; | ||||||
|  | import NoteIconWidget from "../widgets/note_icon.js"; | ||||||
|  | import SearchResultWidget from "../widgets/search_result.js"; | ||||||
| import ScrollingContainer from "../widgets/containers/scrolling_container.js"; | import ScrollingContainer from "../widgets/containers/scrolling_container.js"; | ||||||
| import RootContainer from "../widgets/containers/root_container.js"; | import RootContainer from "../widgets/containers/root_container.js"; | ||||||
| import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; | import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; | ||||||
| import SpacerWidget from "../widgets/spacer.js"; | import SpacerWidget from "../widgets/spacer.js"; | ||||||
| import QuickSearchWidget from "../widgets/quick_search.js"; | import QuickSearchWidget from "../widgets/quick_search.js"; | ||||||
| import SplitNoteContainer from "../widgets/containers/split_note_container.js"; | import SplitNoteContainer from "../widgets/containers/split_note_container.js"; | ||||||
|  | import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js"; | ||||||
| import CreatePaneButton from "../widgets/buttons/create_pane_button.js"; | import CreatePaneButton from "../widgets/buttons/create_pane_button.js"; | ||||||
| import ClosePaneButton from "../widgets/buttons/close_pane_button.js"; | import ClosePaneButton from "../widgets/buttons/close_pane_button.js"; | ||||||
|  | import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js"; | ||||||
|  | import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js"; | ||||||
|  | import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js"; | ||||||
|  | import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js"; | ||||||
|  | import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js"; | ||||||
|  | import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js"; | ||||||
| import RightPaneContainer from "../widgets/containers/right_pane_container.js"; | import RightPaneContainer from "../widgets/containers/right_pane_container.js"; | ||||||
|  | import EditButton from "../widgets/floating_buttons/edit_button.js"; | ||||||
|  | import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js"; | ||||||
|  | import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js"; | ||||||
|  | import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js"; | ||||||
| import NoteWrapperWidget from "../widgets/note_wrapper.js"; | import NoteWrapperWidget from "../widgets/note_wrapper.js"; | ||||||
|  | import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js"; | ||||||
|  | import SharedInfoWidget from "../widgets/shared_info.js"; | ||||||
| import FindWidget from "../widgets/find.js"; | import FindWidget from "../widgets/find.js"; | ||||||
| import TocWidget from "../widgets/toc.js"; | import TocWidget from "../widgets/toc.js"; | ||||||
| import HighlightsListWidget from "../widgets/highlights_list.js"; | import HighlightsListWidget from "../widgets/highlights_list.js"; | ||||||
| import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; | import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; | ||||||
|  | import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; | ||||||
|  | import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; | ||||||
|  | import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; | ||||||
| import LauncherContainer from "../widgets/containers/launcher_container.js"; | import LauncherContainer from "../widgets/containers/launcher_container.js"; | ||||||
|  | import RevisionsButton from "../widgets/buttons/revisions_button.js"; | ||||||
|  | import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js"; | ||||||
|  | import ApiLogWidget from "../widgets/api_log.js"; | ||||||
|  | import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; | ||||||
|  | import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js"; | ||||||
| import MovePaneButton from "../widgets/buttons/move_pane_button.js"; | import MovePaneButton from "../widgets/buttons/move_pane_button.js"; | ||||||
| import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js"; | import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js"; | ||||||
| import ScrollPadding from "../widgets/scroll_padding.js"; | import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js"; | ||||||
|  | import ScrollPaddingWidget from "../widgets/scroll_padding.js"; | ||||||
|  | import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||||
| import options from "../services/options.js"; | import options from "../services/options.js"; | ||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
|  | import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; | ||||||
|  | import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; | ||||||
|  | import CloseZenButton from "../widgets/close_zen_button.js"; | ||||||
| import type { AppContext } from "../components/app_context.js"; | import type { AppContext } from "../components/app_context.js"; | ||||||
| import type { WidgetsByParent } from "../services/bundle.js"; | import type { WidgetsByParent } from "../services/bundle.js"; | ||||||
|  | import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js"; | ||||||
|  | import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js"; | ||||||
|  | import PngExportButton from "../widgets/floating_buttons/png_export_button.js"; | ||||||
|  | import RefreshButton from "../widgets/floating_buttons/refresh_button.js"; | ||||||
| import { applyModals } from "./layout_commons.js"; | import { applyModals } from "./layout_commons.js"; | ||||||
| import Ribbon from "../widgets/ribbon/Ribbon.jsx"; |  | ||||||
| import FloatingButtons from "../widgets/FloatingButtons.jsx"; |  | ||||||
| import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; |  | ||||||
| import SearchResult from "../widgets/search_result.jsx"; |  | ||||||
| import GlobalMenu from "../widgets/buttons/global_menu.jsx"; |  | ||||||
| import SqlResults from "../widgets/sql_result.js"; |  | ||||||
| import SqlTableSchemas from "../widgets/sql_table_schemas.js"; |  | ||||||
| import TitleBarButtons from "../widgets/title_bar_buttons.jsx"; |  | ||||||
| import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js"; |  | ||||||
| import ApiLog from "../widgets/api_log.jsx"; |  | ||||||
| import CloseZenModeButton from "../widgets/close_zen_button.jsx"; |  | ||||||
| import SharedInfo from "../widgets/shared_info.jsx"; |  | ||||||
| import NoteList from "../widgets/collections/NoteList.jsx"; |  | ||||||
| 
 | 
 | ||||||
| export default class DesktopLayout { | export default class DesktopLayout { | ||||||
| 
 | 
 | ||||||
| @@ -76,9 +107,9 @@ export default class DesktopLayout { | |||||||
|                 new FlexContainer("row") |                 new FlexContainer("row") | ||||||
|                     .class("tab-row-container") |                     .class("tab-row-container") | ||||||
|                     .child(new FlexContainer("row").id("tab-row-left-spacer")) |                     .child(new FlexContainer("row").id("tab-row-left-spacer")) | ||||||
|                     .optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />) |                     .optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true)) | ||||||
|                     .child(new TabRowWidget().class("full-width")) |                     .child(new TabRowWidget().class("full-width")) | ||||||
|                     .optChild(customTitleBarButtons, <TitleBarButtons />) |                     .optChild(customTitleBarButtons, new TitleBarButtonsWidget()) | ||||||
|                     .css("height", "40px") |                     .css("height", "40px") | ||||||
|                     .css("background-color", "var(--launcher-pane-background-color)") |                     .css("background-color", "var(--launcher-pane-background-color)") | ||||||
|                     .setParent(appContext) |                     .setParent(appContext) | ||||||
| @@ -99,7 +130,7 @@ export default class DesktopLayout { | |||||||
|                         new FlexContainer("column") |                         new FlexContainer("column") | ||||||
|                             .id("rest-pane") |                             .id("rest-pane") | ||||||
|                             .css("flex-grow", "1") |                             .css("flex-grow", "1") | ||||||
|                             .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px")) |                             .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px")) | ||||||
|                             .child( |                             .child( | ||||||
|                                 new FlexContainer("row") |                                 new FlexContainer("row") | ||||||
|                                     .filling() |                                     .filling() | ||||||
| @@ -120,30 +151,69 @@ export default class DesktopLayout { | |||||||
|                                                                 .css("min-height", "50px") |                                                                 .css("min-height", "50px") | ||||||
|                                                                 .css("align-items", "center") |                                                                 .css("align-items", "center") | ||||||
|                                                                 .cssBlock(".title-row > * { margin: 5px; }") |                                                                 .cssBlock(".title-row > * { margin: 5px; }") | ||||||
|                                                                 .child(<NoteIconWidget />) |                                                                 .child(new NoteIconWidget()) | ||||||
|                                                                 .child(<NoteTitleWidget />) |                                                                 .child(new NoteTitleWidget()) | ||||||
|                                                                 .child(new SpacerWidget(0, 1)) |                                                                 .child(new SpacerWidget(0, 1)) | ||||||
|                                                                 .child(<MovePaneButton direction="left" />) |                                                                 .child(new MovePaneButton(true)) | ||||||
|                                                                 .child(<MovePaneButton direction="right" />) |                                                                 .child(new MovePaneButton(false)) | ||||||
|                                                                 .child(<ClosePaneButton />) |                                                                 .child(new ClosePaneButton()) | ||||||
|                                                                 .child(<CreatePaneButton />) |                                                                 .child(new CreatePaneButton()) | ||||||
|                                                         ) |                                                         ) | ||||||
|                                                         .child(<Ribbon />) |                                                         .child( | ||||||
|                                                         .child(<SharedInfo />) |                                                             new RibbonContainer() | ||||||
|  |                                                                 // the order of the widgets matter. Some of these want to "activate" themselves
 | ||||||
|  |                                                                 // when visible. When this happens to multiple of them, the first one "wins".
 | ||||||
|  |                                                                 // promoted attributes should always win.
 | ||||||
|  |                                                                 .ribbon(new ClassicEditorToolbar()) | ||||||
|  |                                                                 .ribbon(new ScriptExecutorWidget()) | ||||||
|  |                                                                 .ribbon(new SearchDefinitionWidget()) | ||||||
|  |                                                                 .ribbon(new EditedNotesWidget()) | ||||||
|  |                                                                 .ribbon(new BookPropertiesWidget()) | ||||||
|  |                                                                 .ribbon(new NotePropertiesWidget()) | ||||||
|  |                                                                 .ribbon(new FilePropertiesWidget()) | ||||||
|  |                                                                 .ribbon(new ImagePropertiesWidget()) | ||||||
|  |                                                                 .ribbon(new BasicPropertiesWidget()) | ||||||
|  |                                                                 .ribbon(new OwnedAttributeListWidget()) | ||||||
|  |                                                                 .ribbon(new InheritedAttributesWidget()) | ||||||
|  |                                                                 .ribbon(new NotePathsWidget()) | ||||||
|  |                                                                 .ribbon(new NoteMapRibbonWidget()) | ||||||
|  |                                                                 .ribbon(new SimilarNotesWidget()) | ||||||
|  |                                                                 .ribbon(new NoteInfoWidget()) | ||||||
|  |                                                                 .button(new RevisionsButton()) | ||||||
|  |                                                                 .button(new NoteActionsWidget()) | ||||||
|  |                                                         ) | ||||||
|  |                                                         .child(new SharedInfoWidget()) | ||||||
|                                                         .child(new WatchedFileUpdateStatusWidget()) |                                                         .child(new WatchedFileUpdateStatusWidget()) | ||||||
|                                                         .child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />) |                                                         .child( | ||||||
|  |                                                             new FloatingButtons() | ||||||
|  |                                                                 .child(new RefreshButton()) | ||||||
|  |                                                                 .child(new SwitchSplitOrientationButton()) | ||||||
|  |                                                                 .child(new ToggleReadOnlyButton()) | ||||||
|  |                                                                 .child(new EditButton()) | ||||||
|  |                                                                 .child(new ShowTocWidgetButton()) | ||||||
|  |                                                                 .child(new ShowHighlightsListWidgetButton()) | ||||||
|  |                                                                 .child(new CodeButtonsWidget()) | ||||||
|  |                                                                 .child(new RelationMapButtons()) | ||||||
|  |                                                                 .child(new GeoMapButtons()) | ||||||
|  |                                                                 .child(new CopyImageReferenceButton()) | ||||||
|  |                                                                 .child(new SvgExportButton()) | ||||||
|  |                                                                 .child(new PngExportButton()) | ||||||
|  |                                                                 .child(new BacklinksWidget()) | ||||||
|  |                                                                 .child(new ContextualHelpButton()) | ||||||
|  |                                                                 .child(new HideFloatingButtonsButton()) | ||||||
|  |                                                         ) | ||||||
|                                                         .child( |                                                         .child( | ||||||
|                                                             new ScrollingContainer() |                                                             new ScrollingContainer() | ||||||
|                                                                 .filling() |                                                                 .filling() | ||||||
|                                                                 .child(new PromotedAttributesWidget()) |                                                                 .child(new PromotedAttributesWidget()) | ||||||
|                                                                 .child(<SqlTableSchemas />) |                                                                 .child(new SqlTableSchemasWidget()) | ||||||
|                                                                 .child(new NoteDetailWidget()) |                                                                 .child(new NoteDetailWidget()) | ||||||
|                                                                 .child(<NoteList />) |                                                                 .child(new NoteListWidget(false)) | ||||||
|                                                                 .child(<SearchResult />) |                                                                 .child(new SearchResultWidget()) | ||||||
|                                                                 .child(<SqlResults />) |                                                                 .child(new SqlResultWidget()) | ||||||
|                                                                 .child(<ScrollPadding />) |                                                                 .child(new ScrollPaddingWidget()) | ||||||
|                                                         ) |                                                         ) | ||||||
|                                                         .child(<ApiLog />) |                                                         .child(new ApiLogWidget()) | ||||||
|                                                         .child(new FindWidget()) |                                                         .child(new FindWidget()) | ||||||
|                                                         .child( |                                                         .child( | ||||||
|                                                             ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
 |                                                             ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
 | ||||||
| @@ -162,11 +232,11 @@ export default class DesktopLayout { | |||||||
|                             ) |                             ) | ||||||
|                     ) |                     ) | ||||||
|             ) |             ) | ||||||
|             .child(<CloseZenModeButton />) |             .child(new CloseZenButton()) | ||||||
| 
 | 
 | ||||||
|             // Desktop-specific dialogs.
 |             // Desktop-specific dialogs.
 | ||||||
|             .child(<PasswordNoteSetDialog />) |             .child(new PasswordNoteSetDialog()) | ||||||
|             .child(<UploadAttachmentsDialog />); |             .child(new UploadAttachmentsDialog()); | ||||||
| 
 | 
 | ||||||
|         applyModals(rootContainer); |         applyModals(rootContainer); | ||||||
|         return rootContainer; |         return rootContainer; | ||||||
| @@ -176,18 +246,14 @@ export default class DesktopLayout { | |||||||
|         let launcherPane; |         let launcherPane; | ||||||
| 
 | 
 | ||||||
|         if (isHorizontal) { |         if (isHorizontal) { | ||||||
|             launcherPane = new FlexContainer("row") |             launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)); | ||||||
|                 .css("height", "53px") |  | ||||||
|                 .class("horizontal") |  | ||||||
|                 .child(new LauncherContainer(true)) |  | ||||||
|                 .child(<GlobalMenu isHorizontalLayout={true} />); |  | ||||||
|         } else { |         } else { | ||||||
|             launcherPane = new FlexContainer("column") |             launcherPane = new FlexContainer("column") | ||||||
|                 .css("width", "53px") |                 .css("width", "53px") | ||||||
|                 .class("vertical") |                 .class("vertical") | ||||||
|                 .child(<GlobalMenu isHorizontalLayout={false} />) |                 .child(new GlobalMenuWidget(false)) | ||||||
|                 .child(new LauncherContainer(false)) |                 .child(new LauncherContainer(false)) | ||||||
|                 .child(<LeftPaneToggle isHorizontalLayout={false} />); |                 .child(new LeftPaneToggleWidget(false)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         launcherPane.id("launcher-pane"); |         launcherPane.id("launcher-pane"); | ||||||
| @@ -24,48 +24,46 @@ import InfoDialog from "../widgets/dialogs/info.js"; | |||||||
| import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; | import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; | ||||||
| import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; | import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; | ||||||
| import FlexContainer from "../widgets/containers/flex_container.js"; | import FlexContainer from "../widgets/containers/flex_container.js"; | ||||||
| import NoteIconWidget from "../widgets/note_icon"; | import NoteIconWidget from "../widgets/note_icon.js"; | ||||||
| import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; | import NoteTitleWidget from "../widgets/note_title.js"; | ||||||
|  | import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||||
|  | import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; | ||||||
| import NoteDetailWidget from "../widgets/note_detail.js"; | import NoteDetailWidget from "../widgets/note_detail.js"; | ||||||
| import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx"; | import NoteListWidget from "../widgets/note_list.js"; | ||||||
| import NoteTitleWidget from "../widgets/note_title.jsx"; |  | ||||||
| import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js"; |  | ||||||
| import NoteList from "../widgets/collections/NoteList.jsx"; |  | ||||||
| 
 | 
 | ||||||
| export function applyModals(rootContainer: RootContainer) { | export function applyModals(rootContainer: RootContainer) { | ||||||
|     rootContainer |     rootContainer | ||||||
|         .child(<BulkActionsDialog />) |         .child(new BulkActionsDialog()) | ||||||
|         .child(<AboutDialog />) |         .child(new AboutDialog()) | ||||||
|         .child(<HelpDialog />) |         .child(new HelpDialog()) | ||||||
|         .child(<RecentChangesDialog />) |         .child(new RecentChangesDialog()) | ||||||
|         .child(<BranchPrefixDialog />) |         .child(new BranchPrefixDialog()) | ||||||
|         .child(<SortChildNotesDialog />) |         .child(new SortChildNotesDialog()) | ||||||
|         .child(<IncludeNoteDialog />) |         .child(new IncludeNoteDialog()) | ||||||
|         .child(<NoteTypeChooserDialog />) |         .child(new NoteTypeChooserDialog()) | ||||||
|         .child(<JumpToNoteDialog />) |         .child(new JumpToNoteDialog()) | ||||||
|         .child(<AddLinkDialog />) |         .child(new AddLinkDialog()) | ||||||
|         .child(<CloneToDialog />) |         .child(new CloneToDialog()) | ||||||
|         .child(<MoveToDialog />) |         .child(new MoveToDialog()) | ||||||
|         .child(<ImportDialog />) |         .child(new ImportDialog()) | ||||||
|         .child(<ExportDialog />) |         .child(new ExportDialog()) | ||||||
|         .child(<MarkdownImportDialog />) |         .child(new MarkdownImportDialog()) | ||||||
|         .child(<ProtectedSessionPasswordDialog />) |         .child(new ProtectedSessionPasswordDialog()) | ||||||
|         .child(<RevisionsDialog />) |         .child(new RevisionsDialog()) | ||||||
|         .child(<DeleteNotesDialog />) |         .child(new DeleteNotesDialog()) | ||||||
|         .child(<InfoDialog />) |         .child(new InfoDialog()) | ||||||
|         .child(<ConfirmDialog />) |         .child(new ConfirmDialog()) | ||||||
|         .child(<PromptDialog />) |         .child(new PromptDialog()) | ||||||
|         .child(<IncorrectCpuArchDialog />) |         .child(new IncorrectCpuArchDialog()) | ||||||
|         .child(new PopupEditorDialog() |         .child(new PopupEditorDialog() | ||||||
|                 .child(new FlexContainer("row") |                 .child(new FlexContainer("row") | ||||||
|                     .class("title-row") |                     .class("title-row") | ||||||
|                     .css("align-items", "center") |                     .css("align-items", "center") | ||||||
|                     .cssBlock(".title-row > * { margin: 5px; }") |                     .cssBlock(".title-row > * { margin: 5px; }") | ||||||
|                     .child(<NoteIconWidget />) |                     .child(new NoteIconWidget()) | ||||||
|                     .child(<NoteTitleWidget />)) |                     .child(new NoteTitleWidget())) | ||||||
|                 .child(<PopupEditorFormattingToolbar />) |                 .child(new ClassicEditorToolbar()) | ||||||
|                 .child(new PromotedAttributesWidget()) |                 .child(new PromotedAttributesWidget()) | ||||||
|                 .child(new NoteDetailWidget()) |                 .child(new NoteDetailWidget()) | ||||||
|                 .child(<NoteList displayOnlyCollections />)) |                 .child(new NoteListWidget(true))) | ||||||
|         .child(<CallToActionDialog />); |  | ||||||
| } | } | ||||||
| @@ -3,27 +3,29 @@ import NoteTitleWidget from "../widgets/note_title.js"; | |||||||
| import NoteDetailWidget from "../widgets/note_detail.js"; | import NoteDetailWidget from "../widgets/note_detail.js"; | ||||||
| import QuickSearchWidget from "../widgets/quick_search.js"; | import QuickSearchWidget from "../widgets/quick_search.js"; | ||||||
| import NoteTreeWidget from "../widgets/note_tree.js"; | import NoteTreeWidget from "../widgets/note_tree.js"; | ||||||
|  | import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js"; | ||||||
|  | import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js"; | ||||||
| import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; | import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; | ||||||
| import ScrollingContainer from "../widgets/containers/scrolling_container.js"; | import ScrollingContainer from "../widgets/containers/scrolling_container.js"; | ||||||
|  | import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; | ||||||
|  | import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; | ||||||
|  | import EditButton from "../widgets/floating_buttons/edit_button.js"; | ||||||
|  | import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; | ||||||
|  | import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; | ||||||
|  | import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js"; | ||||||
|  | import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; | ||||||
|  | import NoteListWidget from "../widgets/note_list.js"; | ||||||
| import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; | import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; | ||||||
| import LauncherContainer from "../widgets/containers/launcher_container.js"; | import LauncherContainer from "../widgets/containers/launcher_container.js"; | ||||||
| import RootContainer from "../widgets/containers/root_container.js"; | import RootContainer from "../widgets/containers/root_container.js"; | ||||||
| import SharedInfoWidget from "../widgets/shared_info.js"; | import SharedInfoWidget from "../widgets/shared_info.js"; | ||||||
| import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; | import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; | ||||||
| import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; | import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; | ||||||
| import type AppContext from "../components/app_context.js"; | import type AppContext from "../components/app_context.js"; | ||||||
| import TabRowWidget from "../widgets/tab_row.js"; | import TabRowWidget from "../widgets/tab_row.js"; | ||||||
| import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js"; | import RefreshButton from "../widgets/floating_buttons/refresh_button.js"; | ||||||
|  | import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js"; | ||||||
| import { applyModals } from "./layout_commons.js"; | import { applyModals } from "./layout_commons.js"; | ||||||
| import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; |  | ||||||
| import { useNoteContext } from "../widgets/react/hooks.jsx"; |  | ||||||
| import FloatingButtons from "../widgets/FloatingButtons.jsx"; |  | ||||||
| import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; |  | ||||||
| import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx"; |  | ||||||
| import CloseZenModeButton from "../widgets/close_zen_button.js"; |  | ||||||
| import NoteWrapperWidget from "../widgets/note_wrapper.js"; |  | ||||||
| import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js"; |  | ||||||
| import NoteList from "../widgets/collections/NoteList.jsx"; |  | ||||||
| 
 | 
 | ||||||
| const MOBILE_CSS = ` | const MOBILE_CSS = ` | ||||||
| <style> | <style> | ||||||
| @@ -132,33 +134,38 @@ export default class MobileLayout { | |||||||
|                             .child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS))) |                             .child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS))) | ||||||
|                     ) |                     ) | ||||||
|                     .child( |                     .child( | ||||||
|                         new ScreenContainer("detail", "row") |                         new ScreenContainer("detail", "column") | ||||||
|                             .id("detail-container") |                             .id("detail-container") | ||||||
|                             .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9") |                             .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9") | ||||||
|                             .child( |                             .child( | ||||||
|                                 new NoteWrapperWidget() |                                 new FlexContainer("row") | ||||||
|                                     .child( |                                     .contentSized() | ||||||
|                                         new FlexContainer("row") |                                     .css("font-size", "larger") | ||||||
|                                             .contentSized() |                                     .css("align-items", "center") | ||||||
|                                             .css("font-size", "larger") |                                     .child(new ToggleSidebarButtonWidget().contentSized()) | ||||||
|                                             .css("align-items", "center") |                                     .child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em")) | ||||||
|                                             .child(<ToggleSidebarButton />) |                                     .child(new MobileDetailMenuWidget(true).contentSized()) | ||||||
|                                             .child(<NoteTitleWidget />) |  | ||||||
|                                             .child(<MobileDetailMenu />) |  | ||||||
|                                     ) |  | ||||||
|                                     .child(<SharedInfoWidget />) |  | ||||||
|                                     .child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />) |  | ||||||
|                                     .child(new PromotedAttributesWidget()) |  | ||||||
|                                     .child( |  | ||||||
|                                         new ScrollingContainer() |  | ||||||
|                                             .filling() |  | ||||||
|                                             .contentSized() |  | ||||||
|                                             .child(new NoteDetailWidget()) |  | ||||||
|                                             .child(<NoteList />) |  | ||||||
|                                             .child(<FilePropertiesWrapper />) |  | ||||||
|                                     ) |  | ||||||
|                                     .child(<MobileEditorToolbar />) |  | ||||||
|                             ) |                             ) | ||||||
|  |                             .child(new SharedInfoWidget()) | ||||||
|  |                             .child( | ||||||
|  |                                 new FloatingButtons() | ||||||
|  |                                     .child(new RefreshButton()) | ||||||
|  |                                     .child(new EditButton()) | ||||||
|  |                                     .child(new RelationMapButtons()) | ||||||
|  |                                     .child(new SvgExportButton()) | ||||||
|  |                                     .child(new BacklinksWidget()) | ||||||
|  |                                     .child(new HideFloatingButtonsButton()) | ||||||
|  |                             ) | ||||||
|  |                             .child(new PromotedAttributesWidget()) | ||||||
|  |                             .child( | ||||||
|  |                                 new ScrollingContainer() | ||||||
|  |                                     .filling() | ||||||
|  |                                     .contentSized() | ||||||
|  |                                     .child(new NoteDetailWidget()) | ||||||
|  |                                     .child(new NoteListWidget(false)) | ||||||
|  |                                     .child(new FilePropertiesWidget().css("font-size", "smaller")) | ||||||
|  |                             ) | ||||||
|  |                             .child(new MobileEditorToolbar()) | ||||||
|                     ) |                     ) | ||||||
|             ) |             ) | ||||||
|             .child( |             .child( | ||||||
| @@ -166,25 +173,9 @@ export default class MobileLayout { | |||||||
|                     .contentSized() |                     .contentSized() | ||||||
|                     .id("mobile-bottom-bar") |                     .id("mobile-bottom-bar") | ||||||
|                     .child(new TabRowWidget().css("height", "40px")) |                     .child(new TabRowWidget().css("height", "40px")) | ||||||
|                     .child(new FlexContainer("row") |                     .child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane")) | ||||||
|                         .class("horizontal") |             ); | ||||||
|                         .css("height", "53px") |  | ||||||
|                         .child(new LauncherContainer(true)) |  | ||||||
|                         .child(<GlobalMenuWidget isHorizontalLayout />) |  | ||||||
|                         .id("launcher-pane")) |  | ||||||
|             ) |  | ||||||
|             .child(<CloseZenModeButton />); |  | ||||||
|         applyModals(rootContainer); |         applyModals(rootContainer); | ||||||
|         return rootContainer; |         return rootContainer; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| function FilePropertiesWrapper() { |  | ||||||
|     const { note } = useNoteContext(); |  | ||||||
| 
 |  | ||||||
|     return ( |  | ||||||
|         <div> |  | ||||||
|             {note?.type === "file" && <FilePropertiesTab note={note} />} |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "bootstrap/dist/css/bootstrap.min.css"; | import "./stylesheets/bootstrap.scss"; | ||||||
|  |  | ||||||
| // @ts-ignore - module = undefined | // @ts-ignore - module = undefined | ||||||
| // Required for correct loading of scripts in Electron | // Required for correct loading of scripts in Electron | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| import { KeyboardActionNames } from "@triliumnext/commons"; | import keyboardActionService from "../services/keyboard_actions.js"; | ||||||
| import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js"; |  | ||||||
| import note_tooltip from "../services/note_tooltip.js"; | import note_tooltip from "../services/note_tooltip.js"; | ||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
| import { should } from "vitest"; |  | ||||||
|  |  | ||||||
| export interface ContextMenuOptions<T> { | export interface ContextMenuOptions<T> { | ||||||
|     x: number; |     x: number; | ||||||
| @@ -15,13 +13,8 @@ export interface ContextMenuOptions<T> { | |||||||
|     onHide?: () => void; |     onHide?: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface MenuSeparatorItem { | interface MenuSeparatorItem { | ||||||
|     kind: "separator"; |     title: "----"; | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface MenuHeader { |  | ||||||
|     title: string; |  | ||||||
|     kind: "header"; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface MenuItemBadge { | export interface MenuItemBadge { | ||||||
| @@ -33,11 +26,6 @@ export interface MenuCommandItem<T> { | |||||||
|     title: string; |     title: string; | ||||||
|     command?: T; |     command?: T; | ||||||
|     type?: string; |     type?: string; | ||||||
|     /** |  | ||||||
|      * The icon to display in the menu item. |  | ||||||
|      * |  | ||||||
|      * If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`. |  | ||||||
|      */ |  | ||||||
|     uiIcon?: string; |     uiIcon?: string; | ||||||
|     badges?: MenuItemBadge[]; |     badges?: MenuItemBadge[]; | ||||||
|     templateNoteId?: string; |     templateNoteId?: string; | ||||||
| @@ -45,13 +33,12 @@ export interface MenuCommandItem<T> { | |||||||
|     handler?: MenuHandler<T>; |     handler?: MenuHandler<T>; | ||||||
|     items?: MenuItem<T>[] | null; |     items?: MenuItem<T>[] | null; | ||||||
|     shortcut?: string; |     shortcut?: string; | ||||||
|     keyboardShortcut?: KeyboardActionNames; |  | ||||||
|     spellingSuggestion?: string; |     spellingSuggestion?: string; | ||||||
|     checked?: boolean; |     checked?: boolean; | ||||||
|     columns?: number; |     columns?: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader; | export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem; | ||||||
| export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void; | export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void; | ||||||
| export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent; | export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent; | ||||||
|  |  | ||||||
| @@ -156,51 +143,14 @@ class ContextMenu { | |||||||
|             .addClass("show"); |             .addClass("show"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[], multicolumn = false) { |     addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[]) { | ||||||
|         let $group = $parent; // The current group or parent element to which items are being appended |         for (const item of items) { | ||||||
|         let shouldStartNewGroup = false; // If true, the next item will start a new group |  | ||||||
|         let shouldResetGroup = false; // If true, the next item will be the last one from the group |  | ||||||
|  |  | ||||||
|         for (let index = 0; index < items.length; index++) { |  | ||||||
|             const item = items[index]; |  | ||||||
|             if (!item) { |             if (!item) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // If the current item is a header, start a new group. This group will contain the |             if (item.title === "----") { | ||||||
|             // header and the next item that follows the header. |                 $parent.append($("<div>").addClass("dropdown-divider")); | ||||||
|             if ("kind" in item && item.kind === "header") { |  | ||||||
|                 if (multicolumn && !shouldResetGroup) { |  | ||||||
|                     shouldStartNewGroup = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // If the next item is a separator, start a new group. This group will contain the |  | ||||||
|             // current item, the separator, and the next item after the separator. |  | ||||||
|             const nextItem = (index < items.length - 1) ? items[index + 1] : null; |  | ||||||
|             if (multicolumn && nextItem && "kind" in nextItem && nextItem.kind === "separator") { |  | ||||||
|                 if (!shouldResetGroup) { |  | ||||||
|                     shouldStartNewGroup = true; |  | ||||||
|                 } else { |  | ||||||
|                     shouldResetGroup = true; // Continue the current group |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Create a new group to avoid column breaks before and after the seaparator / header. |  | ||||||
|             // This is a workaround for Firefox not supporting break-before / break-after: avoid  |  | ||||||
|             // for columns. |  | ||||||
|             if (shouldStartNewGroup) { |  | ||||||
|                 $group = $("<div class='dropdown-no-break'>"); |  | ||||||
|                 $parent.append($group); |  | ||||||
|                 shouldStartNewGroup = false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if ("kind" in item && item.kind === "separator") { |  | ||||||
|                 $group.append($("<div>").addClass("dropdown-divider")); |  | ||||||
|                 shouldResetGroup = true; // End the group after the next item |  | ||||||
|             } else if ("kind" in item && item.kind === "header") { |  | ||||||
|                 $group.append($("<h6>").addClass("dropdown-header").text(item.title)); |  | ||||||
|                 shouldResetGroup = true; |  | ||||||
|             } else { |             } else { | ||||||
|                 const $icon = $("<span>"); |                 const $icon = $("<span>"); | ||||||
|  |  | ||||||
| @@ -230,23 +180,7 @@ class ContextMenu { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 if ("keyboardShortcut" in item && item.keyboardShortcut) { |                 if ("shortcut" in item && item.shortcut) { | ||||||
|                     const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts; |  | ||||||
|                     if (shortcuts) { |  | ||||||
|                         const allShortcuts: string[] = []; |  | ||||||
|                         for (const effectiveShortcut of shortcuts) { |  | ||||||
|                             allShortcuts.push(effectiveShortcut.split("+") |  | ||||||
|                                 .map(key => `<kbd>${key}</kbd>`) |  | ||||||
|                                 .join("+")); |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         if (allShortcuts.length) { |  | ||||||
|                             const container = $("<span>").addClass("keyboard-shortcut"); |  | ||||||
|                             container.append($(allShortcuts.join(","))); |  | ||||||
|                             $link.append(container); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } else if ("shortcut" in item && item.shortcut) { |  | ||||||
|                     $link.append($("<kbd>").text(item.shortcut)); |                     $link.append($("<kbd>").text(item.shortcut)); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
| @@ -302,24 +236,16 @@ class ContextMenu { | |||||||
|                     $link.addClass("dropdown-toggle"); |                     $link.addClass("dropdown-toggle"); | ||||||
|  |  | ||||||
|                     const $subMenu = $("<ul>").addClass("dropdown-menu"); |                     const $subMenu = $("<ul>").addClass("dropdown-menu"); | ||||||
|                     const hasColumns = !!item.columns && item.columns > 1; |                     if (!this.isMobile && item.columns) { | ||||||
|                     if (!this.isMobile && hasColumns) { |                         $subMenu.css("column-count", item.columns); | ||||||
|                         $subMenu.css("column-count", item.columns!); |  | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     this.addItems($subMenu, item.items, hasColumns); |                     this.addItems($subMenu, item.items); | ||||||
|  |  | ||||||
|                     $item.append($subMenu); |                     $item.append($subMenu); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 $group.append($item); |                 $parent.append($item); | ||||||
|                  |  | ||||||
|                 // After adding a menu item, if the previous item was a separator or header, |  | ||||||
|                 // reset the group so that the next item will be appended directly to the parent. |  | ||||||
|                 if (shouldResetGroup) { |  | ||||||
|                     $group = $parent; |  | ||||||
|                     shouldResetGroup = false; |  | ||||||
|                 }; |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ function setupContextMenu() { | |||||||
|                 handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) |                 handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             items.push({ kind: "separator" }); |             items.push({ title: `----` }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (params.isEditable) { |         if (params.isEditable) { | ||||||
| @@ -112,7 +112,7 @@ function setupContextMenu() { | |||||||
|             // Replace the placeholder with the real search keyword. |             // Replace the placeholder with the real search keyword. | ||||||
|             let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); |             let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); | ||||||
|  |  | ||||||
|             items.push({ kind: "separator" }); |             items.push({ title: "----" }); | ||||||
|  |  | ||||||
|             items.push({ |             items.push({ | ||||||
|                 title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }), |                 title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }), | ||||||
|   | |||||||
| @@ -45,16 +45,16 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener< | |||||||
|             isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null, |             isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null, | ||||||
|             isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null, |             isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null, | ||||||
|             isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null, |             isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null, | ||||||
|             isVisibleRoot || isAvailableRoot ? { kind: "separator" } : null, |             isVisibleRoot || isAvailableRoot ? { title: "----" } : null, | ||||||
|  |  | ||||||
|             isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null, |             isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null, | ||||||
|             isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null, |             isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null, | ||||||
|             isVisibleItem || isAvailableItem ? { kind: "separator" } : null, |             isVisibleItem || isAvailableItem ? { title: "----" } : null, | ||||||
|  |  | ||||||
|             { title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem }, |             { title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem }, | ||||||
|             { title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted }, |             { title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } |             { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } | ||||||
|         ]; |         ]; | ||||||
|   | |||||||
| @@ -13,8 +13,6 @@ import type NoteTreeWidget from "../widgets/note_tree.js"; | |||||||
| import type FAttachment from "../entities/fattachment.js"; | import type FAttachment from "../entities/fattachment.js"; | ||||||
| import type { SelectMenuItemEventListener } from "../components/events.js"; | import type { SelectMenuItemEventListener } from "../components/events.js"; | ||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
| import attributes from "../services/attributes.js"; |  | ||||||
| import { executeBulkActions } from "../services/bulk_action.js"; |  | ||||||
|  |  | ||||||
| // TODO: Deduplicate once client/server is well split. | // TODO: Deduplicate once client/server is well split. | ||||||
| interface ConvertToAttachmentResponse { | interface ConvertToAttachmentResponse { | ||||||
| @@ -25,7 +23,7 @@ let lastTargetNode: HTMLElement | null = null; | |||||||
|  |  | ||||||
| // This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator, | // This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator, | ||||||
| // so they need to be added manually. | // so they need to be added manually. | ||||||
| export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog" | "searchInSubtree"; | export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog"; | ||||||
|  |  | ||||||
| export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> { | export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> { | ||||||
|     private treeWidget: NoteTreeWidget; |     private treeWidget: NoteTreeWidget; | ||||||
| @@ -63,11 +61,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|         // the only exception is when the only selected note is the one that was right-clicked, then |         // the only exception is when the only selected note is the one that was right-clicked, then | ||||||
|         // it's clear what the user meant to do. |         // it's clear what the user meant to do. | ||||||
|         const selNodes = this.treeWidget.getSelectedNodes(); |         const selNodes = this.treeWidget.getSelectedNodes(); | ||||||
|         const selectedNotes = await froca.getNotes(selNodes.map(node => node.data.noteId)); |  | ||||||
|         if (note && !selectedNotes.includes(note)) selectedNotes.push(note); |  | ||||||
|         const isArchived = selectedNotes.every(note => note.isArchived); |  | ||||||
|         const canToggleArchived = !selectedNotes.some(note => note.isArchived !== isArchived); |  | ||||||
|  |  | ||||||
|         const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node); |         const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node); | ||||||
|  |  | ||||||
|         const notSearch = note?.type !== "search"; |         const notSearch = note?.type !== "search"; | ||||||
| @@ -76,29 +69,27 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|         const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch; |         const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch; | ||||||
|  |  | ||||||
|         const items: (MenuItem<TreeCommandNames> | null)[] = [ |         const items: (MenuItem<TreeCommandNames> | null)[] = [ | ||||||
|             { title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes }, |             { title: `${t("tree-context-menu.open-in-a-new-tab")}`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes }, | ||||||
|             { title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes }, |             { title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes }, | ||||||
|             { title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes }, |             { title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes }, | ||||||
|  |  | ||||||
|             isHoisted |             isHoisted | ||||||
|                 ? null |                 ? null | ||||||
|                 : { |                 : { | ||||||
|                       title: `${t("tree-context-menu.hoist-note")}`, |                       title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, | ||||||
|                       command: "toggleNoteHoisting", |                       command: "toggleNoteHoisting", | ||||||
|                       keyboardShortcut: "toggleNoteHoisting", |  | ||||||
|                       uiIcon: "bx bxs-chevrons-up", |                       uiIcon: "bx bxs-chevrons-up", | ||||||
|                       enabled: noSelectedNotes && notSearch |                       enabled: noSelectedNotes && notSearch | ||||||
|                   }, |                   }, | ||||||
|             !isHoisted || !isNotRoot |             !isHoisted || !isNotRoot | ||||||
|                 ? null |                 ? null | ||||||
|                 : { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" }, |                 : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.insert-note-after"), |                 title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`, | ||||||
|                 command: "insertNoteAfter", |                 command: "insertNoteAfter", | ||||||
|                 keyboardShortcut: "createNoteAfter", |  | ||||||
|                 uiIcon: "bx bx-plus", |                 uiIcon: "bx bx-plus", | ||||||
|                 items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null, |                 items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null, | ||||||
|                 enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp, |                 enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp, | ||||||
| @@ -106,22 +97,21 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.insert-child-note"), |                 title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`, | ||||||
|                 command: "insertChildNote", |                 command: "insertChildNote", | ||||||
|                 keyboardShortcut: "createNoteInto", |  | ||||||
|                 uiIcon: "bx bx-plus", |                 uiIcon: "bx bx-plus", | ||||||
|                 items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null, |                 items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null, | ||||||
|                 enabled: notSearch && noSelectedNotes && notOptionsOrHelp, |                 enabled: notSearch && noSelectedNotes && notOptionsOrHelp, | ||||||
|                 columns: 2 |                 columns: 2 | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes }, |             { title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes }, | ||||||
|  |  | ||||||
|             { title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes }, |             { title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.advanced"), |                 title: t("tree-context-menu.advanced"), | ||||||
| @@ -130,52 +120,54 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|                 items: [ |                 items: [ | ||||||
|                     { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true }, |                     { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true }, | ||||||
|  |  | ||||||
|                     { kind: "separator" }, |                     { title: "----" }, | ||||||
|  |  | ||||||
|                     { |                     { | ||||||
|                         title: t("tree-context-menu.edit-branch-prefix"), |                         title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, | ||||||
|                         command: "editBranchPrefix", |                         command: "editBranchPrefix", | ||||||
|                         keyboardShortcut: "editBranchPrefix", |  | ||||||
|                         uiIcon: "bx bx-rename", |                         uiIcon: "bx bx-rename", | ||||||
|                         enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp |                         enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp | ||||||
|                     }, |                     }, | ||||||
|                     { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp }, |                     { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp }, | ||||||
|  |  | ||||||
|                     { kind: "separator" }, |  | ||||||
|  |  | ||||||
|                     { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, |  | ||||||
|                     { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, |  | ||||||
|                     { |                     { | ||||||
|                         title: t("tree-context-menu.sort-by"), |                         title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, | ||||||
|  |                         command: "duplicateSubtree", | ||||||
|  |                         uiIcon: "bx bx-outline", | ||||||
|  |                         enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp | ||||||
|  |                     }, | ||||||
|  |  | ||||||
|  |                     { title: "----" }, | ||||||
|  |  | ||||||
|  |                     { title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, | ||||||
|  |                     { title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, | ||||||
|  |                     { | ||||||
|  |                         title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, | ||||||
|                         command: "sortChildNotes", |                         command: "sortChildNotes", | ||||||
|                         keyboardShortcut: "sortChildNotes", |  | ||||||
|                         uiIcon: "bx bx-sort-down", |                         uiIcon: "bx bx-sort-down", | ||||||
|                         enabled: noSelectedNotes && notSearch |                         enabled: noSelectedNotes && notSearch | ||||||
|                     }, |                     }, | ||||||
|  |  | ||||||
|                     { kind: "separator" }, |                     { title: "----" }, | ||||||
|  |  | ||||||
|                     { title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true }, |                     { title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true }, | ||||||
|                     { title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp } |                     { title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp } | ||||||
|                 ] |                 ] | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.cut"), |                 title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, | ||||||
|                 command: "cutNotesToClipboard", |                 command: "cutNotesToClipboard", | ||||||
|                 keyboardShortcut: "cutNotesToClipboard", |  | ||||||
|                 uiIcon: "bx bx-cut", |                 uiIcon: "bx bx-cut", | ||||||
|                 enabled: isNotRoot && !isHoisted && parentNotSearch |                 enabled: isNotRoot && !isHoisted && parentNotSearch | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { title: t("tree-context-menu.copy-clone"), command: "copyNotesToClipboard", keyboardShortcut: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted }, |             { title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.paste-into"), |                 title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, | ||||||
|                 command: "pasteNotesFromClipboard", |                 command: "pasteNotesFromClipboard", | ||||||
|                 keyboardShortcut: "pasteNotesFromClipboard", |  | ||||||
|                 uiIcon: "bx bx-paste", |                 uiIcon: "bx bx-paste", | ||||||
|                 enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes |                 enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes | ||||||
|             }, |             }, | ||||||
| @@ -188,71 +180,32 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.move-to"), |                 title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, | ||||||
|                 command: "moveNotesTo", |                 command: "moveNotesTo", | ||||||
|                 keyboardShortcut: "moveNotesTo", |  | ||||||
|                 uiIcon: "bx bx-transfer", |                 uiIcon: "bx bx-transfer", | ||||||
|                 enabled: isNotRoot && !isHoisted && parentNotSearch |                 enabled: isNotRoot && !isHoisted && parentNotSearch | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { title: t("tree-context-menu.clone-to"), command: "cloneNotesTo", keyboardShortcut: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted }, |             { title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.duplicate"), |                 title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, | ||||||
|                 command: "duplicateSubtree", |  | ||||||
|                 keyboardShortcut: "duplicateSubtree", |  | ||||||
|                 uiIcon: "bx bx-outline", |  | ||||||
|                 enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp |  | ||||||
|             }, |  | ||||||
|  |  | ||||||
|             { |  | ||||||
|                 title: !isArchived ? t("tree-context-menu.archive") : t("tree-context-menu.unarchive"), |  | ||||||
|                 uiIcon: !isArchived ? "bx bx-archive" : "bx bx-archive-out", |  | ||||||
|                 enabled: canToggleArchived, |  | ||||||
|                 handler: () => { |  | ||||||
|                     if (!selectedNotes.length) return; |  | ||||||
|  |  | ||||||
|                     if (selectedNotes.length == 1) { |  | ||||||
|                         const note = selectedNotes[0]; |  | ||||||
|                         if (!isArchived) { |  | ||||||
|                             attributes.addLabel(note.noteId, "archived"); |  | ||||||
|                         } else { |  | ||||||
|                             attributes.removeOwnedLabelByName(note, "archived"); |  | ||||||
|                         } |  | ||||||
|                     } else { |  | ||||||
|                         const noteIds = selectedNotes.map(note => note.noteId); |  | ||||||
|                         if (!isArchived) { |  | ||||||
|                             executeBulkActions(noteIds, [{ |  | ||||||
|                                 name: "addLabel", labelName: "archived" |  | ||||||
|                             }]); |  | ||||||
|                         } else { |  | ||||||
|                             executeBulkActions(noteIds, [{ |  | ||||||
|                                 name: "deleteLabel", labelName: "archived" |  | ||||||
|                             }]); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 title: t("tree-context-menu.delete"), |  | ||||||
|                 command: "deleteNotes", |                 command: "deleteNotes", | ||||||
|                 keyboardShortcut: "deleteNotes", |  | ||||||
|                 uiIcon: "bx bx-trash destructive-action-icon", |                 uiIcon: "bx bx-trash destructive-action-icon", | ||||||
|                 enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp |                 enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp }, |             { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp }, | ||||||
|  |  | ||||||
|             { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp }, |             { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp }, | ||||||
|  |  | ||||||
|             { kind: "separator" }, |             { title: "----" }, | ||||||
|  |  | ||||||
|             { |             { | ||||||
|                 title: t("tree-context-menu.search-in-subtree"), |                 title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, | ||||||
|                 command: "searchInSubtree", |                 command: "searchInSubtree", | ||||||
|                 keyboardShortcut: "searchInSubtree", |  | ||||||
|                 uiIcon: "bx bx-search", |                 uiIcon: "bx bx-search", | ||||||
|                 enabled: notSearch && noSelectedNotes |                 enabled: notSearch && noSelectedNotes | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import appContext from "./components/app_context.js"; | import appContext from "./components/app_context.js"; | ||||||
| import noteAutocompleteService from "./services/note_autocomplete.js"; | import noteAutocompleteService from "./services/note_autocomplete.js"; | ||||||
| import glob from "./services/glob.js"; | import glob from "./services/glob.js"; | ||||||
| import "bootstrap/dist/css/bootstrap.min.css"; | import "./stylesheets/bootstrap.scss"; | ||||||
| import "boxicons/css/boxicons.min.css"; | import "boxicons/css/boxicons.min.css"; | ||||||
| import "autocomplete.js/index_jquery.js"; | import "autocomplete.js/index_jquery.js"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -79,19 +79,7 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b | |||||||
|     return $container; |     return $container; | ||||||
| } | } | ||||||
|  |  | ||||||
| const HIDDEN_ATTRIBUTES = [ | const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName"]; | ||||||
|     "originalFileName", |  | ||||||
|     "fileSize", |  | ||||||
|     "template", |  | ||||||
|     "inherit", |  | ||||||
|     "cssClass", |  | ||||||
|     "iconClass", |  | ||||||
|     "pageSize", |  | ||||||
|     "viewType", |  | ||||||
|     "geolocation", |  | ||||||
|     "docName", |  | ||||||
|     "webViewSrc" |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| async function renderNormalAttributes(note: FNote) { | async function renderNormalAttributes(note: FNote) { | ||||||
|     const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); |     const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import server from "./server.js"; | |||||||
| import froca from "./froca.js"; | import froca from "./froca.js"; | ||||||
| import type FNote from "../entities/fnote.js"; | import type FNote from "../entities/fnote.js"; | ||||||
| import type { AttributeRow } from "./load_results.js"; | import type { AttributeRow } from "./load_results.js"; | ||||||
| import { AttributeType } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) { | async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) { | ||||||
|     await server.put(`notes/${noteId}/attribute`, { |     await server.put(`notes/${noteId}/attribute`, { | ||||||
| @@ -13,12 +12,11 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) { | export async function setLabel(noteId: string, name: string, value: string = "") { | ||||||
|     await server.put(`notes/${noteId}/set-attribute`, { |     await server.put(`notes/${noteId}/set-attribute`, { | ||||||
|         type: "label", |         type: "label", | ||||||
|         name: name, |         name: name, | ||||||
|         value: value, |         value: value | ||||||
|         isInheritable |  | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -26,14 +24,6 @@ async function removeAttributeById(noteId: string, attributeId: string) { | |||||||
|     await server.remove(`notes/${noteId}/attributes/${attributeId}`); |     await server.remove(`notes/${noteId}/attributes/${attributeId}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) { |  | ||||||
|     for (const attr of note.getOwnedAttributes()) { |  | ||||||
|         if (attr.type === type && attr.name === name) { |  | ||||||
|             await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e. |  * Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e. | ||||||
|  * it will not remove inherited attributes. |  * it will not remove inherited attributes. | ||||||
| @@ -61,7 +51,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) { | |||||||
|  * @param value the value of the attribute to set. |  * @param value the value of the attribute to set. | ||||||
|  */ |  */ | ||||||
| export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { | export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { | ||||||
|     if (value !== null && value !== undefined) { |     if (value) { | ||||||
|         // Create or update the attribute. |         // Create or update the attribute. | ||||||
|         await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); |         await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -95,15 +95,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) { | ||||||
|  * Shows the delete confirmation screen |  | ||||||
|  * |  | ||||||
|  * @param branchIdsToDelete the list of branch IDs to delete. |  | ||||||
|  * @param forceDeleteAllClones whether to check by default the "Delete also all clones" checkbox. |  | ||||||
|  * @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s). |  | ||||||
|  * @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded. |  | ||||||
|  */ |  | ||||||
| async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) { |  | ||||||
|     branchIdsToDelete = filterRootNote(branchIdsToDelete); |     branchIdsToDelete = filterRootNote(branchIdsToDelete); | ||||||
|  |  | ||||||
|     if (branchIdsToDelete.length === 0) { |     if (branchIdsToDelete.length === 0) { | ||||||
| @@ -118,12 +110,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (moveToParent) { |     try { | ||||||
|         try { |         await activateParentNotePath(); | ||||||
|             await activateParentNotePath(); |     } catch (e) { | ||||||
|         } catch (e) { |         console.error(e); | ||||||
|             console.error(e); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const taskId = utils.randomString(10); |     const taskId = utils.randomString(10); | ||||||
| @@ -210,7 +200,7 @@ function makeToast(id: string, message: string): ToastOptions { | |||||||
| } | } | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async (message) => { | ws.subscribeToMessages(async (message) => { | ||||||
|     if (!("taskType" in message) || message.taskType !== "deleteNotes") { |     if (message.taskType !== "deleteNotes") { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -228,7 +218,7 @@ ws.subscribeToMessages(async (message) => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async (message) => { | ws.subscribeToMessages(async (message) => { | ||||||
|     if (!("taskType" in message) || message.taskType !== "undeleteNotes") { |     if (message.taskType !== "undeleteNotes") { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,10 +15,8 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation | |||||||
| import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; | import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; | ||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import type FNote from "../entities/fnote.js"; | import type FNote from "../entities/fnote.js"; | ||||||
| import toast from "./toast.js"; |  | ||||||
| import { BulkAction } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| export const ACTION_GROUPS = [ | const ACTION_GROUPS = [ | ||||||
|     { |     { | ||||||
|         title: t("bulk_actions.labels"), |         title: t("bulk_actions.labels"), | ||||||
|         actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction] |         actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction] | ||||||
| @@ -91,17 +89,6 @@ function parseActions(note: FNote) { | |||||||
|         .filter((action) => !!action); |         .filter((action) => !!action); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function executeBulkActions(targetNoteIds: string[], actions: BulkAction[], includeDescendants = false) { |  | ||||||
|     await server.post("bulk-action/execute", { |  | ||||||
|         noteIds: targetNoteIds, |  | ||||||
|         includeDescendants, |  | ||||||
|         actions |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await ws.waitForMaxKnownEntityChangeId(); |  | ||||||
|     toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     addAction, |     addAction, | ||||||
|     parseActions, |     parseActions, | ||||||
|   | |||||||
| @@ -1,295 +0,0 @@ | |||||||
| import { ActionKeyboardShortcut } from "@triliumnext/commons"; |  | ||||||
| import appContext, { type CommandNames } from "../components/app_context.js"; |  | ||||||
| import type NoteTreeWidget from "../widgets/note_tree.js"; |  | ||||||
| import { t, translationsInitializedPromise } from "./i18n.js"; |  | ||||||
| import keyboardActions from "./keyboard_actions.js"; |  | ||||||
| import utils from "./utils.js"; |  | ||||||
|  |  | ||||||
| export interface CommandDefinition { |  | ||||||
|     id: string; |  | ||||||
|     name: string; |  | ||||||
|     description?: string; |  | ||||||
|     icon?: string; |  | ||||||
|     shortcut?: string; |  | ||||||
|     commandName?: CommandNames; |  | ||||||
|     handler?: () => Promise<unknown> | null | undefined | void; |  | ||||||
|     aliases?: string[]; |  | ||||||
|     source?: "manual" | "keyboard-action"; |  | ||||||
|     /** Reference to the original keyboard action for scope checking. */ |  | ||||||
|     keyboardAction?: ActionKeyboardShortcut; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class CommandRegistry { |  | ||||||
|     private commands: Map<string, CommandDefinition> = new Map(); |  | ||||||
|     private aliases: Map<string, string> = new Map(); |  | ||||||
|  |  | ||||||
|     constructor() { |  | ||||||
|         this.loadCommands(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async loadCommands() { |  | ||||||
|         await translationsInitializedPromise; |  | ||||||
|         this.registerDefaultCommands(); |  | ||||||
|         await this.loadKeyboardActionsAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private registerDefaultCommands() { |  | ||||||
|         this.register({ |  | ||||||
|             id: "export-note", |  | ||||||
|             name: t("command_palette.export_note_title"), |  | ||||||
|             description: t("command_palette.export_note_description"), |  | ||||||
|             icon: "bx bx-export", |  | ||||||
|             handler: () => { |  | ||||||
|                 const notePath = appContext.tabManager.getActiveContextNotePath(); |  | ||||||
|                 if (notePath) { |  | ||||||
|                     appContext.triggerCommand("showExportDialog", { |  | ||||||
|                         notePath, |  | ||||||
|                         defaultType: "single" |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         this.register({ |  | ||||||
|             id: "show-attachments", |  | ||||||
|             name: t("command_palette.show_attachments_title"), |  | ||||||
|             description: t("command_palette.show_attachments_description"), |  | ||||||
|             icon: "bx bx-paperclip", |  | ||||||
|             handler: () => appContext.triggerCommand("showAttachments") |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         // Special search commands with custom logic |  | ||||||
|         this.register({ |  | ||||||
|             id: "search-notes", |  | ||||||
|             name: t("command_palette.search_notes_title"), |  | ||||||
|             description: t("command_palette.search_notes_description"), |  | ||||||
|             icon: "bx bx-search", |  | ||||||
|             handler: () => appContext.triggerCommand("searchNotes", {}) |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         this.register({ |  | ||||||
|             id: "search-in-subtree", |  | ||||||
|             name: t("command_palette.search_subtree_title"), |  | ||||||
|             description: t("command_palette.search_subtree_description"), |  | ||||||
|             icon: "bx bx-search-alt", |  | ||||||
|             handler: () => { |  | ||||||
|                 const notePath = appContext.tabManager.getActiveContextNotePath(); |  | ||||||
|                 if (notePath) { |  | ||||||
|                     appContext.triggerCommand("searchInSubtree", { notePath }); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         this.register({ |  | ||||||
|             id: "show-search-history", |  | ||||||
|             name: t("command_palette.search_history_title"), |  | ||||||
|             description: t("command_palette.search_history_description"), |  | ||||||
|             icon: "bx bx-history", |  | ||||||
|             handler: () => appContext.triggerCommand("showSearchHistory") |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         this.register({ |  | ||||||
|             id: "show-launch-bar", |  | ||||||
|             name: t("command_palette.configure_launch_bar_title"), |  | ||||||
|             description: t("command_palette.configure_launch_bar_description"), |  | ||||||
|             icon: "bx bx-sidebar", |  | ||||||
|             handler: () => appContext.triggerCommand("showLaunchBarSubtree") |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async loadKeyboardActionsAsync() { |  | ||||||
|         try { |  | ||||||
|             const actions = await keyboardActions.getActions(); |  | ||||||
|             this.registerKeyboardActions(actions); |  | ||||||
|         } catch (error) { |  | ||||||
|             console.error("Failed to load keyboard actions:", error); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private registerKeyboardActions(actions: ActionKeyboardShortcut[]) { |  | ||||||
|         for (const action of actions) { |  | ||||||
|             // Skip actions that we've already manually registered |  | ||||||
|             if (this.commands.has(action.actionName)) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Skip actions that don't have a description (likely separators) |  | ||||||
|             if (!action.description) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Skip Electron-only actions if not in Electron environment |  | ||||||
|             if (action.isElectronOnly && !utils.isElectron()) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Skip actions that should not appear in the command palette |  | ||||||
|             if (action.ignoreFromCommandPalette) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Get the primary shortcut (first one in the list) |  | ||||||
|             const primaryShortcut = action.effectiveShortcuts?.[0]; |  | ||||||
|  |  | ||||||
|             let name = action.friendlyName; |  | ||||||
|             if (action.scope === "note-tree") { |  | ||||||
|                 name = t("command_palette.tree-action-name", { name: action.friendlyName }); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Create a command definition from the keyboard action |  | ||||||
|             const commandDef: CommandDefinition = { |  | ||||||
|                 id: action.actionName, |  | ||||||
|                 name, |  | ||||||
|                 description: action.description, |  | ||||||
|                 icon: action.iconClass, |  | ||||||
|                 shortcut: primaryShortcut ? this.formatShortcut(primaryShortcut) : undefined, |  | ||||||
|                 commandName: action.actionName as CommandNames, |  | ||||||
|                 source: "keyboard-action", |  | ||||||
|                 keyboardAction: action |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             this.register(commandDef); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private formatShortcut(shortcut: string): string { |  | ||||||
|         // Convert electron accelerator format to display format |  | ||||||
|         return shortcut |  | ||||||
|             .replace(/CommandOrControl/g, 'Ctrl') |  | ||||||
|             .replace(/\+/g, ' + '); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     register(command: CommandDefinition) { |  | ||||||
|         this.commands.set(command.id, command); |  | ||||||
|  |  | ||||||
|         // Register aliases |  | ||||||
|         if (command.aliases) { |  | ||||||
|             for (const alias of command.aliases) { |  | ||||||
|                 this.aliases.set(alias.toLowerCase(), command.id); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     getCommand(id: string): CommandDefinition | undefined { |  | ||||||
|         return this.commands.get(id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     getAllCommands(): CommandDefinition[] { |  | ||||||
|         const commands = Array.from(this.commands.values()); |  | ||||||
|  |  | ||||||
|         // Sort commands by name |  | ||||||
|         commands.sort((a, b) => a.name.localeCompare(b.name)); |  | ||||||
|  |  | ||||||
|         return commands; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     searchCommands(query: string): CommandDefinition[] { |  | ||||||
|         const normalizedQuery = query.toLowerCase(); |  | ||||||
|         const results: { command: CommandDefinition; score: number }[] = []; |  | ||||||
|  |  | ||||||
|         for (const command of this.commands.values()) { |  | ||||||
|             let score = 0; |  | ||||||
|  |  | ||||||
|             // Exact match on name |  | ||||||
|             if (command.name.toLowerCase() === normalizedQuery) { |  | ||||||
|                 score = 100; |  | ||||||
|             } |  | ||||||
|             // Name starts with query |  | ||||||
|             else if (command.name.toLowerCase().startsWith(normalizedQuery)) { |  | ||||||
|                 score = 80; |  | ||||||
|             } |  | ||||||
|             // Name contains query |  | ||||||
|             else if (command.name.toLowerCase().includes(normalizedQuery)) { |  | ||||||
|                 score = 60; |  | ||||||
|             } |  | ||||||
|             // Description contains query |  | ||||||
|             else if (command.description?.toLowerCase().includes(normalizedQuery)) { |  | ||||||
|                 score = 40; |  | ||||||
|             } |  | ||||||
|             // Check aliases |  | ||||||
|             else if (command.aliases?.some(alias => alias.toLowerCase().includes(normalizedQuery))) { |  | ||||||
|                 score = 50; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (score > 0) { |  | ||||||
|                 results.push({ command, score }); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Sort by score (highest first) and then by name |  | ||||||
|         results.sort((a, b) => { |  | ||||||
|             if (a.score !== b.score) { |  | ||||||
|                 return b.score - a.score; |  | ||||||
|             } |  | ||||||
|             return a.command.name.localeCompare(b.command.name); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         return results.map(r => r.command); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async executeCommand(commandId: string) { |  | ||||||
|         const command = this.getCommand(commandId); |  | ||||||
|         if (!command) { |  | ||||||
|             console.error(`Command not found: ${commandId}`); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Execute custom handler if provided |  | ||||||
|         if (command.handler) { |  | ||||||
|             await command.handler(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Handle keyboard action with scope-aware execution |  | ||||||
|         if (command.keyboardAction && command.commandName) { |  | ||||||
|             if (command.keyboardAction.scope === "note-tree") { |  | ||||||
|                 this.executeWithNoteTreeFocus(command.commandName); |  | ||||||
|             } else if (command.keyboardAction.scope === "text-detail") { |  | ||||||
|                 this.executeWithTextDetail(command.commandName); |  | ||||||
|             } else { |  | ||||||
|                 appContext.triggerCommand(command.commandName, { |  | ||||||
|                     ntxId: appContext.tabManager.activeNtxId |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Fallback for commands without keyboard action reference |  | ||||||
|         if (command.commandName) { |  | ||||||
|             appContext.triggerCommand(command.commandName, { |  | ||||||
|                 ntxId: appContext.tabManager.activeNtxId |  | ||||||
|             }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         console.error(`Command ${commandId} has no handler or commandName`); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private executeWithNoteTreeFocus(actionName: CommandNames) { |  | ||||||
|         const tree = document.querySelector(".tree-wrapper") as HTMLElement; |  | ||||||
|         if (!tree) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const treeComponent = appContext.getComponentByEl(tree) as NoteTreeWidget; |  | ||||||
|         const activeNode = treeComponent.getActiveNode(); |  | ||||||
|         treeComponent.triggerCommand(actionName, { |  | ||||||
|             ntxId: appContext.tabManager.activeNtxId, |  | ||||||
|             node: activeNode |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async executeWithTextDetail(actionName: CommandNames) { |  | ||||||
|         const typeWidget = await appContext.tabManager.getActiveContext()?.getTypeWidget(); |  | ||||||
|         if (!typeWidget) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         typeWidget.triggerCommand(actionName, { |  | ||||||
|             ntxId: appContext.tabManager.activeNtxId |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const commandRegistry = new CommandRegistry(); |  | ||||||
| export default commandRegistry; |  | ||||||
| @@ -65,9 +65,6 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FA | |||||||
|  |  | ||||||
|         $renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button)); |         $renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button)); | ||||||
|     } else if (entity instanceof FNote) { |     } else if (entity instanceof FNote) { | ||||||
|         $renderedContent |  | ||||||
|             .css("display", "flex") |  | ||||||
|             .css("flex-direction", "column"); |  | ||||||
|         $renderedContent.append( |         $renderedContent.append( | ||||||
|             $("<div>") |             $("<div>") | ||||||
|                 .css("display", "flex") |                 .css("display", "flex") | ||||||
| @@ -75,33 +72,8 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FA | |||||||
|                 .css("align-items", "center") |                 .css("align-items", "center") | ||||||
|                 .css("height", "100%") |                 .css("height", "100%") | ||||||
|                 .css("font-size", "500%") |                 .css("font-size", "500%") | ||||||
|                 .css("flex-grow", "1") |  | ||||||
|                 .append($("<span>").addClass(entity.getIcon())) |                 .append($("<span>").addClass(entity.getIcon())) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         if (entity.type === "webView" && entity.hasLabel("webViewSrc")) { |  | ||||||
|             const $footer = $("<footer>") |  | ||||||
|                 .addClass("webview-footer"); |  | ||||||
|             const $openButton = $(` |  | ||||||
|                 <button class="file-open btn btn-primary" type="button"> |  | ||||||
|                     <span class="bx bx-link-external"></span> |  | ||||||
|                     ${t("content_renderer.open_externally")} |  | ||||||
|                 </button> |  | ||||||
|             `) |  | ||||||
|                 .appendTo($footer) |  | ||||||
|                 .on("click", () => { |  | ||||||
|                     const webViewSrc = entity.getLabelValue("webViewSrc"); |  | ||||||
|                     if (webViewSrc) { |  | ||||||
|                         if (utils.isElectron()) { |  | ||||||
|                             const electron = utils.dynamicRequire("electron"); |  | ||||||
|                             electron.shell.openExternal(webViewSrc); |  | ||||||
|                         } else { |  | ||||||
|                             window.open(webViewSrc, '_blank', 'noopener,noreferrer'); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             $footer.appendTo($renderedContent); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (entity instanceof FNote) { |     if (entity instanceof FNote) { | ||||||
| @@ -256,19 +228,8 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: | |||||||
|             </button> |             </button> | ||||||
|         `); |         `); | ||||||
|  |  | ||||||
|         $downloadButton.on("click", (e) => { |         $downloadButton.on("click", () => openService.downloadFileNote(entity.noteId)); | ||||||
|             e.stopPropagation(); |         $openButton.on("click", () => openService.openNoteExternally(entity.noteId, entity.mime)); | ||||||
|             openService.downloadFileNote(entity.noteId) |  | ||||||
|         }); |  | ||||||
|         $openButton.on("click", async (e) => { |  | ||||||
|             const iconEl = $openButton.find("> .bx"); |  | ||||||
|             iconEl.removeClass("bx bx-link-external"); |  | ||||||
|             iconEl.addClass("bx bx-loader spin"); |  | ||||||
|             e.stopPropagation(); |  | ||||||
|             await openService.openNoteExternally(entity.noteId, entity.mime) |  | ||||||
|             iconEl.removeClass("bx bx-loader spin"); |  | ||||||
|             iconEl.addClass("bx bx-link-external"); |  | ||||||
|         }); |  | ||||||
|         // open doesn't work for protected notes since it works through a browser which isn't in protected session |         // open doesn't work for protected notes since it works through a browser which isn't in protected session | ||||||
|         $openButton.toggle(!entity.isProtected); |         $openButton.toggle(!entity.isProtected); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -41,14 +41,8 @@ async function info(message: string) { | |||||||
|     return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); |     return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Displays a confirmation dialog with the given message. |  | ||||||
|  * |  | ||||||
|  * @param message the message to display in the dialog. |  | ||||||
|  * @returns A promise that resolves to true if the user confirmed, false otherwise. |  | ||||||
|  */ |  | ||||||
| async function confirm(message: string) { | async function confirm(message: string) { | ||||||
|     return new Promise<boolean>((res) => |     return new Promise((res) => | ||||||
|         appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{ |         appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{ | ||||||
|             message, |             message, | ||||||
|             callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) |             callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) | ||||||
| @@ -60,7 +54,7 @@ async function confirmDeleteNoteBoxWithNote(title: string) { | |||||||
|     return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res })); |     return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res })); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function prompt(props: PromptDialogOptions) { | async function prompt(props: PromptDialogOptions) { | ||||||
|     return new Promise<string | null>((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res })); |     return new Promise<string | null>((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res })); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -48,6 +48,6 @@ function getUrl(docNameValue: string, language: string) { | |||||||
|     // Cannot have spaces in the URL due to how JQuery.load works. |     // Cannot have spaces in the URL due to how JQuery.load works. | ||||||
|     docNameValue = docNameValue.replaceAll(" ", "%20"); |     docNameValue = docNameValue.replaceAll(" ", "%20"); | ||||||
|  |  | ||||||
|     const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath; |     const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath; | ||||||
|     return `${basePath}/doc_notes/${language}/${docNameValue}.html`; |     return `${basePath}/doc_notes/${language}/${docNameValue}.html`; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,16 @@ | |||||||
| import ws from "./ws.js"; | import ws from "./ws.js"; | ||||||
| import appContext from "../components/app_context.js"; | import appContext from "../components/app_context.js"; | ||||||
| import { OpenedFileUpdateStatus } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| const fileModificationStatus: Record<string, Record<string, OpenedFileUpdateStatus>> = { | // TODO: Deduplicate | ||||||
|  | interface Message { | ||||||
|  |     type: string; | ||||||
|  |     entityType: string; | ||||||
|  |     entityId: string; | ||||||
|  |     lastModifiedMs: number; | ||||||
|  |     filePath: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const fileModificationStatus: Record<string, Record<string, Message>> = { | ||||||
|     notes: {}, |     notes: {}, | ||||||
|     attachments: {} |     attachments: {} | ||||||
| }; | }; | ||||||
| @@ -31,7 +39,7 @@ function ignoreModification(entityType: string, entityId: string) { | |||||||
|     delete fileModificationStatus[entityType][entityId]; |     delete fileModificationStatus[entityType][entityId]; | ||||||
| } | } | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async message => { | ws.subscribeToMessages(async (message: Message) => { | ||||||
|     if (message.type !== "openedFileUpdated") { |     if (message.type !== "openedFileUpdated") { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; | |||||||
| import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; | import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; | ||||||
| import type { default as FNote, FNoteRow } from "../entities/fnote.js"; | import type { default as FNote, FNoteRow } from "../entities/fnote.js"; | ||||||
| import type { EntityChange } from "../server_types.js"; | import type { EntityChange } from "../server_types.js"; | ||||||
| import type { OptionNames } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| async function processEntityChanges(entityChanges: EntityChange[]) { | async function processEntityChanges(entityChanges: EntityChange[]) { | ||||||
|     const loadResults = new LoadResults(entityChanges); |     const loadResults = new LoadResults(entityChanges); | ||||||
| @@ -31,14 +30,13 @@ async function processEntityChanges(entityChanges: EntityChange[]) { | |||||||
|                     continue; // only noise |                     continue; // only noise | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 options.set(attributeEntity.name as OptionNames, attributeEntity.value); |                 options.set(attributeEntity.name, attributeEntity.value); | ||||||
|                 loadResults.addOption(attributeEntity.name as OptionNames); |  | ||||||
|  |                 loadResults.addOption(attributeEntity.name); | ||||||
|             } else if (ec.entityName === "attachments") { |             } else if (ec.entityName === "attachments") { | ||||||
|                 processAttachment(loadResults, ec); |                 processAttachment(loadResults, ec); | ||||||
|             } else if (ec.entityName === "blobs") { |             } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { | ||||||
|                 // NOOP - these entities are handled at the backend level and don't require frontend processing |                 // NOOP - these entities are handled at the backend level and don't require frontend processing | ||||||
|             } else if (ec.entityName === "etapi_tokens") { |  | ||||||
|                 loadResults.hasEtapiTokenChanges = true; |  | ||||||
|             } else { |             } else { | ||||||
|                 throw new Error(`Unknown entityName '${ec.entityName}'`); |                 throw new Error(`Unknown entityName '${ec.entityName}'`); | ||||||
|             } |             } | ||||||
| @@ -79,7 +77,9 @@ async function processEntityChanges(entityChanges: EntityChange[]) { | |||||||
|             noteAttributeCache.invalidate(); |             noteAttributeCache.invalidate(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const appContext = (await import("../components/app_context.js")).default; |         // TODO: Remove after porting the file | ||||||
|  |         // @ts-ignore | ||||||
|  |         const appContext = (await import("../components/app_context.js")).default as any; | ||||||
|         await appContext.triggerEvent("entitiesReloaded", { loadResults }); |         await appContext.triggerEvent("entitiesReloaded", { loadResults }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,7 +21,6 @@ import dayjs from "dayjs"; | |||||||
| import type NoteContext from "../components/note_context.js"; | import type NoteContext from "../components/note_context.js"; | ||||||
| import type NoteDetailWidget from "../widgets/note_detail.js"; | import type NoteDetailWidget from "../widgets/note_detail.js"; | ||||||
| import type Component from "../components/component.js"; | import type Component from "../components/component.js"; | ||||||
| import { formatLogMessage } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A whole number |  * A whole number | ||||||
| @@ -456,7 +455,7 @@ export interface Api { | |||||||
|     /** |     /** | ||||||
|      * Log given message to the log pane in UI |      * Log given message to the log pane in UI | ||||||
|      */ |      */ | ||||||
|     log(message: string | object): void; |     log(message: string): void; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -697,7 +696,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig | |||||||
|     this.log = (message) => { |     this.log = (message) => { | ||||||
|         const { noteId } = this.startNote; |         const { noteId } = this.startNote; | ||||||
|  |  | ||||||
|         message = `${utils.now()}: ${formatLogMessage(message)}`; |         message = `${utils.now()}: ${message}`; | ||||||
|  |  | ||||||
|         console.log(`Script ${noteId}: ${message}`); |         console.log(`Script ${noteId}: ${message}`); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,21 +3,14 @@ import i18next from "i18next"; | |||||||
| import i18nextHttpBackend from "i18next-http-backend"; | import i18nextHttpBackend from "i18next-http-backend"; | ||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
| import type { Locale } from "@triliumnext/commons"; | import type { Locale } from "@triliumnext/commons"; | ||||||
| import { initReactI18next } from "react-i18next"; |  | ||||||
|  |  | ||||||
| let locales: Locale[] | null; | let locales: Locale[] | null; | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A deferred promise that resolves when translations are initialized. |  | ||||||
|  */ |  | ||||||
| export let translationsInitializedPromise = $.Deferred(); |  | ||||||
|  |  | ||||||
| export async function initLocale() { | export async function initLocale() { | ||||||
|     const locale = (options.get("locale") as string) || "en"; |     const locale = (options.get("locale") as string) || "en"; | ||||||
|  |  | ||||||
|     locales = await server.get<Locale[]>("options/locales"); |     locales = await server.get<Locale[]>("options/locales"); | ||||||
|  |  | ||||||
|     i18next.use(initReactI18next); |  | ||||||
|     await i18next.use(i18nextHttpBackend).init({ |     await i18next.use(i18nextHttpBackend).init({ | ||||||
|         lng: locale, |         lng: locale, | ||||||
|         fallbackLng: "en", |         fallbackLng: "en", | ||||||
| @@ -26,8 +19,6 @@ export async function initLocale() { | |||||||
|         }, |         }, | ||||||
|         returnEmptyString: false |         returnEmptyString: false | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     translationsInitializedPromise.resolve(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getAvailableLocales() { | export function getAvailableLocales() { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import toastService, { showError } from "./toast.js"; | import toastService, { showError } from "./toast.js"; | ||||||
|  |  | ||||||
| export function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) { | function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) { | ||||||
|     try { |     try { | ||||||
|         $imageWrapper.attr("contenteditable", "true"); |         $imageWrapper.attr("contenteditable", "true"); | ||||||
|         selectImage($imageWrapper.get(0)); |         selectImage($imageWrapper.get(0)); | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import ws from "./ws.js"; | |||||||
| import utils from "./utils.js"; | import utils from "./utils.js"; | ||||||
| import appContext from "../components/app_context.js"; | import appContext from "../components/app_context.js"; | ||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import { WebSocketMessage } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| type BooleanLike = boolean | "true" | "false"; | type BooleanLike = boolean | "true" | "false"; | ||||||
|  |  | ||||||
| @@ -67,7 +66,7 @@ function makeToast(id: string, message: string): ToastOptions { | |||||||
| } | } | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async (message) => { | ws.subscribeToMessages(async (message) => { | ||||||
|     if (!("taskType" in message) || message.taskType !== "importNotes") { |     if (message.taskType !== "importNotes") { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -88,8 +87,8 @@ ws.subscribeToMessages(async (message) => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async (message: WebSocketMessage) => { | ws.subscribeToMessages(async (message) => { | ||||||
|     if (!("taskType" in message) || message.taskType !== "importAttachments") { |     if (message.taskType !== "importAttachments") { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,43 +0,0 @@ | |||||||
| import { NoteType } from "@triliumnext/commons"; |  | ||||||
| import FNote from "../entities/fnote"; |  | ||||||
| import { ViewTypeOptions } from "../widgets/collections/interface"; |  | ||||||
|  |  | ||||||
| export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = { |  | ||||||
|     canvas: null, |  | ||||||
|     code: null, |  | ||||||
|     contentWidget: null, |  | ||||||
|     doc: null, |  | ||||||
|     file: null, |  | ||||||
|     image: null, |  | ||||||
|     launcher: null, |  | ||||||
|     mermaid: null, |  | ||||||
|     mindMap: null, |  | ||||||
|     noteMap: null, |  | ||||||
|     relationMap: null, |  | ||||||
|     render: null, |  | ||||||
|     search: null, |  | ||||||
|     text: null, |  | ||||||
|     webView: null, |  | ||||||
|     aiChat: null |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const byBookType: Record<ViewTypeOptions, string | null> = { |  | ||||||
|     list: "mULW0Q3VojwY", |  | ||||||
|     grid: "8QqnMzx393bx", |  | ||||||
|     calendar: "xWbu3jpNWapp", |  | ||||||
|     table: "2FvYrpmOXm29", |  | ||||||
|     geoMap: "81SGnPGMk7Xc", |  | ||||||
|     board: "CtBQqbwXDx1w" |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export function getHelpUrlForNote(note: FNote | null | undefined) { |  | ||||||
|     if (note && note.type !== "book" && byNoteType[note.type]) { |  | ||||||
|         return byNoteType[note.type]; |  | ||||||
|     } else if (note?.hasLabel("calendarRoot")) { |  | ||||||
|         return "l0tKav7yLHGF"; |  | ||||||
|     } else if (note?.hasLabel("textSnippet")) { |  | ||||||
|         return "pwc194wlRzcH"; |  | ||||||
|     } else if (note && note.type === "book") { |  | ||||||
|         return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""] |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -2,15 +2,21 @@ import server from "./server.js"; | |||||||
| import appContext, { type CommandNames } from "../components/app_context.js"; | import appContext, { type CommandNames } from "../components/app_context.js"; | ||||||
| import shortcutService from "./shortcuts.js"; | import shortcutService from "./shortcuts.js"; | ||||||
| import type Component from "../components/component.js"; | import type Component from "../components/component.js"; | ||||||
| import type { ActionKeyboardShortcut } from "@triliumnext/commons"; |  | ||||||
|  |  | ||||||
| const keyboardActionRepo: Record<string, ActionKeyboardShortcut> = {}; | const keyboardActionRepo: Record<string, Action> = {}; | ||||||
|  |  | ||||||
| const keyboardActionsLoaded = server.get<ActionKeyboardShortcut[]>("keyboard-actions").then((actions) => { | // TODO: Deduplicate with server. | ||||||
|  | export interface Action { | ||||||
|  |     actionName: CommandNames; | ||||||
|  |     effectiveShortcuts: string[]; | ||||||
|  |     scope: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const keyboardActionsLoaded = server.get<Action[]>("keyboard-actions").then((actions) => { | ||||||
|     actions = actions.filter((a) => !!a.actionName); // filter out separators |     actions = actions.filter((a) => !!a.actionName); // filter out separators | ||||||
|  |  | ||||||
|     for (const action of actions) { |     for (const action of actions) { | ||||||
|         action.effectiveShortcuts = (action.effectiveShortcuts ?? []).filter((shortcut) => !shortcut.startsWith("global:")); |         action.effectiveShortcuts = action.effectiveShortcuts.filter((shortcut) => !shortcut.startsWith("global:")); | ||||||
|  |  | ||||||
|         keyboardActionRepo[action.actionName] = action; |         keyboardActionRepo[action.actionName] = action; | ||||||
|     } |     } | ||||||
| @@ -32,7 +38,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c | |||||||
|     const actions = await getActionsForScope(scope); |     const actions = await getActionsForScope(scope); | ||||||
|  |  | ||||||
|     for (const action of actions) { |     for (const action of actions) { | ||||||
|         for (const shortcut of action.effectiveShortcuts ?? []) { |         for (const shortcut of action.effectiveShortcuts) { | ||||||
|             shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); |             shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -40,7 +46,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c | |||||||
|  |  | ||||||
| getActionsForScope("window").then((actions) => { | getActionsForScope("window").then((actions) => { | ||||||
|     for (const action of actions) { |     for (const action of actions) { | ||||||
|         for (const shortcut of action.effectiveShortcuts ?? []) { |         for (const shortcut of action.effectiveShortcuts) { | ||||||
|             shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); |             shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -62,10 +68,6 @@ async function getAction(actionName: string, silent = false) { | |||||||
|     return action; |     return action; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getActionSync(actionName: string) { |  | ||||||
|     return keyboardActionRepo[actionName]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function updateDisplayedShortcuts($container: JQuery<HTMLElement>) { | function updateDisplayedShortcuts($container: JQuery<HTMLElement>) { | ||||||
|     //@ts-ignore |     //@ts-ignore | ||||||
|     //TODO: each() does not support async callbacks. |     //TODO: each() does not support async callbacks. | ||||||
| @@ -78,7 +80,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) { | |||||||
|         const action = await getAction(actionName, true); |         const action = await getAction(actionName, true); | ||||||
|  |  | ||||||
|         if (action) { |         if (action) { | ||||||
|             const keyboardActions = (action.effectiveShortcuts ?? []).join(", "); |             const keyboardActions = action.effectiveShortcuts.join(", "); | ||||||
|  |  | ||||||
|             if (keyboardActions || $(el).text() !== "not set") { |             if (keyboardActions || $(el).text() !== "not set") { | ||||||
|                 $(el).text(keyboardActions); |                 $(el).text(keyboardActions); | ||||||
| @@ -97,7 +99,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) { | |||||||
|  |  | ||||||
|         if (action) { |         if (action) { | ||||||
|             const title = $(el).attr("title"); |             const title = $(el).attr("title"); | ||||||
|             const shortcuts = (action.effectiveShortcuts ?? []).join(", "); |             const shortcuts = action.effectiveShortcuts.join(", "); | ||||||
|  |  | ||||||
|             if (title?.includes(shortcuts)) { |             if (title?.includes(shortcuts)) { | ||||||
|                 return; |                 return; | ||||||
|   | |||||||
| @@ -35,7 +35,8 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) { | |||||||
|     return icon; |     return icon; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type ViewMode = "default" | "source" | "attachments" | "contextual-help"; | // TODO: Remove `string` once all the view modes have been mapped. | ||||||
|  | type ViewMode = "default" | "source" | "attachments" | "contextual-help" | string; | ||||||
|  |  | ||||||
| export interface ViewScope { | export interface ViewScope { | ||||||
|     /** |     /** | ||||||
| @@ -315,7 +316,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | |||||||
|     const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey; |     const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey; | ||||||
|  |  | ||||||
|     if (notePath) { |     if (notePath) { | ||||||
|         if (isLeftClick && openInPopup) { |         if (openInPopup) { | ||||||
|             appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); |             appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); | ||||||
|         } else if (openInNewWindow) { |         } else if (openInNewWindow) { | ||||||
|             appContext.triggerCommand("openInWindow", { notePath, viewScope }); |             appContext.triggerCommand("openInWindow", { notePath, viewScope }); | ||||||
| @@ -404,7 +405,7 @@ function linkContextMenu(e: PointerEvent) { | |||||||
|     linkContextMenuService.openContextMenu(notePath, e, viewScope, null); |     linkContextMenuService.openContextMenu(notePath, e, viewScope, null); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) { | export async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) { | ||||||
|     const $link = $el[0].tagName === "A" ? $el : $el.find("a"); |     const $link = $el[0].tagName === "A" ? $el : $el.find("a"); | ||||||
|  |  | ||||||
|     href = href || $link.attr("href"); |     href = href || $link.attr("href"); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import type { AttachmentRow, EtapiTokenRow, OptionNames } from "@triliumnext/commons"; | import type { AttachmentRow } from "@triliumnext/commons"; | ||||||
| import type { AttributeType } from "../entities/fattribute.js"; | import type { AttributeType } from "../entities/fattribute.js"; | ||||||
| import type { EntityChange } from "../server_types.js"; | import type { EntityChange } from "../server_types.js"; | ||||||
|  |  | ||||||
| @@ -53,7 +53,6 @@ type EntityRowMappings = { | |||||||
|     options: OptionRow; |     options: OptionRow; | ||||||
|     revisions: RevisionRow; |     revisions: RevisionRow; | ||||||
|     note_reordering: NoteReorderingRow; |     note_reordering: NoteReorderingRow; | ||||||
|     etapi_tokens: EtapiTokenRow; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type EntityRowNames = keyof EntityRowMappings; | export type EntityRowNames = keyof EntityRowMappings; | ||||||
| @@ -67,9 +66,8 @@ export default class LoadResults { | |||||||
|     private revisionRows: RevisionRow[]; |     private revisionRows: RevisionRow[]; | ||||||
|     private noteReorderings: string[]; |     private noteReorderings: string[]; | ||||||
|     private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[]; |     private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[]; | ||||||
|     private optionNames: OptionNames[]; |     private optionNames: string[]; | ||||||
|     private attachmentRows: AttachmentRow[]; |     private attachmentRows: AttachmentRow[]; | ||||||
|     public hasEtapiTokenChanges: boolean = false; |  | ||||||
|  |  | ||||||
|     constructor(entityChanges: EntityChange[]) { |     constructor(entityChanges: EntityChange[]) { | ||||||
|         const entities: Record<string, Record<string, any>> = {}; |         const entities: Record<string, Record<string, any>> = {}; | ||||||
| @@ -180,11 +178,11 @@ export default class LoadResults { | |||||||
|         return this.contentNoteIdToComponentId.find((l) => l.noteId === noteId && l.componentId !== componentId); |         return this.contentNoteIdToComponentId.find((l) => l.noteId === noteId && l.componentId !== componentId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     addOption(name: OptionNames) { |     addOption(name: string) { | ||||||
|         this.optionNames.push(name); |         this.optionNames.push(name); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     isOptionReloaded(name: OptionNames) { |     isOptionReloaded(name: string) { | ||||||
|         return this.optionNames.includes(name); |         return this.optionNames.includes(name); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -217,8 +215,7 @@ export default class LoadResults { | |||||||
|             this.revisionRows.length === 0 && |             this.revisionRows.length === 0 && | ||||||
|             this.contentNoteIdToComponentId.length === 0 && |             this.contentNoteIdToComponentId.length === 0 && | ||||||
|             this.optionNames.length === 0 && |             this.optionNames.length === 0 && | ||||||
|             this.attachmentRows.length === 0 && |             this.attachmentRows.length === 0 | ||||||
|             !this.hasEtapiTokenChanges |  | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import appContext from "../components/app_context.js"; | |||||||
| import noteCreateService from "./note_create.js"; | import noteCreateService from "./note_create.js"; | ||||||
| import froca from "./froca.js"; | import froca from "./froca.js"; | ||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import commandRegistry from "./command_registry.js"; |  | ||||||
| import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5"; | import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5"; | ||||||
|  |  | ||||||
| // this key needs to have this value, so it's hit by the tooltip | // this key needs to have this value, so it's hit by the tooltip | ||||||
| @@ -30,28 +29,18 @@ export interface Suggestion { | |||||||
|     notePathTitle?: string; |     notePathTitle?: string; | ||||||
|     notePath?: string; |     notePath?: string; | ||||||
|     highlightedNotePathTitle?: string; |     highlightedNotePathTitle?: string; | ||||||
|     action?: string | "create-note" | "search-notes" | "external-link" | "command"; |     action?: string | "create-note" | "search-notes" | "external-link"; | ||||||
|     parentNoteId?: string; |     parentNoteId?: string; | ||||||
|     icon?: string; |     icon?: string; | ||||||
|     commandId?: string; |  | ||||||
|     commandDescription?: string; |  | ||||||
|     commandShortcut?: string; |  | ||||||
|     attributeSnippet?: string; |  | ||||||
|     highlightedAttributeSnippet?: string; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface Options { | interface Options { | ||||||
|     container?: HTMLElement | null; |     container?: HTMLElement; | ||||||
|     fastSearch?: boolean; |     fastSearch?: boolean; | ||||||
|     allowCreatingNotes?: boolean; |     allowCreatingNotes?: boolean; | ||||||
|     allowJumpToSearchNotes?: boolean; |     allowJumpToSearchNotes?: boolean; | ||||||
|     allowExternalLinks?: boolean; |     allowExternalLinks?: boolean; | ||||||
|     /** If set, hides the right-side button corresponding to go to selected note. */ |  | ||||||
|     hideGoToSelectedNoteButton?: boolean; |     hideGoToSelectedNoteButton?: boolean; | ||||||
|     /** If set, hides all right-side buttons in the autocomplete dropdown */ |  | ||||||
|     hideAllButtons?: boolean; |  | ||||||
|     /** If set, enables command palette mode */ |  | ||||||
|     isCommandPalette?: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function autocompleteSourceForCKEditor(queryText: string) { | async function autocompleteSourceForCKEditor(queryText: string) { | ||||||
| @@ -81,31 +70,6 @@ async function autocompleteSourceForCKEditor(queryText: string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) { | async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) { | ||||||
|     // Check if we're in command mode |  | ||||||
|     if (options.isCommandPalette && term.startsWith(">")) { |  | ||||||
|         const commandQuery = term.substring(1).trim(); |  | ||||||
|  |  | ||||||
|         // Get commands (all if no query, filtered if query provided) |  | ||||||
|         const commands = commandQuery.length === 0 |  | ||||||
|             ? commandRegistry.getAllCommands() |  | ||||||
|             : commandRegistry.searchCommands(commandQuery); |  | ||||||
|  |  | ||||||
|         // Convert commands to suggestions |  | ||||||
|         const commandSuggestions: Suggestion[] = commands.map(cmd => ({ |  | ||||||
|             action: "command", |  | ||||||
|             commandId: cmd.id, |  | ||||||
|             noteTitle: cmd.name, |  | ||||||
|             notePathTitle: `>${cmd.name}`, |  | ||||||
|             highlightedNotePathTitle: cmd.name, |  | ||||||
|             commandDescription: cmd.description, |  | ||||||
|             commandShortcut: cmd.shortcut, |  | ||||||
|             icon: cmd.icon |  | ||||||
|         })); |  | ||||||
|  |  | ||||||
|         cb(commandSuggestions); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const fastSearch = options.fastSearch === false ? false : true; |     const fastSearch = options.fastSearch === false ? false : true; | ||||||
|     if (fastSearch === false) { |     if (fastSearch === false) { | ||||||
|         if (term.trim().length === 0) { |         if (term.trim().length === 0) { | ||||||
| @@ -179,12 +143,6 @@ function showRecentNotes($el: JQuery<HTMLElement>) { | |||||||
|     $el.trigger("focus"); |     $el.trigger("focus"); | ||||||
| } | } | ||||||
|  |  | ||||||
| function showAllCommands($el: JQuery<HTMLElement>) { |  | ||||||
|     searchDelay = 0; |  | ||||||
|     $el.setSelectedNotePath(""); |  | ||||||
|     $el.autocomplete("val", ">").autocomplete("open"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function fullTextSearch($el: JQuery<HTMLElement>, options: Options) { | function fullTextSearch($el: JQuery<HTMLElement>, options: Options) { | ||||||
|     const searchString = $el.autocomplete("val") as unknown as string; |     const searchString = $el.autocomplete("val") as unknown as string; | ||||||
|     if (options.fastSearch === false || searchString?.trim().length === 0) { |     if (options.fastSearch === false || searchString?.trim().length === 0) { | ||||||
| @@ -232,11 +190,9 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||||||
|  |  | ||||||
|     const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); |     const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); | ||||||
|  |  | ||||||
|     if (!options.hideAllButtons) { |     $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); | ||||||
|         $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) { |     if (!options.hideGoToSelectedNoteButton) { | ||||||
|         $el.after($goToSelectedNoteButton); |         $el.after($goToSelectedNoteButton); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -309,50 +265,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||||||
|                 }, |                 }, | ||||||
|                 displayKey: "notePathTitle", |                 displayKey: "notePathTitle", | ||||||
|                 templates: { |                 templates: { | ||||||
|                     suggestion: (suggestion) => { |                     suggestion: (suggestion) => `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}` | ||||||
|                         if (suggestion.action === "command") { |  | ||||||
|                             let html = `<div class="command-suggestion">`; |  | ||||||
|                             html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`; |  | ||||||
|                             html += `<div class="command-content">`; |  | ||||||
|                             html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`; |  | ||||||
|                             if (suggestion.commandDescription) { |  | ||||||
|                                 html += `<div class="command-description">${suggestion.commandDescription}</div>`; |  | ||||||
|                             } |  | ||||||
|                             html += `</div>`; |  | ||||||
|                             if (suggestion.commandShortcut) { |  | ||||||
|                                 html += `<kbd class="command-shortcut">${suggestion.commandShortcut}</kbd>`; |  | ||||||
|                             } |  | ||||||
|                             html += '</div>'; |  | ||||||
|                             return html; |  | ||||||
|                         } |  | ||||||
|                         // Add special class for search-notes action |  | ||||||
|                         const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : ""; |  | ||||||
|  |  | ||||||
|                         // Choose appropriate icon based on action |  | ||||||
|                         let iconClass = suggestion.icon ?? "bx bx-note"; |  | ||||||
|                         if (suggestion.action === "search-notes") { |  | ||||||
|                             iconClass = "bx bx-search"; |  | ||||||
|                         } else if (suggestion.action === "create-note") { |  | ||||||
|                             iconClass = "bx bx-plus"; |  | ||||||
|                         } else if (suggestion.action === "external-link") { |  | ||||||
|                             iconClass = "bx bx-link-external"; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         // Simplified HTML structure without nested divs |  | ||||||
|                         let html = `<div class="note-suggestion ${actionClass}">`; |  | ||||||
|                         html += `<span class="icon ${iconClass}"></span>`; |  | ||||||
|                         html += `<span class="text">`; |  | ||||||
|                         html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`; |  | ||||||
|  |  | ||||||
|                         // Add attribute snippet inline if available |  | ||||||
|                         if (suggestion.highlightedAttributeSnippet) { |  | ||||||
|                             html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         html += `</span>`; |  | ||||||
|                         html += `</div>`; |  | ||||||
|                         return html; |  | ||||||
|                     } |  | ||||||
|                 }, |                 }, | ||||||
|                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added |                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added | ||||||
|                 cache: false |                 cache: false | ||||||
| @@ -362,12 +275,6 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||||||
|  |  | ||||||
|     // TODO: Types fail due to "autocomplete:selected" not being registered in type definitions. |     // TODO: Types fail due to "autocomplete:selected" not being registered in type definitions. | ||||||
|     ($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => { |     ($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => { | ||||||
|         if (suggestion.action === "command") { |  | ||||||
|             $el.autocomplete("close"); |  | ||||||
|             $el.trigger("autocomplete:commandselected", [suggestion]); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (suggestion.action === "external-link") { |         if (suggestion.action === "external-link") { | ||||||
|             $el.setSelectedNotePath(null); |             $el.setSelectedNotePath(null); | ||||||
|             $el.setSelectedExternalLink(suggestion.externalLink); |             $el.setSelectedExternalLink(suggestion.externalLink); | ||||||
| @@ -480,26 +387,10 @@ function init() { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Convenience function which triggers the display of recent notes in the autocomplete input and focuses it. |  | ||||||
|  * |  | ||||||
|  * @param inputElement - The input element to trigger recent notes on. |  | ||||||
|  */ |  | ||||||
| export function triggerRecentNotes(inputElement: HTMLInputElement | null | undefined) { |  | ||||||
|     if (!inputElement) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const $el = $(inputElement); |  | ||||||
|     showRecentNotes($el); |  | ||||||
|     $el.trigger("focus").trigger("select"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     autocompleteSourceForCKEditor, |     autocompleteSourceForCKEditor, | ||||||
|     initNoteAutocomplete, |     initNoteAutocomplete, | ||||||
|     showRecentNotes, |     showRecentNotes, | ||||||
|     showAllCommands, |  | ||||||
|     setText, |     setText, | ||||||
|     init |     init | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js"; | |||||||
| import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; | import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; | ||||||
| import type { CKTextEditor } from "@triliumnext/ckeditor5"; | import type { CKTextEditor } from "@triliumnext/ckeditor5"; | ||||||
|  |  | ||||||
| export interface CreateNoteOpts { | interface CreateNoteOpts { | ||||||
|     isProtected?: boolean; |     isProtected?: boolean; | ||||||
|     saveSelection?: boolean; |     saveSelection?: boolean; | ||||||
|     title?: string | null; |     title?: string | null; | ||||||
| @@ -109,6 +109,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot | |||||||
|  |  | ||||||
| async function chooseNoteType() { | async function chooseNoteType() { | ||||||
|     return new Promise<ChooseNoteTypeResponse>((res) => { |     return new Promise<ChooseNoteTypeResponse>((res) => { | ||||||
|  |         // TODO: Remove ignore after callback for chooseNoteType is defined in app_context.ts | ||||||
|  |         //@ts-ignore | ||||||
|         appContext.triggerCommand("chooseNoteType", { callback: res }); |         appContext.triggerCommand("chooseNoteType", { callback: res }); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								apps/client/src/services/note_list_renderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								apps/client/src/services/note_list_renderer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | import type FNote from "../entities/fnote.js"; | ||||||
|  | import CalendarView from "../widgets/view_widgets/calendar_view.js"; | ||||||
|  | import GeoView from "../widgets/view_widgets/geo_view/index.js"; | ||||||
|  | import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; | ||||||
|  | import TableView from "../widgets/view_widgets/table_view/index.js"; | ||||||
|  | import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; | ||||||
|  | import type ViewMode from "../widgets/view_widgets/view_mode.js"; | ||||||
|  |  | ||||||
|  | export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap"; | ||||||
|  |  | ||||||
|  | export default class NoteListRenderer { | ||||||
|  |  | ||||||
|  |     private viewType: ViewTypeOptions; | ||||||
|  |     public viewMode: ViewMode<any> | null; | ||||||
|  |  | ||||||
|  |     constructor(args: ViewModeArgs) { | ||||||
|  |         this.viewType = this.#getViewType(args.parentNote); | ||||||
|  |  | ||||||
|  |         switch (this.viewType) { | ||||||
|  |             case "list": | ||||||
|  |             case "grid": | ||||||
|  |                 this.viewMode = new ListOrGridView(this.viewType, args); | ||||||
|  |                 break; | ||||||
|  |             case "calendar": | ||||||
|  |                 this.viewMode = new CalendarView(args); | ||||||
|  |                 break; | ||||||
|  |             case "table": | ||||||
|  |                 this.viewMode = new TableView(args); | ||||||
|  |                 break; | ||||||
|  |             case "geoMap": | ||||||
|  |                 this.viewMode = new GeoView(args); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 this.viewMode = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #getViewType(parentNote: FNote): ViewTypeOptions { | ||||||
|  |         const viewType = parentNote.getLabelValue("viewType"); | ||||||
|  |  | ||||||
|  |         if (!["list", "grid", "calendar", "table", "geoMap"].includes(viewType || "")) { | ||||||
|  |             // when not explicitly set, decide based on the note type | ||||||
|  |             return parentNote.type === "search" ? "list" : "grid"; | ||||||
|  |         } else { | ||||||
|  |             return viewType as ViewTypeOptions; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get isFullHeight() { | ||||||
|  |         return this.viewMode?.isFullHeight; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async renderList() { | ||||||
|  |         if (!this.viewMode) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return await this.viewMode.renderList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -13,8 +13,8 @@ let openTooltipElements: JQuery<HTMLElement>[] = []; | |||||||
| let dismissTimer: ReturnType<typeof setTimeout>; | let dismissTimer: ReturnType<typeof setTimeout>; | ||||||
|  |  | ||||||
| function setupGlobalTooltip() { | function setupGlobalTooltip() { | ||||||
|     $(document).on("mouseenter", "a:not(.no-tooltip-preview)", mouseEnterHandler); |     $(document).on("mouseenter", "a", mouseEnterHandler); | ||||||
|     $(document).on("mouseenter", "[data-href]:not(.no-tooltip-preview)", mouseEnterHandler); |     $(document).on("mouseenter", "[data-href]", mouseEnterHandler); | ||||||
|  |  | ||||||
|     // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen |     // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen | ||||||
|     $(document).on("click", (e) => { |     $(document).on("click", (e) => { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import froca from "./froca.js"; | import froca from "./froca.js"; | ||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
| import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js"; | import type { MenuCommandItem, MenuItem, MenuItemBadge } from "../menus/context_menu.js"; | ||||||
| import type { NoteType } from "../entities/fnote.js"; | import type { NoteType } from "../entities/fnote.js"; | ||||||
| import type { TreeCommandNames } from "../menus/tree_context_menu.js"; | import type { TreeCommandNames } from "../menus/tree_context_menu.js"; | ||||||
|  |  | ||||||
| @@ -73,7 +73,7 @@ const BETA_BADGE = { | |||||||
|     title: t("note_types.beta-feature") |     title: t("note_types.beta-feature") | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const SEPARATOR: MenuSeparatorItem = { kind: "separator" }; | const SEPARATOR = { title: "----" }; | ||||||
|  |  | ||||||
| const creationDateCache = new Map<string, Date>(); | const creationDateCache = new Map<string, Date>(); | ||||||
| let rootCreationDate: Date | undefined; | let rootCreationDate: Date | undefined; | ||||||
| @@ -81,8 +81,8 @@ let rootCreationDate: Date | undefined; | |||||||
| async function getNoteTypeItems(command?: TreeCommandNames) { | async function getNoteTypeItems(command?: TreeCommandNames) { | ||||||
|     const items: MenuItem<TreeCommandNames>[] = [ |     const items: MenuItem<TreeCommandNames>[] = [ | ||||||
|         ...getBlankNoteTypes(command), |         ...getBlankNoteTypes(command), | ||||||
|         ...await getBuiltInTemplates(null, command, false), |  | ||||||
|         ...await getBuiltInTemplates(t("note_types.collections"), command, true), |         ...await getBuiltInTemplates(t("note_types.collections"), command, true), | ||||||
|  |         ...await getBuiltInTemplates(null, command, false), | ||||||
|         ...await getUserTemplates(command) |         ...await getUserTemplates(command) | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
| @@ -121,10 +121,7 @@ async function getUserTemplates(command?: TreeCommandNames) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const items: MenuItem<TreeCommandNames>[] = [ |     const items: MenuItem<TreeCommandNames>[] = [ | ||||||
|         { |         SEPARATOR | ||||||
|             title: t("note_type_chooser.templates"), |  | ||||||
|             kind: "header" |  | ||||||
|         } |  | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     for (const templateNote of templateNotes) { |     for (const templateNote of templateNotes) { | ||||||
| @@ -161,7 +158,8 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam | |||||||
|     if (title) { |     if (title) { | ||||||
|         items.push({ |         items.push({ | ||||||
|             title: title, |             title: title, | ||||||
|             kind: "header" |             enabled: false, | ||||||
|  |             uiIcon: "bx bx-empty" | ||||||
|         }); |         }); | ||||||
|     } else { |     } else { | ||||||
|         items.push(SEPARATOR); |         items.push(SEPARATOR); | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ function download(url: string) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function downloadFileNote(noteId: string) { | function downloadFileNote(noteId: string) { | ||||||
|     const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache |     const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache | ||||||
|  |  | ||||||
|     download(url); |     download(url); | ||||||
| @@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime); | const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime); | ||||||
| const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime); | const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime); | ||||||
|  |  | ||||||
| function getHost() { | function getHost() { | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import { OptionNames } from "@triliumnext/commons"; |  | ||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
| import { isShare } from "./utils.js"; | import { isShare } from "./utils.js"; | ||||||
|  |  | ||||||
| export type OptionValue = number | string; | type OptionValue = number | string; | ||||||
|  |  | ||||||
| class Options { | class Options { | ||||||
|     initializedPromise: Promise<void>; |     initializedPromise: Promise<void>; | ||||||
| @@ -20,7 +19,7 @@ class Options { | |||||||
|         this.arr = arr; |         this.arr = arr; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     get(key: OptionNames) { |     get(key: string) { | ||||||
|         return this.arr?.[key] as string; |         return this.arr?.[key] as string; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -40,7 +39,7 @@ class Options { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getInt(key: OptionNames) { |     getInt(key: string) { | ||||||
|         const value = this.arr?.[key]; |         const value = this.arr?.[key]; | ||||||
|         if (typeof value === "number") { |         if (typeof value === "number") { | ||||||
|             return value; |             return value; | ||||||
| @@ -52,7 +51,7 @@ class Options { | |||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getFloat(key: OptionNames) { |     getFloat(key: string) { | ||||||
|         const value = this.arr?.[key]; |         const value = this.arr?.[key]; | ||||||
|         if (typeof value !== "string") { |         if (typeof value !== "string") { | ||||||
|             return null; |             return null; | ||||||
| @@ -60,15 +59,15 @@ class Options { | |||||||
|         return parseFloat(value); |         return parseFloat(value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     is(key: OptionNames) { |     is(key: string) { | ||||||
|         return this.arr[key] === "true"; |         return this.arr[key] === "true"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     set(key: OptionNames, value: OptionValue) { |     set(key: string, value: OptionValue) { | ||||||
|         this.arr[key] = value; |         this.arr[key] = value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async save(key: OptionNames, value: OptionValue) { |     async save(key: string, value: OptionValue) { | ||||||
|         this.set(key, value); |         this.set(key, value); | ||||||
|  |  | ||||||
|         const payload: Record<string, OptionValue> = {}; |         const payload: Record<string, OptionValue> = {}; | ||||||
| @@ -77,15 +76,7 @@ class Options { | |||||||
|         await server.put(`options`, payload); |         await server.put(`options`, payload); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     async toggle(key: string) { | ||||||
|      * Saves multiple options at once, by supplying a record where the keys are the option names and the values represent the stringified value to set. |  | ||||||
|      * @param newValues the record of keys and values. |  | ||||||
|      */ |  | ||||||
|     async saveMany<T extends OptionNames>(newValues: Record<T, OptionValue>) { |  | ||||||
|         await server.put<void>("options", newValues); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async toggle(key: OptionNames) { |  | ||||||
|         await this.save(key, (!this.is(key)).toString()); |         await this.save(key, (!this.is(key)).toString()); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color"; | export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url"; | ||||||
| type Multiplicity = "single" | "multi"; | type Multiplicity = "single" | "multi"; | ||||||
|  |  | ||||||
| export interface DefinitionObject { | export interface DefinitionObject { | ||||||
| @@ -17,7 +17,7 @@ function parse(value: string) { | |||||||
|     for (const token of tokens) { |     for (const token of tokens) { | ||||||
|         if (token === "promoted") { |         if (token === "promoted") { | ||||||
|             defObj.isPromoted = true; |             defObj.isPromoted = true; | ||||||
|         } else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) { |         } else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) { | ||||||
|             defObj.labelType = token as LabelType; |             defObj.labelType = token as LabelType; | ||||||
|         } else if (["single", "multi"].includes(token)) { |         } else if (["single", "multi"].includes(token)) { | ||||||
|             defObj.multiplicity = token as Multiplicity; |             defObj.multiplicity = token as Multiplicity; | ||||||
|   | |||||||
| @@ -107,11 +107,11 @@ function makeToast(message: Message, title: string, text: string): ToastOptions | |||||||
| } | } | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async (message) => { | ws.subscribeToMessages(async (message) => { | ||||||
|     if (!("taskType" in message) || message.taskType !== "protectNotes") { |     if (message.taskType !== "protectNotes") { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const isProtecting = message.data?.protect; |     const isProtecting = message.data.protect; | ||||||
|     const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title"); |     const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title"); | ||||||
|  |  | ||||||
|     if (message.type === "taskError") { |     if (message.type === "taskError") { | ||||||
|   | |||||||
| @@ -10,10 +10,6 @@ let leftInstance: ReturnType<typeof Split> | null; | |||||||
| let rightPaneWidth: number; | let rightPaneWidth: number; | ||||||
| let rightInstance: ReturnType<typeof Split> | null; | let rightInstance: ReturnType<typeof Split> | null; | ||||||
|  |  | ||||||
| const noteSplitMap = new Map<string[], ReturnType<typeof Split> | undefined>(); // key: a group of ntxIds, value: the corresponding Split instance |  | ||||||
| const noteSplitRafMap = new Map<string[], number>(); |  | ||||||
| let splitNoteContainer: HTMLElement | undefined; |  | ||||||
|  |  | ||||||
| function setupLeftPaneResizer(leftPaneVisible: boolean) { | function setupLeftPaneResizer(leftPaneVisible: boolean) { | ||||||
|     if (leftInstance) { |     if (leftInstance) { | ||||||
|         leftInstance.destroy(); |         leftInstance.destroy(); | ||||||
| @@ -87,86 +83,7 @@ function setupRightPaneResizer() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function findKeyByNtxId(ntxId: string): string[] | undefined { |  | ||||||
|     // Find the corresponding key in noteSplitMap based on ntxId |  | ||||||
|     for (const key of noteSplitMap.keys()) { |  | ||||||
|         if (key.includes(ntxId)) return key; |  | ||||||
|     } |  | ||||||
|     return undefined; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function setupNoteSplitResizer(ntxIds: string[]) { |  | ||||||
|     let targetNtxIds: string[] | undefined; |  | ||||||
|     for (const ntxId of ntxIds) { |  | ||||||
|         targetNtxIds = findKeyByNtxId(ntxId); |  | ||||||
|         if (targetNtxIds) break;  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (targetNtxIds) { |  | ||||||
|         noteSplitMap.get(targetNtxIds)?.destroy(); |  | ||||||
|         for (const id of ntxIds) { |  | ||||||
|             if (!targetNtxIds.includes(id)) { |  | ||||||
|                 targetNtxIds.push(id) |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         targetNtxIds = [...ntxIds]; |  | ||||||
|     } |  | ||||||
|     noteSplitMap.set(targetNtxIds, undefined); |  | ||||||
|     createSplitInstance(targetNtxIds); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| function delNoteSplitResizer(ntxIds: string[]) { |  | ||||||
|     let targetNtxIds = findKeyByNtxId(ntxIds[0]); |  | ||||||
|     if (!targetNtxIds) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     noteSplitMap.get(targetNtxIds)?.destroy(); |  | ||||||
|     noteSplitMap.delete(targetNtxIds); |  | ||||||
|     targetNtxIds = targetNtxIds.filter(id => !ntxIds.includes(id)); |  | ||||||
|  |  | ||||||
|     if (targetNtxIds.length >= 2) { |  | ||||||
|         noteSplitMap.set(targetNtxIds, undefined); |  | ||||||
|         createSplitInstance(targetNtxIds); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function moveNoteSplitResizer(ntxId: string) { |  | ||||||
|     const targetNtxIds = findKeyByNtxId(ntxId); |  | ||||||
|     if (!targetNtxIds) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|     noteSplitMap.get(targetNtxIds)?.destroy(); |  | ||||||
|     noteSplitMap.set(targetNtxIds, undefined); |  | ||||||
|     createSplitInstance(targetNtxIds); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function createSplitInstance(targetNtxIds: string[]) { |  | ||||||
|     const prevRafId = noteSplitRafMap.get(targetNtxIds); |  | ||||||
|     if (prevRafId) { |  | ||||||
|         cancelAnimationFrame(prevRafId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const rafId = requestAnimationFrame(() => { |  | ||||||
|         splitNoteContainer = splitNoteContainer ?? $("#center-pane").find(".split-note-container-widget")[0]; |  | ||||||
|         const splitPanels = [...splitNoteContainer.querySelectorAll<HTMLElement>(':scope > .note-split')] |  | ||||||
|             .filter(el => targetNtxIds.includes(el.getAttribute('data-ntx-id') ?? "")); |  | ||||||
|         const splitInstance = Split(splitPanels, { |  | ||||||
|             gutterSize: DEFAULT_GUTTER_SIZE, |  | ||||||
|             minSize: 150, |  | ||||||
|         }); |  | ||||||
|         noteSplitMap.set(targetNtxIds, splitInstance); |  | ||||||
|         noteSplitRafMap.delete(targetNtxIds); |  | ||||||
|     }); |  | ||||||
|     noteSplitRafMap.set(targetNtxIds, rafId); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     setupLeftPaneResizer, |     setupLeftPaneResizer, | ||||||
|     setupRightPaneResizer, |     setupRightPaneResizer | ||||||
|     setupNoteSplitResizer, |  | ||||||
|     delNoteSplitResizer, |  | ||||||
|     moveNoteSplitResizer |  | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -218,7 +218,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile | |||||||
| if (utils.isElectron()) { | if (utils.isElectron()) { | ||||||
|     const ipc = utils.dynamicRequire("electron").ipcRenderer; |     const ipc = utils.dynamicRequire("electron").ipcRenderer; | ||||||
|  |  | ||||||
|     ipc.on("server-response", async (_, arg: Arg) => { |     ipc.on("server-response", async (event: string, arg: Arg) => { | ||||||
|         if (arg.statusCode >= 200 && arg.statusCode < 300) { |         if (arg.statusCode >= 200 && arg.statusCode < 300) { | ||||||
|             handleSuccessfulResponse(arg); |             handleSuccessfulResponse(arg); | ||||||
|         } else { |         } else { | ||||||
|   | |||||||
| @@ -1,372 +0,0 @@ | |||||||
| import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; |  | ||||||
| import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js"; |  | ||||||
|  |  | ||||||
| // Mock utils module |  | ||||||
| vi.mock("./utils.js", () => ({ |  | ||||||
|     default: { |  | ||||||
|         isDesktop: () => true |  | ||||||
|     } |  | ||||||
| })); |  | ||||||
|  |  | ||||||
| // Mock jQuery globally since it's used in the shortcuts module |  | ||||||
| const mockElement = { |  | ||||||
|     addEventListener: vi.fn(), |  | ||||||
|     removeEventListener: vi.fn() |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const mockJQuery = vi.fn(() => [mockElement]); |  | ||||||
| (mockJQuery as any).length = 1; |  | ||||||
| mockJQuery[0] = mockElement; |  | ||||||
|  |  | ||||||
| (global as any).$ = mockJQuery as any; |  | ||||||
| global.document = mockElement as any; |  | ||||||
|  |  | ||||||
| describe("shortcuts", () => { |  | ||||||
|     beforeEach(() => { |  | ||||||
|         vi.clearAllMocks(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     afterEach(() => { |  | ||||||
|         // Clean up any active bindings after each test |  | ||||||
|         shortcuts.removeGlobalShortcut("test-namespace"); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("normalizeShortcut", () => { |  | ||||||
|         it("should normalize shortcut to lowercase and remove whitespace", () => { |  | ||||||
|             expect(shortcuts.normalizeShortcut("Ctrl + A")).toBe("ctrl+a"); |  | ||||||
|             expect(shortcuts.normalizeShortcut("  SHIFT + F1  ")).toBe("shift+f1"); |  | ||||||
|             expect(shortcuts.normalizeShortcut("Alt+Space")).toBe("alt+space"); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should handle empty or null shortcuts", () => { |  | ||||||
|             expect(shortcuts.normalizeShortcut("")).toBe(""); |  | ||||||
|             expect(shortcuts.normalizeShortcut(null as any)).toBe(null); |  | ||||||
|             expect(shortcuts.normalizeShortcut(undefined as any)).toBe(undefined); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should handle shortcuts with multiple spaces", () => { |  | ||||||
|             expect(shortcuts.normalizeShortcut("Ctrl   +   Shift   +   A")).toBe("ctrl+shift+a"); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should warn about malformed shortcuts", () => { |  | ||||||
|             const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); |  | ||||||
|  |  | ||||||
|             shortcuts.normalizeShortcut("ctrl+"); |  | ||||||
|             shortcuts.normalizeShortcut("+a"); |  | ||||||
|             shortcuts.normalizeShortcut("ctrl++a"); |  | ||||||
|  |  | ||||||
|             expect(consoleSpy).toHaveBeenCalledTimes(3); |  | ||||||
|             consoleSpy.mockRestore(); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("keyMatches", () => { |  | ||||||
|         const createKeyboardEvent = (key: string, code?: string) => ({ |  | ||||||
|             key, |  | ||||||
|             code: code || `Key${key.toUpperCase()}` |  | ||||||
|         } as KeyboardEvent); |  | ||||||
|  |  | ||||||
|         it("should match regular letter keys using key code", () => { |  | ||||||
|             const event = createKeyboardEvent("a", "KeyA"); |  | ||||||
|             expect(keyMatches(event, "a")).toBe(true); |  | ||||||
|             expect(keyMatches(event, "A")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should match number keys using digit codes", () => { |  | ||||||
|             const event = createKeyboardEvent("1", "Digit1"); |  | ||||||
|             expect(keyMatches(event, "1")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should match special keys using key mapping", () => { |  | ||||||
|             expect(keyMatches({ key: "Enter" } as KeyboardEvent, "return")).toBe(true); |  | ||||||
|             expect(keyMatches({ key: "Enter" } as KeyboardEvent, "enter")).toBe(true); |  | ||||||
|             expect(keyMatches({ key: "Delete" } as KeyboardEvent, "del")).toBe(true); |  | ||||||
|             expect(keyMatches({ key: "Escape" } as KeyboardEvent, "esc")).toBe(true); |  | ||||||
|             expect(keyMatches({ key: " " } as KeyboardEvent, "space")).toBe(true); |  | ||||||
|             expect(keyMatches({ key: "ArrowUp" } as KeyboardEvent, "up")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should match function keys", () => { |  | ||||||
|             expect(keyMatches({ key: "F1" } as KeyboardEvent, "f1")).toBe(true); |  | ||||||
|             expect(keyMatches({ key: "F12" } as KeyboardEvent, "f12")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should handle undefined or null keys", () => { |  | ||||||
|             const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); |  | ||||||
|  |  | ||||||
|             expect(keyMatches({} as KeyboardEvent, null as any)).toBe(false); |  | ||||||
|             expect(keyMatches({} as KeyboardEvent, undefined as any)).toBe(false); |  | ||||||
|  |  | ||||||
|             expect(consoleSpy).toHaveBeenCalled(); |  | ||||||
|             consoleSpy.mockRestore(); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("matchesShortcut", () => { |  | ||||||
|         const createKeyboardEvent = (options: { |  | ||||||
|             key: string; |  | ||||||
|             code?: string; |  | ||||||
|             ctrlKey?: boolean; |  | ||||||
|             altKey?: boolean; |  | ||||||
|             shiftKey?: boolean; |  | ||||||
|             metaKey?: boolean; |  | ||||||
|         }) => ({ |  | ||||||
|             key: options.key, |  | ||||||
|             code: options.code || `Key${options.key.toUpperCase()}`, |  | ||||||
|             ctrlKey: options.ctrlKey || false, |  | ||||||
|             altKey: options.altKey || false, |  | ||||||
|             shiftKey: options.shiftKey || false, |  | ||||||
|             metaKey: options.metaKey || false |  | ||||||
|         } as KeyboardEvent); |  | ||||||
|  |  | ||||||
|         it("should match shortcuts with modifiers", () => { |  | ||||||
|             const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true }); |  | ||||||
|             expect(matchesShortcut(event, "ctrl+a")).toBe(true); |  | ||||||
|  |  | ||||||
|             const shiftEvent = createKeyboardEvent({ key: "a", code: "KeyA", shiftKey: true }); |  | ||||||
|             expect(matchesShortcut(shiftEvent, "shift+a")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should match complex modifier combinations", () => { |  | ||||||
|             const event = createKeyboardEvent({ |  | ||||||
|                 key: "a", |  | ||||||
|                 code: "KeyA", |  | ||||||
|                 ctrlKey: true, |  | ||||||
|                 shiftKey: true |  | ||||||
|             }); |  | ||||||
|             expect(matchesShortcut(event, "ctrl+shift+a")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should not match when modifiers don't match", () => { |  | ||||||
|             const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true }); |  | ||||||
|             expect(matchesShortcut(event, "alt+a")).toBe(false); |  | ||||||
|             expect(matchesShortcut(event, "a")).toBe(false); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should not match when no modifiers are used", () => { |  | ||||||
|             const event = createKeyboardEvent({ key: "a", code: "KeyA" }); |  | ||||||
|             expect(matchesShortcut(event, "a")).toBe(false); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should match some keys even with no modifiers", () => { |  | ||||||
|             // Bare function keys |  | ||||||
|             let event = createKeyboardEvent({ key: "F1", code: "F1" }); |  | ||||||
|             expect(matchesShortcut(event, "F1")).toBeTruthy(); |  | ||||||
|             expect(matchesShortcut(event, "f1")).toBeTruthy(); |  | ||||||
|  |  | ||||||
|             // Function keys with shift |  | ||||||
|             event = createKeyboardEvent({ key: "F1", code: "F1", shiftKey: true }); |  | ||||||
|             expect(matchesShortcut(event, "Shift+F1")).toBeTruthy(); |  | ||||||
|  |  | ||||||
|             // Special keys |  | ||||||
|             for (const keyCode of [ "Delete", "Enter" ]) { |  | ||||||
|                 event = createKeyboardEvent({ key: keyCode, code: keyCode }); |  | ||||||
|                 expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should handle alternative modifier names", () => { |  | ||||||
|             const ctrlEvent = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true }); |  | ||||||
|             expect(matchesShortcut(ctrlEvent, "control+a")).toBe(true); |  | ||||||
|  |  | ||||||
|             const metaEvent = createKeyboardEvent({ key: "a", code: "KeyA", metaKey: true }); |  | ||||||
|             expect(matchesShortcut(metaEvent, "cmd+a")).toBe(true); |  | ||||||
|             expect(matchesShortcut(metaEvent, "command+a")).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should handle empty or invalid shortcuts", () => { |  | ||||||
|             const event = createKeyboardEvent({ key: "a", code: "KeyA" }); |  | ||||||
|             expect(matchesShortcut(event, "")).toBe(false); |  | ||||||
|             expect(matchesShortcut(event, null as any)).toBe(false); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should handle invalid events", () => { |  | ||||||
|             const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); |  | ||||||
|  |  | ||||||
|             expect(matchesShortcut(null as any, "a")).toBe(false); |  | ||||||
|             expect(matchesShortcut({} as KeyboardEvent, "a")).toBe(false); |  | ||||||
|  |  | ||||||
|             expect(consoleSpy).toHaveBeenCalled(); |  | ||||||
|             consoleSpy.mockRestore(); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should warn about invalid shortcut formats", () => { |  | ||||||
|             const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); |  | ||||||
|             const event = createKeyboardEvent({ key: "a", code: "KeyA" }); |  | ||||||
|  |  | ||||||
|             matchesShortcut(event, "ctrl+"); |  | ||||||
|             matchesShortcut(event, "+"); |  | ||||||
|  |  | ||||||
|             expect(consoleSpy).toHaveBeenCalled(); |  | ||||||
|             consoleSpy.mockRestore(); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("bindGlobalShortcut", () => { |  | ||||||
|         it("should bind a global shortcut", () => { |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should not bind shortcuts when handler is null", () => { |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", null, "test-namespace"); |  | ||||||
|  |  | ||||||
|             expect(mockElement.addEventListener).not.toHaveBeenCalled(); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should remove previous bindings when namespace is reused", () => { |  | ||||||
|             const handler1 = vi.fn(); |  | ||||||
|             const handler2 = vi.fn(); |  | ||||||
|  |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", handler1, "test-namespace"); |  | ||||||
|             expect(mockElement.addEventListener).toHaveBeenCalledTimes(1); |  | ||||||
|  |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+b", handler2, "test-namespace"); |  | ||||||
|             expect(mockElement.removeEventListener).toHaveBeenCalledTimes(1); |  | ||||||
|             expect(mockElement.addEventListener).toHaveBeenCalledTimes(2); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("bindElShortcut", () => { |  | ||||||
|         it("should bind shortcut to specific element", () => { |  | ||||||
|             const mockEl = { addEventListener: vi.fn(), removeEventListener: vi.fn() }; |  | ||||||
|             const mockJQueryEl = [mockEl] as any; |  | ||||||
|             mockJQueryEl.length = 1; |  | ||||||
|  |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should fall back to document when element is empty", () => { |  | ||||||
|             const emptyJQuery = [] as any; |  | ||||||
|             emptyJQuery.length = 0; |  | ||||||
|  |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("removeGlobalShortcut", () => { |  | ||||||
|         it("should remove shortcuts for a specific namespace", () => { |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             shortcuts.removeGlobalShortcut("test-namespace"); |  | ||||||
|  |  | ||||||
|             expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe("event handling", () => { |  | ||||||
|         it.skip("should call handler when shortcut matches", () => { |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             // Get the listener that was registered |  | ||||||
|             expect(mockElement.addEventListener.mock.calls).toHaveLength(1); |  | ||||||
|             const [, listener] = mockElement.addEventListener.mock.calls[0]; |  | ||||||
|  |  | ||||||
|             // First verify that matchesShortcut works directly |  | ||||||
|             const testEvent = { |  | ||||||
|                 type: "keydown", |  | ||||||
|                 key: "a", |  | ||||||
|                 code: "KeyA", |  | ||||||
|                 ctrlKey: true, |  | ||||||
|                 altKey: false, |  | ||||||
|                 shiftKey: false, |  | ||||||
|                 metaKey: false, |  | ||||||
|                 preventDefault: vi.fn(), |  | ||||||
|                 stopPropagation: vi.fn() |  | ||||||
|             } as any; |  | ||||||
|  |  | ||||||
|             // Test matchesShortcut directly first |  | ||||||
|             expect(matchesShortcut(testEvent, "ctrl+a")).toBe(true); |  | ||||||
|  |  | ||||||
|             // Now test the actual listener |  | ||||||
|             listener(testEvent); |  | ||||||
|  |  | ||||||
|             expect(handler).toHaveBeenCalled(); |  | ||||||
|             expect(testEvent.preventDefault).toHaveBeenCalled(); |  | ||||||
|             expect(testEvent.stopPropagation).toHaveBeenCalled(); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should not call handler for non-keyboard events", () => { |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             const [, listener] = mockElement.addEventListener.mock.calls[0]; |  | ||||||
|  |  | ||||||
|             // Simulate a non-keyboard event |  | ||||||
|             const event = { |  | ||||||
|                 type: "click" |  | ||||||
|             } as any; |  | ||||||
|  |  | ||||||
|             listener(event); |  | ||||||
|  |  | ||||||
|             expect(handler).not.toHaveBeenCalled(); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it("should not call handler when shortcut doesn't match", () => { |  | ||||||
|             const handler = vi.fn(); |  | ||||||
|             shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); |  | ||||||
|  |  | ||||||
|             const [, listener] = mockElement.addEventListener.mock.calls[0]; |  | ||||||
|  |  | ||||||
|             // Simulate a non-matching keydown event |  | ||||||
|             const event = { |  | ||||||
|                 type: "keydown", |  | ||||||
|                 key: "b", |  | ||||||
|                 code: "KeyB", |  | ||||||
|                 ctrlKey: true, |  | ||||||
|                 altKey: false, |  | ||||||
|                 shiftKey: false, |  | ||||||
|                 metaKey: false, |  | ||||||
|                 preventDefault: vi.fn(), |  | ||||||
|                 stopPropagation: vi.fn() |  | ||||||
|             } as any; |  | ||||||
|  |  | ||||||
|             listener(event); |  | ||||||
|  |  | ||||||
|             expect(handler).not.toHaveBeenCalled(); |  | ||||||
|             expect(event.preventDefault).not.toHaveBeenCalled(); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     describe('isIMEComposing', () => { |  | ||||||
|         it('should return true when event.isComposing is true', () => { |  | ||||||
|             const event = { isComposing: true, keyCode: 65 } as KeyboardEvent; |  | ||||||
|             expect(isIMEComposing(event)).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it('should return true when keyCode is 229', () => { |  | ||||||
|             const event = { isComposing: false, keyCode: 229 } as KeyboardEvent; |  | ||||||
|             expect(isIMEComposing(event)).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it('should return true when both isComposing is true and keyCode is 229', () => { |  | ||||||
|             const event = { isComposing: true, keyCode: 229 } as KeyboardEvent; |  | ||||||
|             expect(isIMEComposing(event)).toBe(true); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it('should return false for normal keys', () => { |  | ||||||
|             const event = { isComposing: false, keyCode: 65 } as KeyboardEvent; |  | ||||||
|             expect(isIMEComposing(event)).toBe(false); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it('should return false when isComposing is undefined and keyCode is not 229', () => { |  | ||||||
|             const event = { keyCode: 13 } as KeyboardEvent; |  | ||||||
|             expect(isIMEComposing(event)).toBe(false); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         it('should handle null/undefined events gracefully', () => { |  | ||||||
|             expect(isIMEComposing(null as any)).toBe(false); |  | ||||||
|             expect(isIMEComposing(undefined as any)).toBe(false); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| @@ -1,71 +1,7 @@ | |||||||
| import utils from "./utils.js"; | import utils from "./utils.js"; | ||||||
|  |  | ||||||
| type ElementType = HTMLElement | Document; | type ElementType = HTMLElement | Document; | ||||||
| type Handler = (e: KeyboardEvent) => void; | type Handler = (e: JQuery.TriggeredEvent<ElementType | Element, string, ElementType | Element, ElementType | Element>) => void; | ||||||
|  |  | ||||||
| interface ShortcutBinding { |  | ||||||
|     element: HTMLElement | Document; |  | ||||||
|     shortcut: string; |  | ||||||
|     handler: Handler; |  | ||||||
|     namespace: string | null; |  | ||||||
|     listener: (evt: Event) => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Store all active shortcut bindings for management |  | ||||||
| const activeBindings: Map<string, ShortcutBinding[]> = new Map(); |  | ||||||
|  |  | ||||||
| // Handle special key mappings and aliases |  | ||||||
| const keyMap: { [key: string]: string[] } = { |  | ||||||
|     'return': ['Enter'], |  | ||||||
|     'enter': ['Enter'],  // alias for return |  | ||||||
|     'del': ['Delete'], |  | ||||||
|     'delete': ['Delete'], // alias for del |  | ||||||
|     'esc': ['Escape'], |  | ||||||
|     'escape': ['Escape'], // alias for esc |  | ||||||
|     'space': [' ', 'Space'], |  | ||||||
|     'tab': ['Tab'], |  | ||||||
|     'backspace': ['Backspace'], |  | ||||||
|     'home': ['Home'], |  | ||||||
|     'end': ['End'], |  | ||||||
|     'pageup': ['PageUp'], |  | ||||||
|     'pagedown': ['PageDown'], |  | ||||||
|     'up': ['ArrowUp'], |  | ||||||
|     'down': ['ArrowDown'], |  | ||||||
|     'left': ['ArrowLeft'], |  | ||||||
|     'right': ['ArrowRight'] |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Function keys |  | ||||||
| const functionKeyCodes: string[] = []; |  | ||||||
| for (let i = 1; i <= 19; i++) { |  | ||||||
|     const keyCode = `F${i}`; |  | ||||||
|     functionKeyCodes.push(keyCode); |  | ||||||
|     keyMap[`f${i}`] = [ keyCode ]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const KEYCODES_WITH_NO_MODIFIER = new Set([ |  | ||||||
|     "Delete", |  | ||||||
|     "Enter", |  | ||||||
|     ...functionKeyCodes |  | ||||||
| ]); |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Check if IME (Input Method Editor) is composing |  | ||||||
|  * This is used to prevent keyboard shortcuts from firing during IME composition |  | ||||||
|  * @param e - The keyboard event to check |  | ||||||
|  * @returns true if IME is currently composing, false otherwise |  | ||||||
|  */ |  | ||||||
| export function isIMEComposing(e: KeyboardEvent): boolean { |  | ||||||
|     // Handle null/undefined events gracefully |  | ||||||
|     if (!e) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Standard check for composition state |  | ||||||
|     // e.isComposing is true when IME is actively composing |  | ||||||
|     // e.keyCode === 229 is a fallback for older browsers where 229 indicates IME processing |  | ||||||
|     return e.isComposing || e.keyCode === 229; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function removeGlobalShortcut(namespace: string) { | function removeGlobalShortcut(namespace: string) { | ||||||
|     bindGlobalShortcut("", null, namespace); |     bindGlobalShortcut("", null, namespace); | ||||||
| @@ -79,154 +15,38 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st | |||||||
|     if (utils.isDesktop()) { |     if (utils.isDesktop()) { | ||||||
|         keyboardShortcut = normalizeShortcut(keyboardShortcut); |         keyboardShortcut = normalizeShortcut(keyboardShortcut); | ||||||
|  |  | ||||||
|         // If namespace is provided, remove all previous bindings for this namespace |         let eventName = "keydown"; | ||||||
|  |  | ||||||
|         if (namespace) { |         if (namespace) { | ||||||
|             removeNamespaceBindings(namespace); |             eventName += `.${namespace}`; | ||||||
|  |  | ||||||
|  |             // if there's a namespace, then we replace the existing event handler with the new one | ||||||
|  |             $el.off(eventName); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted) |         // method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted) | ||||||
|         if (keyboardShortcut && handler) { |         if (keyboardShortcut) { | ||||||
|             const element = $el.length > 0 ? $el[0] as (HTMLElement | Document) : document; |             $el.bind(eventName, keyboardShortcut, (e) => { | ||||||
|  |                 if (handler) { | ||||||
|             const listener = (evt: Event) => { |  | ||||||
|                 // Only handle keyboard events |  | ||||||
|                 if (evt.type !== 'keydown' || !(evt instanceof KeyboardEvent)) { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 const e = evt as KeyboardEvent; |  | ||||||
|  |  | ||||||
|                 // Skip processing if IME is composing to prevent shortcuts from |  | ||||||
|                 // interfering with text input in CJK languages |  | ||||||
|                 if (isIMEComposing(e)) { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (matchesShortcut(e, keyboardShortcut)) { |  | ||||||
|                     e.preventDefault(); |  | ||||||
|                     e.stopPropagation(); |  | ||||||
|                     handler(e); |                     handler(e); | ||||||
|                 } |                 } | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             // Add the event listener |                 e.preventDefault(); | ||||||
|             element.addEventListener('keydown', listener); |                 e.stopPropagation(); | ||||||
|  |             }); | ||||||
|             // Store the binding for later cleanup |  | ||||||
|             const binding: ShortcutBinding = { |  | ||||||
|                 element, |  | ||||||
|                 shortcut: keyboardShortcut, |  | ||||||
|                 handler, |  | ||||||
|                 namespace, |  | ||||||
|                 listener |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             const key = namespace || 'global'; |  | ||||||
|             if (!activeBindings.has(key)) { |  | ||||||
|                 activeBindings.set(key, []); |  | ||||||
|             } |  | ||||||
|             activeBindings.get(key)!.push(binding); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function removeNamespaceBindings(namespace: string) { |  | ||||||
|     const bindings = activeBindings.get(namespace); |  | ||||||
|     if (bindings) { |  | ||||||
|         // Remove all event listeners for this namespace |  | ||||||
|         bindings.forEach(binding => { |  | ||||||
|             binding.element.removeEventListener('keydown', binding.listener); |  | ||||||
|         }); |  | ||||||
|         activeBindings.delete(namespace); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function matchesShortcut(e: KeyboardEvent, shortcut: string): boolean { |  | ||||||
|     if (!shortcut) return false; |  | ||||||
|  |  | ||||||
|     // Ensure we have a proper KeyboardEvent with key property |  | ||||||
|     if (!e || typeof e.key !== 'string') { |  | ||||||
|         console.warn('matchesShortcut called with invalid event:', e); |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const parts = shortcut.toLowerCase().split('+'); |  | ||||||
|     const key = parts[parts.length - 1]; // Last part is the actual key |  | ||||||
|     const modifiers = parts.slice(0, -1); // Everything before is modifiers |  | ||||||
|  |  | ||||||
|     // Defensive check - ensure we have a valid key |  | ||||||
|     if (!key || key.trim() === '') { |  | ||||||
|         console.warn('Invalid shortcut format:', shortcut); |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Check if the main key matches |  | ||||||
|     if (!keyMatches(e, key)) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Check modifiers |  | ||||||
|     const expectedCtrl = modifiers.includes('ctrl') || modifiers.includes('control'); |  | ||||||
|     const expectedAlt = modifiers.includes('alt'); |  | ||||||
|     const expectedShift = modifiers.includes('shift'); |  | ||||||
|     const expectedMeta = modifiers.includes('meta') || modifiers.includes('cmd') || modifiers.includes('command'); |  | ||||||
|  |  | ||||||
|     // Refuse key combinations that don't include modifiers because they interfere with the normal usage of the application. |  | ||||||
|     // Some keys such as function keys are an exception. |  | ||||||
|     if (!(expectedCtrl || expectedAlt || expectedShift || expectedMeta) && !KEYCODES_WITH_NO_MODIFIER.has(e.code)) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return e.ctrlKey === expectedCtrl && |  | ||||||
|            e.altKey === expectedAlt && |  | ||||||
|            e.shiftKey === expectedShift && |  | ||||||
|            e.metaKey === expectedMeta; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function keyMatches(e: KeyboardEvent, key: string): boolean { |  | ||||||
|     // Defensive check for undefined/null key |  | ||||||
|     if (!key) { |  | ||||||
|         console.warn('keyMatches called with undefined/null key'); |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const mappedKeys = keyMap[key.toLowerCase()]; |  | ||||||
|     if (mappedKeys) { |  | ||||||
|         return mappedKeys.includes(e.key) || mappedKeys.includes(e.code); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // For number keys, use the physical key code regardless of modifiers |  | ||||||
|     // This works across all keyboard layouts |  | ||||||
|     if (key >= '0' && key <= '9') { |  | ||||||
|         return e.code === `Digit${key}`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // For letter keys, use the physical key code for consistency |  | ||||||
|     if (key.length === 1 && key >= 'a' && key <= 'z') { |  | ||||||
|         return e.key.toLowerCase() === key.toLowerCase(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // For regular keys, check both key and code as fallback |  | ||||||
|     return e.key.toLowerCase() === key.toLowerCase() || |  | ||||||
|            e.code.toLowerCase() === key.toLowerCase(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Simple normalization - just lowercase and trim whitespace |  * Normalize to the form expected by the jquery.hotkeys.js | ||||||
|  */ |  */ | ||||||
| function normalizeShortcut(shortcut: string): string { | function normalizeShortcut(shortcut: string): string { | ||||||
|     if (!shortcut) { |     if (!shortcut) { | ||||||
|         return shortcut; |         return shortcut; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const normalized = shortcut.toLowerCase().trim().replace(/\s+/g, ''); |     return shortcut.toLowerCase().replace("enter", "return").replace("delete", "del").replace("ctrl+alt", "alt+ctrl").replace("meta+alt", "alt+meta"); // alt needs to be first; | ||||||
|  |  | ||||||
|     // Warn about potentially problematic shortcuts |  | ||||||
|     if (normalized.endsWith('+') || normalized.startsWith('+') || normalized.includes('++')) { |  | ||||||
|         console.warn('Potentially malformed shortcut:', shortcut, '-> normalized to:', normalized); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return normalized; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   | |||||||
| @@ -51,14 +51,6 @@ export default class SpacedUpdate { | |||||||
|         this.lastUpdated = Date.now(); |         this.lastUpdated = Date.now(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Sets the update interval for the spaced update. |  | ||||||
|      * @param interval The update interval in milliseconds. |  | ||||||
|      */ |  | ||||||
|     setUpdateInterval(interval: number) { |  | ||||||
|         this.updateInterval = interval; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     triggerUpdate() { |     triggerUpdate() { | ||||||
|         if (!this.changed) { |         if (!this.changed) { | ||||||
|             return; |             return; | ||||||
|   | |||||||
| @@ -36,9 +36,7 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) { | |||||||
|     const $copyButton = $("<button>") |     const $copyButton = $("<button>") | ||||||
|         .addClass("bx component icon-action tn-tool-button bx-copy copy-button") |         .addClass("bx component icon-action tn-tool-button bx-copy copy-button") | ||||||
|         .attr("title", t("code_block.copy_title")) |         .attr("title", t("code_block.copy_title")) | ||||||
|         .on("click", (e) => { |         .on("click", () => { | ||||||
|             e.stopPropagation(); |  | ||||||
|  |  | ||||||
|             if (!isShare) { |             if (!isShare) { | ||||||
|                 copyTextWithToast($codeBlock.text()); |                 copyTextWithToast($codeBlock.text()); | ||||||
|             } else { |             } else { | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
|  | import ws from "./ws.js"; | ||||||
| import utils from "./utils.js"; | import utils from "./utils.js"; | ||||||
|  |  | ||||||
| export interface ToastOptions { | export interface ToastOptions { | ||||||
|     id?: string; |     id?: string; | ||||||
|     icon: string; |     icon: string; | ||||||
|     title?: string; |     title: string; | ||||||
|     message: string; |     message: string; | ||||||
|     delay?: number; |     delay?: number; | ||||||
|     autohide?: boolean; |     autohide?: boolean; | ||||||
| @@ -11,32 +12,20 @@ export interface ToastOptions { | |||||||
| } | } | ||||||
|  |  | ||||||
| function toast(options: ToastOptions) { | function toast(options: ToastOptions) { | ||||||
|     const $toast = $(options.title |     const $toast = $( | ||||||
|         ? `\ |         `<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"> | ||||||
|             <div class="toast" role="alert" aria-live="assertive" aria-atomic="true"> |             <div class="toast-header"> | ||||||
|                 <div class="toast-header"> |                 <strong class="me-auto"> | ||||||
|                     <strong class="me-auto"> |  | ||||||
|                         <span class="bx bx-${options.icon}"></span> |  | ||||||
|                         <span class="toast-title"></span> |  | ||||||
|                     </strong> |  | ||||||
|                     <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="toast-body"></div> |  | ||||||
|             </div>` |  | ||||||
|         : ` |  | ||||||
|             <div class="toast" role="alert" aria-live="assertive" aria-atomic="true"> |  | ||||||
|                 <div class="toast-icon"> |  | ||||||
|                     <span class="bx bx-${options.icon}"></span> |                     <span class="bx bx-${options.icon}"></span> | ||||||
|                 </div> |                     <span class="toast-title"></span> | ||||||
|                 <div class="toast-body"></div> |                 </strong> | ||||||
|                 <div class="toast-header"> |                 <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> | ||||||
|                     <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> |             </div> | ||||||
|                 </div> |             <div class="toast-body"></div> | ||||||
|             </div>` |         </div>` | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     $toast.toggleClass("no-title", !options.title); |     $toast.find(".toast-title").text(options.title); | ||||||
|     $toast.find(".toast-title").text(options.title ?? ""); |  | ||||||
|     $toast.find(".toast-body").html(options.message); |     $toast.find(".toast-body").html(options.message); | ||||||
|  |  | ||||||
|     if (options.id) { |     if (options.id) { | ||||||
| @@ -81,6 +70,7 @@ function showMessage(message: string, delay = 2000) { | |||||||
|     console.debug(utils.now(), "message:", message); |     console.debug(utils.now(), "message:", message); | ||||||
|  |  | ||||||
|     toast({ |     toast({ | ||||||
|  |         title: "Info", | ||||||
|         icon: "check", |         icon: "check", | ||||||
|         message: message, |         message: message, | ||||||
|         autohide: true, |         autohide: true, | ||||||
| @@ -92,6 +82,7 @@ export function showError(message: string, delay = 10000) { | |||||||
|     console.log(utils.now(), "error: ", message); |     console.log(utils.now(), "error: ", message); | ||||||
|  |  | ||||||
|     toast({ |     toast({ | ||||||
|  |         title: "Error", | ||||||
|         icon: "alert", |         icon: "alert", | ||||||
|         message: message, |         message: message, | ||||||
|         autohide: true, |         autohide: true, | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||||
| import type { ViewScope } from "./link.js"; | import type { ViewScope } from "./link.js"; | ||||||
| import FNote from "../entities/fnote"; |  | ||||||
|  |  | ||||||
| const SVG_MIME = "image/svg+xml"; | const SVG_MIME = "image/svg+xml"; | ||||||
|  |  | ||||||
| export const isShare = !window.glob; | export const isShare = !window.glob; | ||||||
|  |  | ||||||
| export function reloadFrontendApp(reason?: string) { | function reloadFrontendApp(reason?: string) { | ||||||
|     if (reason) { |     if (reason) { | ||||||
|         logInfo(`Frontend app reload: ${reason}`); |         logInfo(`Frontend app reload: ${reason}`); | ||||||
|     } |     } | ||||||
| @@ -14,7 +13,7 @@ export function reloadFrontendApp(reason?: string) { | |||||||
|     window.location.reload(); |     window.location.reload(); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function restartDesktopApp() { | function restartDesktopApp() { | ||||||
|     if (!isElectron()) { |     if (!isElectron()) { | ||||||
|         reloadFrontendApp(); |         reloadFrontendApp(); | ||||||
|         return; |         return; | ||||||
| @@ -47,6 +46,27 @@ function parseDate(str: string) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Source: https://stackoverflow.com/a/30465299/4898894 | ||||||
|  | function getMonthsInDateRange(startDate: string, endDate: string) { | ||||||
|  |     const start = startDate.split("-"); | ||||||
|  |     const end = endDate.split("-"); | ||||||
|  |     const startYear = parseInt(start[0]); | ||||||
|  |     const endYear = parseInt(end[0]); | ||||||
|  |     const dates: string[] = []; | ||||||
|  |  | ||||||
|  |     for (let i = startYear; i <= endYear; i++) { | ||||||
|  |         const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; | ||||||
|  |         const startMon = i === startYear ? parseInt(start[1]) - 1 : 0; | ||||||
|  |  | ||||||
|  |         for (let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j + 1) { | ||||||
|  |             const month = j + 1; | ||||||
|  |             const displayMonth = month < 10 ? "0" + month : month; | ||||||
|  |             dates.push([i, displayMonth].join("-")); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return dates; | ||||||
|  | } | ||||||
|  |  | ||||||
| function padNum(num: number) { | function padNum(num: number) { | ||||||
|     return `${num <= 9 ? "0" : ""}${num}`; |     return `${num <= 9 ? "0" : ""}${num}`; | ||||||
| } | } | ||||||
| @@ -105,7 +125,7 @@ function formatDateISO(date: Date) { | |||||||
|     return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; |     return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function formatDateTime(date: Date, userSuppliedFormat?: string): string { | function formatDateTime(date: Date, userSuppliedFormat?: string): string { | ||||||
|     if (userSuppliedFormat?.trim()) { |     if (userSuppliedFormat?.trim()) { | ||||||
|         return dayjs(date).format(userSuppliedFormat); |         return dayjs(date).format(userSuppliedFormat); | ||||||
|     } else { |     } else { | ||||||
| @@ -124,23 +144,11 @@ function now() { | |||||||
| /** | /** | ||||||
|  * Returns `true` if the client is currently running under Electron, or `false` if running in a web browser. |  * Returns `true` if the client is currently running under Electron, or `false` if running in a web browser. | ||||||
|  */ |  */ | ||||||
| export function isElectron() { | function isElectron() { | ||||||
|     return !!(window && window.process && window.process.type); |     return !!(window && window.process && window.process.type); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | function isMac() { | ||||||
|  * Returns `true` if the client is running as a PWA, otherwise `false`. |  | ||||||
|  */ |  | ||||||
| export function isPWA() { |  | ||||||
|     return ( |  | ||||||
|         window.matchMedia('(display-mode: standalone)').matches |  | ||||||
|         || window.matchMedia('(display-mode: window-controls-overlay)').matches |  | ||||||
|         || window.navigator.standalone |  | ||||||
|         || window.navigator.windowControlsOverlay |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function isMac() { |  | ||||||
|     return navigator.platform.indexOf("Mac") > -1; |     return navigator.platform.indexOf("Mac") > -1; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -177,11 +185,7 @@ export function escapeQuotes(value: string) { | |||||||
|     return value.replaceAll('"', """); |     return value.replaceAll('"', """); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function formatSize(size: number | null | undefined) { | function formatSize(size: number) { | ||||||
|     if (size === null || size === undefined) { |  | ||||||
|         return ""; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     size = Math.max(Math.round(size / 1024), 1); |     size = Math.max(Math.round(size / 1024), 1); | ||||||
|  |  | ||||||
|     if (size < 1024) { |     if (size < 1024) { | ||||||
| @@ -214,7 +218,7 @@ function randomString(len: number) { | |||||||
|     return text; |     return text; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function isMobile() { | function isMobile() { | ||||||
|     return ( |     return ( | ||||||
|         window.glob?.device === "mobile" || |         window.glob?.device === "mobile" || | ||||||
|         // window.glob.device is not available in setup |         // window.glob.device is not available in setup | ||||||
| @@ -288,55 +292,7 @@ function isHtmlEmpty(html: string) { | |||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function formatHtml(html: string) { | async function clearBrowserCache() { | ||||||
|     let indent = "\n"; |  | ||||||
|     const tab = "\t"; |  | ||||||
|     let i = 0; |  | ||||||
|     let pre: { indent: string; tag: string }[] = []; |  | ||||||
|  |  | ||||||
|     html = html |  | ||||||
|         .replace(new RegExp("<pre>([\\s\\S]+?)?</pre>"), function (x) { |  | ||||||
|             pre.push({ indent: "", tag: x }); |  | ||||||
|             return "<--TEMPPRE" + i++ + "/-->"; |  | ||||||
|         }) |  | ||||||
|         .replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) { |  | ||||||
|             let ret; |  | ||||||
|             const tagRegEx = /<\/?([^\s/>]+)/.exec(x); |  | ||||||
|             let tag = tagRegEx ? tagRegEx[1] : ""; |  | ||||||
|             let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x); |  | ||||||
|  |  | ||||||
|             if (p) { |  | ||||||
|                 const pInd = parseInt(p[1]); |  | ||||||
|                 pre[pInd].indent = indent; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) { |  | ||||||
|                 // self closing tag |  | ||||||
|                 ret = indent + x; |  | ||||||
|             } else { |  | ||||||
|                 if (x.indexOf("</") < 0) { |  | ||||||
|                     //open tag |  | ||||||
|                     if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length); |  | ||||||
|                     else ret = indent + x; |  | ||||||
|                     !p && (indent += tab); |  | ||||||
|                 } else { |  | ||||||
|                     //close tag |  | ||||||
|                     indent = indent.substr(0, indent.length - 1); |  | ||||||
|                     if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length); |  | ||||||
|                     else ret = indent + x; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return ret; |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|     for (i = pre.length; i--;) { |  | ||||||
|         html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", pre[i].indent + "</pre>")); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export async function clearBrowserCache() { |  | ||||||
|     if (isElectron()) { |     if (isElectron()) { | ||||||
|         const win = dynamicRequire("@electron/remote").getCurrentWindow(); |         const win = dynamicRequire("@electron/remote").getCurrentWindow(); | ||||||
|         await win.webContents.session.clearCache(); |         await win.webContents.session.clearCache(); | ||||||
| @@ -350,13 +306,7 @@ function copySelectionToClipboard() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| type dynamicRequireMappings = { | function dynamicRequire(moduleName: string) { | ||||||
|     "@electron/remote": typeof import("@electron/remote"), |  | ||||||
|     "electron": typeof import("electron"), |  | ||||||
|     "child_process": typeof import("child_process") |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{ |  | ||||||
|     if (typeof __non_webpack_require__ !== "undefined") { |     if (typeof __non_webpack_require__ !== "undefined") { | ||||||
|         return __non_webpack_require__(moduleName); |         return __non_webpack_require__(moduleName); | ||||||
|     } else { |     } else { | ||||||
| @@ -424,42 +374,33 @@ async function openInAppHelp($button: JQuery<HTMLElement>) { | |||||||
|  |  | ||||||
|     const inAppHelpPage = $button.attr("data-in-app-help"); |     const inAppHelpPage = $button.attr("data-in-app-help"); | ||||||
|     if (inAppHelpPage) { |     if (inAppHelpPage) { | ||||||
|         openInAppHelpFromUrl(inAppHelpPage); |         // Dynamic import to avoid import issues in tests. | ||||||
|     } |         const appContext = (await import("../components/app_context.js")).default; | ||||||
| } |         const activeContext = appContext.tabManager.getActiveContext(); | ||||||
|  |         if (!activeContext) { | ||||||
| /** |             return; | ||||||
|  * Opens the in-app help at the given page in a split note. If there already is a split note open with a help page, it will be replaced by this one. |         } | ||||||
|  * |         const subContexts = activeContext.getSubContexts(); | ||||||
|  * @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix). |         const targetNote = `_help_${inAppHelpPage}`; | ||||||
|  * @returns a promise that resolves once the help has been opened. |         const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help"); | ||||||
|  */ |         const viewScope: ViewScope = { | ||||||
| export async function openInAppHelpFromUrl(inAppHelpPage: string) { |             viewMode: "contextual-help", | ||||||
|     // Dynamic import to avoid import issues in tests. |         }; | ||||||
|     const appContext = (await import("../components/app_context.js")).default; |         if (!helpSubcontext) { | ||||||
|     const activeContext = appContext.tabManager.getActiveContext(); |             // The help is not already open, open a new split with it. | ||||||
|     if (!activeContext) { |             const { ntxId } = subContexts[subContexts.length - 1]; | ||||||
|  |             appContext.triggerCommand("openNewNoteSplit", { | ||||||
|  |                 ntxId, | ||||||
|  |                 notePath: targetNote, | ||||||
|  |                 hoistedNoteId: "_help", | ||||||
|  |                 viewScope | ||||||
|  |             }) | ||||||
|  |         } else { | ||||||
|  |             // There is already a help window open, make sure it opens on the right note. | ||||||
|  |             helpSubcontext.setNote(targetNote, { viewScope }); | ||||||
|  |         } | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     const subContexts = activeContext.getSubContexts(); |  | ||||||
|     const targetNote = `_help_${inAppHelpPage}`; |  | ||||||
|     const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help"); |  | ||||||
|     const viewScope: ViewScope = { |  | ||||||
|         viewMode: "contextual-help", |  | ||||||
|     }; |  | ||||||
|     if (!helpSubcontext) { |  | ||||||
|         // The help is not already open, open a new split with it. |  | ||||||
|         const { ntxId } = subContexts[subContexts.length - 1]; |  | ||||||
|         appContext.triggerCommand("openNewNoteSplit", { |  | ||||||
|             ntxId, |  | ||||||
|             notePath: targetNote, |  | ||||||
|             hoistedNoteId: "_help", |  | ||||||
|             viewScope |  | ||||||
|         }) |  | ||||||
|     } else { |  | ||||||
|         // There is already a help window open, make sure it opens on the right note. |  | ||||||
|         helpSubcontext.setNote(targetNote, { viewScope }); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) { | function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) { | ||||||
| @@ -487,7 +428,7 @@ function sleep(time_ms: number) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function escapeRegExp(str: string) { | function escapeRegExp(str: string) { | ||||||
|     return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); |     return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -620,7 +561,8 @@ function copyHtmlToClipboard(content: string) { | |||||||
|     document.removeEventListener("copy", listener); |     document.removeEventListener("copy", listener); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function createImageSrcUrl(note: FNote) { | // TODO: Set to FNote once the file is ported. | ||||||
|  | function createImageSrcUrl(note: { noteId: string; title: string }) { | ||||||
|     return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; |     return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -789,91 +731,16 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers | |||||||
|     return compareVersions(latestVersion, currentVersion) > 0; |     return compareVersions(latestVersion, currentVersion) > 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function isLaunchBarConfig(noteId: string) { | function isLaunchBarConfig(noteId: string) { | ||||||
|     return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); |     return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Adds a class to the <body> of the page, where the class name is formed via a prefix and a value. |  | ||||||
|  * Useful for configurable options such as `heading-style-markdown`, where `heading-style` is the prefix and `markdown` is the dynamic value. |  | ||||||
|  * There is no separator between the prefix and the value, if needed it has to be supplied manually to the prefix. |  | ||||||
|  * |  | ||||||
|  * @param prefix the prefix. |  | ||||||
|  * @param value the value to be appended to the prefix. |  | ||||||
|  */ |  | ||||||
| export function toggleBodyClass(prefix: string, value: string) { |  | ||||||
|     const $body = $("body"); |  | ||||||
|     for (const clazz of Array.from($body[0].classList)) { |  | ||||||
|         // create copy to safely iterate over while removing classes |  | ||||||
|         if (clazz.startsWith(prefix)) { |  | ||||||
|             $body.removeClass(clazz); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $body.addClass(prefix + value); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Basic comparison for equality between the two arrays. The values are strictly checked via `===`. |  | ||||||
|  * |  | ||||||
|  * @param a the first array to compare. |  | ||||||
|  * @param b the second array to compare. |  | ||||||
|  * @returns `true` if both arrays are equals, `false` otherwise. |  | ||||||
|  */ |  | ||||||
| export function arrayEqual<T>(a: T[], b: T[]) { |  | ||||||
|     if (a === b) { |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|     if (a.length !== b.length) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     for (let i=0; i < a.length; i++) { |  | ||||||
|         if (a[i] !== b[i]) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return true; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Indexed<T extends object> = T & { index: number }; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Given an object array, alters every object in the array to have an index field assigned to it. |  | ||||||
|  * |  | ||||||
|  * @param items the objects to be numbered. |  | ||||||
|  * @returns the same object for convenience, with the type changed to indicate the new index field. |  | ||||||
|  */ |  | ||||||
| export function numberObjectsInPlace<T extends object>(items: T[]): Indexed<T>[] { |  | ||||||
|     let index = 0; |  | ||||||
|     for (const item of items) { |  | ||||||
|         (item as Indexed<T>).index = index++; |  | ||||||
|     } |  | ||||||
|     return items as Indexed<T>[]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function mapToKeyValueArray<K extends string | number | symbol, V>(map: Record<K, V>) { |  | ||||||
|     const values: { key: K, value: V }[] = []; |  | ||||||
|     for (const [ key, value ] of Object.entries(map)) { |  | ||||||
|         values.push({ key: key as K, value: value as V }); |  | ||||||
|     } |  | ||||||
|     return values; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function getErrorMessage(e: unknown) { |  | ||||||
|     if (e && typeof e === "object" && "message" in e && typeof e.message === "string") { |  | ||||||
|         return e.message; |  | ||||||
|     } else { |  | ||||||
|         return "Unknown error"; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     reloadFrontendApp, |     reloadFrontendApp, | ||||||
|     restartDesktopApp, |     restartDesktopApp, | ||||||
|     reloadTray, |     reloadTray, | ||||||
|     parseDate, |     parseDate, | ||||||
|  |     getMonthsInDateRange, | ||||||
|     formatDateISO, |     formatDateISO, | ||||||
|     formatDateTime, |     formatDateTime, | ||||||
|     formatTimeInterval, |     formatTimeInterval, | ||||||
| @@ -881,7 +748,6 @@ export default { | |||||||
|     localNowDateTime, |     localNowDateTime, | ||||||
|     now, |     now, | ||||||
|     isElectron, |     isElectron, | ||||||
|     isPWA, |  | ||||||
|     isMac, |     isMac, | ||||||
|     isCtrlKey, |     isCtrlKey, | ||||||
|     assertArguments, |     assertArguments, | ||||||
| @@ -894,7 +760,6 @@ export default { | |||||||
|     getNoteTypeClass, |     getNoteTypeClass, | ||||||
|     getMimeTypeClass, |     getMimeTypeClass, | ||||||
|     isHtmlEmpty, |     isHtmlEmpty, | ||||||
|     formatHtml, |  | ||||||
|     clearBrowserCache, |     clearBrowserCache, | ||||||
|     copySelectionToClipboard, |     copySelectionToClipboard, | ||||||
|     dynamicRequire, |     dynamicRequire, | ||||||
|   | |||||||
| @@ -6,11 +6,9 @@ import frocaUpdater from "./froca_updater.js"; | |||||||
| import appContext from "../components/app_context.js"; | import appContext from "../components/app_context.js"; | ||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import type { EntityChange } from "../server_types.js"; | import type { EntityChange } from "../server_types.js"; | ||||||
| import { WebSocketMessage } from "@triliumnext/commons"; |  | ||||||
| import toast from "./toast.js"; |  | ||||||
|  |  | ||||||
| type MessageHandler = (message: WebSocketMessage) => void; | type MessageHandler = (message: any) => void; | ||||||
| let messageHandlers: MessageHandler[] = []; | const messageHandlers: MessageHandler[] = []; | ||||||
|  |  | ||||||
| let ws: WebSocket; | let ws: WebSocket; | ||||||
| let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad; | let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad; | ||||||
| @@ -49,14 +47,10 @@ function logInfo(message: string) { | |||||||
| window.logError = logError; | window.logError = logError; | ||||||
| window.logInfo = logInfo; | window.logInfo = logInfo; | ||||||
|  |  | ||||||
| export function subscribeToMessages(messageHandler: MessageHandler) { | function subscribeToMessages(messageHandler: MessageHandler) { | ||||||
|     messageHandlers.push(messageHandler); |     messageHandlers.push(messageHandler); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function unsubscribeToMessage(messageHandler: MessageHandler) { |  | ||||||
|     messageHandlers = messageHandlers.filter(handler => handler !== messageHandler); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // used to serialize frontend update operations | // used to serialize frontend update operations | ||||||
| let consumeQueuePromise: Promise<void> | null = null; | let consumeQueuePromise: Promise<void> | null = null; | ||||||
|  |  | ||||||
| @@ -279,17 +273,13 @@ function connectWebSocket() { | |||||||
|  |  | ||||||
| async function sendPing() { | async function sendPing() { | ||||||
|     if (Date.now() - lastPingTs > 30000) { |     if (Date.now() - lastPingTs > 30000) { | ||||||
|         console.warn(utils.now(), "Lost websocket connection to the backend"); |         console.log( | ||||||
|         toast.showPersistent({ |             utils.now(), | ||||||
|             id: "lost-websocket-connection", |             "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket." | ||||||
|             title: t("ws.lost-websocket-connection-title"), |         ); | ||||||
|             message: t("ws.lost-websocket-connection-message"), |  | ||||||
|             icon: "no-signal" |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (ws.readyState === ws.OPEN) { |     if (ws.readyState === ws.OPEN) { | ||||||
|         toast.closePersistent("lost-websocket-connection"); |  | ||||||
|         ws.send( |         ws.send( | ||||||
|             JSON.stringify({ |             JSON.stringify({ | ||||||
|                 type: "ping", |                 type: "ping", | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "bootstrap/dist/css/bootstrap.min.css"; | import "./stylesheets/bootstrap.scss"; | ||||||
| import "./stylesheets/auth.css"; | import "./stylesheets/auth.css"; | ||||||
|  |  | ||||||
| // @TriliumNextTODO: is this even needed anymore? | // @TriliumNextTODO: is this even needed anymore? | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import "jquery"; | import "jquery"; | ||||||
|  | import "jquery-hotkeys"; | ||||||
| import utils from "./services/utils.js"; | import utils from "./services/utils.js"; | ||||||
| import ko from "knockout"; | import ko from "knockout"; | ||||||
| import "bootstrap/dist/css/bootstrap.min.css"; | import "./stylesheets/bootstrap.scss"; | ||||||
|  |  | ||||||
| // TriliumNextTODO: properly make use of below types | // TriliumNextTODO: properly make use of below types | ||||||
| // type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | ""; | // type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | ""; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import "normalize.css"; | import "normalize.css"; | ||||||
| import "boxicons/css/boxicons.min.css"; | import "boxicons/css/boxicons.min.css"; | ||||||
| import "@triliumnext/ckeditor5/src/theme/ck-content.css"; | import "@triliumnext/ckeditor5/content.css"; | ||||||
| import "@triliumnext/share-theme/styles/index.css"; | import "@triliumnext/share-theme/styles/index.css"; | ||||||
| import "@triliumnext/share-theme/scripts/index.js"; | import "@triliumnext/share-theme/scripts/index.js"; | ||||||
|  |  | ||||||
| @@ -29,14 +29,6 @@ async function formatCodeBlocks() { | |||||||
|     await formatCodeBlocks($("#content")); |     await formatCodeBlocks($("#content")); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function setupTextNote() { |  | ||||||
|     formatCodeBlocks(); |  | ||||||
|     applyMath(); |  | ||||||
|  |  | ||||||
|     const setupMermaid = (await import("./share/mermaid.js")).default; |  | ||||||
|     setupMermaid(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Fetch note with given ID from backend |  * Fetch note with given ID from backend | ||||||
|  * |  * | ||||||
| @@ -55,11 +47,8 @@ async function fetchNote(noteId: string | null = null) { | |||||||
| document.addEventListener( | document.addEventListener( | ||||||
|     "DOMContentLoaded", |     "DOMContentLoaded", | ||||||
|     () => { |     () => { | ||||||
|         const noteType = determineNoteType(); |         formatCodeBlocks(); | ||||||
|  |         applyMath(); | ||||||
|         if (noteType === "text") { |  | ||||||
|             setupTextNote(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const toggleMenuButton = document.getElementById("toggleMenuButton"); |         const toggleMenuButton = document.getElementById("toggleMenuButton"); | ||||||
|         const layout = document.getElementById("layout"); |         const layout = document.getElementById("layout"); | ||||||
| @@ -71,12 +60,6 @@ document.addEventListener( | |||||||
|     false |     false | ||||||
| ); | ); | ||||||
|  |  | ||||||
| function determineNoteType() { |  | ||||||
|     const bodyClass = document.body.className; |  | ||||||
|     const match = bodyClass.match(/type-([^\s]+)/); |  | ||||||
|     return match ? match[1] : null; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // workaround to prevent webpack from removing "fetchNote" as dead code: | // workaround to prevent webpack from removing "fetchNote" as dead code: | ||||||
| // add fetchNote as property to the window object | // add fetchNote as property to the window object | ||||||
| Object.defineProperty(window, "fetchNote", { | Object.defineProperty(window, "fetchNote", { | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user