Compare commits
4 Commits
renovate/h
...
migrate_pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a81e8adde7 | ||
|
|
5aec9229d4 | ||
|
|
0c954322e4 | ||
|
|
9580d636cf |
@@ -1,6 +1,6 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*.{js,cjs,ts,tsx,css}]
|
[*.{js,ts,tsx}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|||||||
5
.github/actions/build-electron/action.yml
vendored
@@ -21,7 +21,7 @@ runs:
|
|||||||
# Certificate setup
|
# Certificate setup
|
||||||
- name: Import Apple certificates
|
- name: Import Apple certificates
|
||||||
if: inputs.os == 'macos'
|
if: inputs.os == 'macos'
|
||||||
uses: apple-actions/import-codesign-certs@v6
|
uses: apple-actions/import-codesign-certs@v5
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
|
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
|
||||||
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
|
||||||
@@ -30,7 +30,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Installer certificate
|
- name: Install Installer certificate
|
||||||
if: inputs.os == 'macos'
|
if: inputs.os == 'macos'
|
||||||
uses: apple-actions/import-codesign-certs@v6
|
uses: apple-actions/import-codesign-certs@v5
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
|
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
|
||||||
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
||||||
@@ -85,7 +85,6 @@ runs:
|
|||||||
APPLE_ID: ${{ env.APPLE_ID }}
|
APPLE_ID: ${{ env.APPLE_ID }}
|
||||||
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 }}
|
||||||
WINDOWS_SIGN_ERROR_LOG: ${{ env.WINDOWS_SIGN_ERROR_LOG }}
|
|
||||||
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 }}
|
TARGET_ARCH: ${{ inputs.arch }}
|
||||||
run: pnpm run --filter 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 }}
|
||||||
|
|||||||
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@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.branch }}
|
ref: ${{ inputs.branch }}
|
||||||
path: br-base
|
path: br-base
|
||||||
|
|||||||
334
.github/copilot-instructions.md
vendored
@@ -1,334 +0,0 @@
|
|||||||
# Trilium Notes - AI Coding Agent Instructions
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
|
|
||||||
|
|
||||||
## Essential Architecture Patterns
|
|
||||||
|
|
||||||
### Three-Layer Cache System (Critical to Understand)
|
|
||||||
- **Becca** (`apps/server/src/becca/`): Server-side entity cache, primary data source
|
|
||||||
- **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synchronized via WebSocket
|
|
||||||
- **Shaca** (`apps/server/src/share/`): Optimized cache for public/shared notes
|
|
||||||
|
|
||||||
**Key insight**: Never bypass these caches with direct DB queries. Always use `becca.notes[noteId]`, `froca.getNote()`, or equivalent cache methods.
|
|
||||||
|
|
||||||
### Entity Relationship Model
|
|
||||||
Notes use a **multi-parent tree** via branches:
|
|
||||||
- `BNote` - The note content and metadata
|
|
||||||
- `BBranch` - Tree relationships (one note can have multiple parents via cloning)
|
|
||||||
- `BAttribute` - Key-value metadata attached to notes (labels and relations)
|
|
||||||
|
|
||||||
### Entity Change System & Sync
|
|
||||||
Every entity modification (notes, branches, attributes) creates an `EntityChange` record that drives synchronization:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Entity changes are automatically tracked
|
|
||||||
note.title = "New Title";
|
|
||||||
note.save(); // Creates EntityChange record with changeId
|
|
||||||
|
|
||||||
// Sync protocol via WebSocket
|
|
||||||
ws.sendMessage({ type: 'sync-pull-in-progress', ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
**Critical**: This is why you must use Becca/Froca methods instead of direct DB writes - they create the change tracking records needed for sync.
|
|
||||||
|
|
||||||
### Entity Lifecycle & Events
|
|
||||||
The event system (`apps/server/src/services/events.ts`) broadcasts entity lifecycle events:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Subscribe to events in widgets or services
|
|
||||||
eventService.subscribe('noteChanged', ({ noteId }) => {
|
|
||||||
// React to note changes
|
|
||||||
});
|
|
||||||
|
|
||||||
// Common events: noteChanged, branchChanged, attributeChanged, noteDeleted
|
|
||||||
// Widget method: entitiesReloadedEvent({loadResults}) for handling reloads
|
|
||||||
```
|
|
||||||
|
|
||||||
**Becca loader priorities**: Events are emitted in order (notes → branches → attributes) during initial load to ensure referential integrity.
|
|
||||||
|
|
||||||
### TaskContext for Long Operations
|
|
||||||
Use `TaskContext` for operations with progress reporting (imports, exports, bulk operations):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const taskContext = new TaskContext("task-id", "import", "Import Notes");
|
|
||||||
taskContext.increaseProgressCount();
|
|
||||||
|
|
||||||
// WebSocket messages: { type: 'taskProgressCount', taskId, taskType, data, progressCount }
|
|
||||||
|
|
||||||
**Pattern**: All long-running operations (delete note trees, export, import) use TaskContext to send WebSocket updates to the frontend.
|
|
||||||
|
|
||||||
### Protected Session Handling
|
|
||||||
Protected notes require an active encryption session:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Always check before accessing protected content
|
|
||||||
if (note.isContentAvailable()) {
|
|
||||||
const content = note.getContent(); // Safe
|
|
||||||
} else {
|
|
||||||
const title = note.getTitleOrProtected(); // Returns "[protected]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protected session management
|
|
||||||
protectedSessionService.isProtectedSessionAvailable() // Check session
|
|
||||||
protectedSessionService.startProtectedSession() // After password entry
|
|
||||||
```
|
|
||||||
|
|
||||||
**Session timeout**: Protected sessions expire after inactivity. The encryption key is kept in memory only.
|
|
||||||
|
|
||||||
### Attribute Inheritance Patterns
|
|
||||||
Attributes can be inherited through three mechanisms:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. Standard inheritance (#hidePromotedAttributes ~hidePromotedAttributes)
|
|
||||||
note.getInheritableAttributes() // Walks up parent tree
|
|
||||||
|
|
||||||
// 2. Child prefix inheritance (child:label copies to children)
|
|
||||||
parentNote.setLabel("child:icon", "book") // All children inherit this
|
|
||||||
|
|
||||||
// 3. Template relation inheritance (#template=templateNoteId)
|
|
||||||
note.setRelation("template", templateNoteId)
|
|
||||||
note.getInheritedAttributes() // Includes template's inheritable attributes
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cycle prevention**: Inheritance tracking prevents infinite loops when notes reference each other.
|
|
||||||
|
|
||||||
### Widget-Based UI Architecture
|
|
||||||
All UI components extend from widget base classes (`apps/client/src/widgets/`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Right panel widget (sidebar)
|
|
||||||
class MyWidget extends RightPanelWidget {
|
|
||||||
get position() { return 100; } // Order in panel
|
|
||||||
get parentWidget() { return 'right-pane'; }
|
|
||||||
isEnabled() { return this.note && this.note.hasLabel('myLabel'); }
|
|
||||||
async refreshWithNote(note) { /* Update UI */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note-aware widget (responds to note changes)
|
|
||||||
class MyNoteWidget extends NoteContextAwareWidget {
|
|
||||||
async refreshWithNote(note) { /* Refresh when note changes */ }
|
|
||||||
async entitiesReloadedEvent({loadResults}) { /* Handle entity updates */ }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Widgets use jQuery (`this.$widget`) for DOM manipulation. Don't mix React patterns here.
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Running & Testing
|
|
||||||
```bash
|
|
||||||
# From root directory
|
|
||||||
pnpm install # Install dependencies
|
|
||||||
corepack enable # Enable pnpm if not available
|
|
||||||
pnpm server:start # Dev server (http://localhost:8080)
|
|
||||||
pnpm server:start-prod # Production mode server
|
|
||||||
pnpm desktop:start # Desktop app development
|
|
||||||
pnpm server:test spec/etapi/search.spec.ts # Run specific test
|
|
||||||
pnpm test:parallel # Client tests (can run parallel)
|
|
||||||
pnpm test:sequential # Server tests (sequential due to shared DB)
|
|
||||||
pnpm test:all # All tests (parallel + sequential)
|
|
||||||
pnpm coverage # Generate coverage reports
|
|
||||||
pnpm typecheck # Type check all projects
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
```bash
|
|
||||||
pnpm client:build # Build client application
|
|
||||||
pnpm server:build # Build server application
|
|
||||||
pnpm desktop:build # Build desktop application
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Organization
|
|
||||||
- **Server tests** (`apps/server/spec/`): Must run sequentially (shared database state)
|
|
||||||
- **Client tests** (`apps/client/src/`): Can run in parallel
|
|
||||||
- **E2E tests** (`apps/server-e2e/`): Use Playwright for integration testing
|
|
||||||
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
|
|
||||||
|
|
||||||
**Pattern**: When adding new API endpoints, add tests in `spec/etapi/` following existing patterns (see `search.spec.ts`).
|
|
||||||
|
|
||||||
### Monorepo Navigation
|
|
||||||
```
|
|
||||||
apps/
|
|
||||||
client/ # Frontend (shared by server & desktop)
|
|
||||||
server/ # Node.js backend with REST API
|
|
||||||
desktop/ # Electron wrapper
|
|
||||||
web-clipper/ # Browser extension for saving web content
|
|
||||||
db-compare/ # Database comparison tool
|
|
||||||
dump-db/ # Database export utility
|
|
||||||
edit-docs/ # Documentation editing tools
|
|
||||||
packages/
|
|
||||||
commons/ # Shared types and utilities
|
|
||||||
ckeditor5/ # Custom rich text editor with Trilium-specific plugins
|
|
||||||
codemirror/ # Code editor integration
|
|
||||||
highlightjs/ # Syntax highlighting
|
|
||||||
share-theme/ # Theme for shared/published notes
|
|
||||||
ckeditor5-admonition/ # Admonition blocks plugin
|
|
||||||
ckeditor5-footnotes/ # Footnotes plugin
|
|
||||||
ckeditor5-math/ # Math equations plugin
|
|
||||||
ckeditor5-mermaid/ # Mermaid diagrams plugin
|
|
||||||
```
|
|
||||||
|
|
||||||
**Filter commands**: Use `pnpm --filter server test` to run commands in specific packages.
|
|
||||||
|
|
||||||
## Critical Code Patterns
|
|
||||||
|
|
||||||
### ETAPI Backwards Compatibility
|
|
||||||
When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), maintain backwards compatibility by checking if new params exist before changing response format.
|
|
||||||
|
|
||||||
**Pattern**: ETAPI consumers expect specific response shapes. Always check for breaking changes.
|
|
||||||
|
|
||||||
### Frontend-Backend Communication
|
|
||||||
- **REST API**: `apps/server/src/routes/api/` - Internal endpoints (no auth required when `noAuthentication=true`)
|
|
||||||
- **ETAPI**: `apps/server/src/etapi/` - External API with authentication
|
|
||||||
- **WebSocket**: Real-time sync via `apps/server/src/services/ws.ts`
|
|
||||||
|
|
||||||
**Auth note**: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
- Add scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
|
|
||||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
|
||||||
- Never bypass Becca cache after migrations
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
1. **Never bypass the cache layers** - Always use `becca.notes[noteId]`, `froca.getNote()`, or equivalent cache methods. Direct database queries will cause sync issues between Becca/Froca/Shaca and won't create EntityChange records needed for synchronization.
|
|
||||||
|
|
||||||
2. **Protected notes require session check** - Before accessing `note.title` or `note.getContent()` on protected notes, check `note.isContentAvailable()` or use `note.getTitleOrProtected()` which handles this automatically.
|
|
||||||
|
|
||||||
3. **Widget lifecycle matters** - Override `refreshWithNote()` for note changes, `doRenderBody()` for initial render, `entitiesReloadedEvent()` for entity updates. Widgets use jQuery (`this.$widget`) - don't mix React patterns.
|
|
||||||
|
|
||||||
4. **Tests run differently** - Server tests must run sequentially (shared database state), client tests can run in parallel. Use `pnpm test:sequential` for backend, `pnpm test:parallel` for frontend.
|
|
||||||
|
|
||||||
5. **ETAPI requires authentication** - ETAPI endpoints use basic auth with tokens. Internal API endpoints (`apps/server/src/routes/api/`) trust the frontend when `noAuthentication=true`.
|
|
||||||
|
|
||||||
6. **Search expressions are evaluated in memory** - The search service loads all matching notes, scores them in JavaScript, then sorts. You cannot add SQL-level LIMIT/OFFSET without losing scoring functionality.
|
|
||||||
|
|
||||||
7. **Documentation edits have rules** - `docs/Script API/` is auto-generated (never edit directly). `docs/User Guide/` should be edited via `pnpm edit-docs:edit-docs`, not manually. Only `docs/Developer Guide/` and `docs/Release Notes/` are safe for direct Markdown editing.
|
|
||||||
|
|
||||||
8. **pnpm workspace filtering** - Use `pnpm --filter server <command>` or shorthand `pnpm server:test` defined in root `package.json`. Note the `--filter` syntax, not `-F` or other shortcuts.
|
|
||||||
|
|
||||||
9. **Event subscription cleanup** - When subscribing to events in widgets, unsubscribe in `cleanup()` or `doDestroy()` to prevent memory leaks.
|
|
||||||
|
|
||||||
10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note.
|
|
||||||
|
|
||||||
## TypeScript Configuration
|
|
||||||
|
|
||||||
- **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`)
|
|
||||||
- **Path mapping**: Use relative imports, not path aliases
|
|
||||||
- **Build order**: `pnpm typecheck` builds all projects in dependency order
|
|
||||||
- **Build system**: Uses Vite for fast development, ESBuild for production optimization
|
|
||||||
- **Patches**: Custom patches in `patches/` directory for CKEditor and other dependencies
|
|
||||||
|
|
||||||
## Key Files for Context
|
|
||||||
|
|
||||||
- `apps/server/src/becca/entities/bnote.ts` - Note entity methods
|
|
||||||
- `apps/client/src/services/froca.ts` - Frontend cache API
|
|
||||||
- `apps/server/src/services/search/services/search.ts` - Search implementation
|
|
||||||
- `apps/server/src/routes/routes.ts` - API route registration
|
|
||||||
- `apps/client/src/widgets/basic_widget.ts` - Widget base class
|
|
||||||
- `apps/server/src/main.ts` - Server startup entry point
|
|
||||||
- `apps/client/src/desktop.ts` - Client initialization
|
|
||||||
- `apps/server/src/services/backend_script_api.ts` - Scripting API
|
|
||||||
- `apps/server/src/assets/db/schema.sql` - Database schema
|
|
||||||
|
|
||||||
## Note Types and Features
|
|
||||||
|
|
||||||
Trilium supports multiple note types with specialized widgets in `apps/client/src/widgets/type_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
|
|
||||||
|
|
||||||
### Collections
|
|
||||||
Notes can be marked with the `#collection` label to enable collection view modes. Collections support multiple view types:
|
|
||||||
- **List**: Standard list view
|
|
||||||
- **Grid**: Card/grid layout
|
|
||||||
- **Calendar**: Calendar-based view
|
|
||||||
- **Table**: Tabular data view
|
|
||||||
- **GeoMap**: Geographic map view
|
|
||||||
- **Board**: Kanban-style board
|
|
||||||
- **Presentation**: Slideshow presentation mode
|
|
||||||
|
|
||||||
View types are configured via `#viewType` label (e.g., `#viewType=table`). Each view mode stores its configuration in a separate attachment (e.g., `table.json`). Collections are organized separately from regular note type templates in the note creation menu.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
- Remember: scoring happens in-memory, not at database level
|
|
||||||
|
|
||||||
### Custom CKEditor Plugins
|
|
||||||
- Create new package in `packages/` following existing plugin structure
|
|
||||||
- Register in `packages/ckeditor5/src/plugins.ts`
|
|
||||||
- See `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` for examples
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
- Add migration scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
|
|
||||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
|
||||||
- Never bypass Becca cache after migrations
|
|
||||||
|
|
||||||
## Security & Features
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
- Per-note encryption with granular protected sessions
|
|
||||||
- CSRF protection for API endpoints
|
|
||||||
- OpenID and TOTP authentication support
|
|
||||||
- Sanitization of user-generated content
|
|
||||||
|
|
||||||
### Scripting System
|
|
||||||
Trilium provides powerful user scripting capabilities:
|
|
||||||
- **Frontend scripts**: Run in browser context with UI access
|
|
||||||
- **Backend scripts**: Run in Node.js context with full API access
|
|
||||||
- Script API documentation in `docs/Script API/`
|
|
||||||
- Backend API available via `api` object in script context
|
|
||||||
|
|
||||||
### Internationalization
|
|
||||||
- Translation files in `apps/client/src/translations/`
|
|
||||||
- Use translation system via `t()` function
|
|
||||||
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
|
|
||||||
|
|
||||||
## Testing Conventions
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ETAPI test pattern
|
|
||||||
describe("etapi/feature", () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
config.General.noAuthentication = false;
|
|
||||||
app = await buildApp();
|
|
||||||
token = await login(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should test feature", async () => {
|
|
||||||
const response = await supertest(app)
|
|
||||||
.get("/etapi/notes?search=test")
|
|
||||||
.auth(USER, token, { type: "basic" })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.results).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Questions to Verify Understanding
|
|
||||||
|
|
||||||
Before implementing significant changes, confirm:
|
|
||||||
- Is this touching the cache layer? (Becca/Froca/Shaca must stay in sync via EntityChange records)
|
|
||||||
- Does this change API response shape? (Check backwards compatibility for ETAPI)
|
|
||||||
- Are you adding search features? (Understand expression-based architecture and in-memory scoring first)
|
|
||||||
- Is this a new widget? (Know which base class and lifecycle methods to use)
|
|
||||||
- Does this involve protected notes? (Check `isContentAvailable()` before accessing content)
|
|
||||||
- Is this a long-running operation? (Use TaskContext for progress reporting)
|
|
||||||
- Are you working with attributes? (Understand inheritance patterns: direct, child-prefix, template)
|
|
||||||
2
.github/workflows/checks.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if PRs have conflicts
|
- name: Check if PRs have conflicts
|
||||||
uses: eps1lon/actions-label-merge-conflict@v3
|
uses: eps1lon/actions-label-merge-conflict@v3
|
||||||
if: ${{ github.repository == vars.REPO_MAIN }}
|
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: "merge-conflicts"
|
dirtyLabel: "merge-conflicts"
|
||||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||||
|
|||||||
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@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# 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`
|
||||||
|
|||||||
4
.github/workflows/deploy-docs.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: ./.github/actions/deploy-to-cloudflare-pages
|
uses: ./.github/actions/deploy-to-cloudflare-pages
|
||||||
if: ${{ github.repository == vars.REPO_MAIN }}
|
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||||
with:
|
with:
|
||||||
project_name: "trilium-docs"
|
project_name: "trilium-docs"
|
||||||
comment_body: "📚 Documentation preview is ready"
|
comment_body: "📚 Documentation preview is ready"
|
||||||
|
|||||||
41
.github/workflows/dev.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
@@ -37,35 +37,8 @@ jobs:
|
|||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: pnpm typecheck
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Run the client-side tests
|
- name: Run the unit tests
|
||||||
run: pnpm run --filter=client test
|
run: pnpm run test:all
|
||||||
|
|
||||||
- name: Upload client test report
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: client-test-report
|
|
||||||
path: apps/client/test-output/vitest/html/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Run the server-side tests
|
|
||||||
run: pnpm run --filter=server test
|
|
||||||
|
|
||||||
- name: Upload server test report
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: server-test-report
|
|
||||||
path: apps/server/test-output/vitest/html/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Run CKEditor e2e tests
|
|
||||||
run: |
|
|
||||||
pnpm run --filter=ckeditor5-mermaid test
|
|
||||||
pnpm run --filter=ckeditor5-math test
|
|
||||||
|
|
||||||
- name: Run the rest of the tests
|
|
||||||
run: pnpm run --filter=\!client --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test
|
|
||||||
|
|
||||||
build_docker:
|
build_docker:
|
||||||
name: Build Docker image
|
name: Build Docker image
|
||||||
@@ -73,7 +46,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- test_dev
|
- test_dev
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- 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
|
||||||
@@ -90,7 +63,7 @@ jobs:
|
|||||||
- name: Trigger server build
|
- name: Trigger server build
|
||||||
run: pnpm run server:build
|
run: pnpm run server:build
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
- uses: docker/build-push-action@v7
|
- uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: apps/server
|
context: apps/server
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
@@ -107,7 +80,7 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -127,7 +100,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and export to Docker
|
- name: Build and export to Docker
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: apps/server
|
context: apps/server
|
||||||
file: apps/server/${{ matrix.dockerfile }}
|
file: apps/server/${{ matrix.dockerfile }}
|
||||||
|
|||||||
30
.github/workflows/i18n.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Internationalization
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "weblate:*"
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "apps/client/src/translations/**"
|
|
||||||
- ".github/workflows/i18n.yml"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
i18n-check:
|
|
||||||
name: Check i18n translations
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- name: Set up node & dependencies
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
- name: Check translations
|
|
||||||
run: pnpm tsx scripts/translation/check-translation-coverage.ts
|
|
||||||
146
.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@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- 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
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
run: pnpm run server:build
|
run: pnpm run server:build
|
||||||
|
|
||||||
- name: Build and export to Docker
|
- name: Build and export to Docker
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: apps/server
|
context: apps/server
|
||||||
file: apps/server/${{ matrix.dockerfile }}
|
file: apps/server/${{ matrix.dockerfile }}
|
||||||
@@ -86,12 +86,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright trace
|
- name: Upload Playwright trace
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: Playwright trace (${{ matrix.dockerfile }})
|
name: Playwright trace (${{ matrix.dockerfile }})
|
||||||
path: test-output/playwright/output
|
path: test-output/playwright/output
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v7
|
- uses: actions/upload-artifact@v5
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
name: Playwright report (${{ matrix.dockerfile }})
|
name: Playwright report (${{ matrix.dockerfile }})
|
||||||
@@ -141,7 +141,7 @@ 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@v6
|
uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -155,18 +155,16 @@ jobs:
|
|||||||
- name: Update build info
|
- name: Update build info
|
||||||
run: pnpm run chore:update-build-info
|
run: pnpm run chore:update-build-info
|
||||||
|
|
||||||
- name: Update nightly version
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: pnpm run chore:ci-update-nightly-version
|
|
||||||
|
|
||||||
- name: Run the TypeScript build
|
- name: Run the TypeScript build
|
||||||
run: pnpm run server:build
|
run: pnpm run server:build
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: |
|
||||||
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
@@ -175,21 +173,28 @@ jobs:
|
|||||||
latest=false
|
latest=false
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: apps/server
|
context: apps/server
|
||||||
file: apps/server/${{ matrix.dockerfile }}
|
file: apps/server/${{ matrix.dockerfile }}
|
||||||
@@ -204,7 +209,7 @@ jobs:
|
|||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
@@ -218,7 +223,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
@@ -228,86 +233,75 @@ jobs:
|
|||||||
- name: Set TEST_TAG to lowercase
|
- name: Set TEST_TAG to lowercase
|
||||||
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up crane
|
- name: Set up Docker Buildx
|
||||||
uses: imjasonh/setup-crane@v0.5
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Create manifest list and push
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v6
|
|
||||||
with:
|
|
||||||
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
type=sha
|
|
||||||
flavor: |
|
|
||||||
latest=false
|
|
||||||
|
|
||||||
- name: Verify digests exist on GHCR
|
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
echo "Verifying all digests are available on GHCR..."
|
# Extract the branch or tag name from the ref
|
||||||
for DIGEST_FILE in *; do
|
REF_NAME=$(echo "${GITHUB_REF}" | sed 's/refs\/heads\///' | sed 's/refs\/tags\///')
|
||||||
DIGEST="sha256:${DIGEST_FILE}"
|
|
||||||
echo -n " ${DIGEST}: "
|
|
||||||
crane manifest "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" > /dev/null
|
|
||||||
echo "OK"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Create and push multi-arch manifest
|
# Create and push the manifest list with both the branch/tag name and the commit SHA
|
||||||
working-directory: /tmp/digests
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
run: |
|
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
|
||||||
GHCR_IMAGE="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}"
|
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
DOCKERHUB_IMAGE="${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}"
|
|
||||||
|
|
||||||
# Build -m flags for crane index append from digest files
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
MANIFEST_ARGS=""
|
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
|
||||||
for d in *; do
|
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
MANIFEST_ARGS="${MANIFEST_ARGS} -m ${GHCR_IMAGE}@sha256:${d}"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Create multi-arch manifest for each tag from metadata, plus copy to DockerHub
|
# If the ref is a tag, also tag the image as stable as this is part of a 'release'
|
||||||
while IFS= read -r TAG; do
|
# and only go in the `if` if there is NOT a `-` in the tag's name, due to tagging of `-alpha`, `-beta`, etc...
|
||||||
echo "Creating manifest: ${TAG}"
|
|
||||||
crane index append ${MANIFEST_ARGS} -t "${TAG}"
|
|
||||||
|
|
||||||
SUFFIX="${TAG#*:}"
|
|
||||||
echo "Copying to DockerHub: ${DOCKERHUB_IMAGE}:${SUFFIX}"
|
|
||||||
crane copy "${TAG}" "${DOCKERHUB_IMAGE}:${SUFFIX}"
|
|
||||||
done <<< "${{ steps.meta.outputs.tags }}"
|
|
||||||
|
|
||||||
# For stable releases (tags without hyphens), also create stable + latest
|
|
||||||
REF_NAME="${GITHUB_REF#refs/tags/}"
|
|
||||||
if [[ "${GITHUB_REF}" == refs/tags/* && ! "${REF_NAME}" =~ - ]]; then
|
if [[ "${GITHUB_REF}" == refs/tags/* && ! "${REF_NAME}" =~ - ]]; then
|
||||||
echo "Creating stable tags..."
|
# First create stable tags
|
||||||
crane index append ${MANIFEST_ARGS} -t "${GHCR_IMAGE}:stable"
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
crane copy "${GHCR_IMAGE}:stable" "${DOCKERHUB_IMAGE}:stable"
|
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
|
||||||
|
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
|
||||||
|
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
# Small delay to ensure stable tag is fully propagated
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Now update latest tags
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||||
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
|
||||||
|
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||||
|
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
|
||||||
|
|
||||||
echo "Creating latest tags..."
|
|
||||||
crane copy "${GHCR_IMAGE}:stable" "${GHCR_IMAGE}:latest"
|
|
||||||
crane copy "${GHCR_IMAGE}:latest" "${DOCKERHUB_IMAGE}:latest"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Inspect manifests
|
- name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
REF_NAME="${GITHUB_REF#refs/heads/}"
|
docker buildx imagetools inspect ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
REF_NAME="${REF_NAME#refs/tags/}"
|
docker buildx imagetools inspect ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
echo "=== GHCR ==="
|
|
||||||
crane manifest "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "=== DockerHub ==="
|
|
||||||
crane manifest "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME}"
|
|
||||||
|
|||||||
30
.github/workflows/nightly.yml
vendored
@@ -26,7 +26,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nightly-electron:
|
nightly-electron:
|
||||||
if: ${{ github.repository == vars.REPO_MAIN }}
|
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||||
name: Deploy nightly
|
name: Deploy nightly
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -45,22 +45,9 @@ 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@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -70,7 +57,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Update nightly version
|
- name: Update nightly version
|
||||||
run: pnpm run chore:ci-update-nightly-version
|
run: npm run chore:ci-update-nightly-version
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-electron
|
uses: ./.github/actions/build-electron
|
||||||
with:
|
with:
|
||||||
@@ -87,11 +74,10 @@ 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 }}
|
||||||
WINDOWS_SIGN_ERROR_LOG: ${{ vars.WINDOWS_SIGN_ERROR_LOG }}
|
|
||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.5.0
|
uses: softprops/action-gh-release@v2.4.2
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
@@ -103,14 +89,14 @@ jobs:
|
|||||||
name: Nightly Build
|
name: Nightly Build
|
||||||
|
|
||||||
- name: Publish artifacts
|
- name: Publish artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
||||||
path: apps/desktop/upload
|
path: apps/desktop/upload
|
||||||
|
|
||||||
nightly-server:
|
nightly-server:
|
||||||
if: ${{ github.repository == vars.REPO_MAIN }}
|
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||||
name: Deploy server nightly
|
name: Deploy server nightly
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -123,7 +109,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@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-server
|
uses: ./.github/actions/build-server
|
||||||
@@ -132,7 +118,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.5.0
|
uses: softprops/action-gh-release@v2.4.2
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
|
|||||||
6
.github/workflows/playwright.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
||||||
TRILIUM_INTEGRATION_TEST: memory
|
TRILIUM_INTEGRATION_TEST: memory
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
filter: tree:0
|
filter: tree:0
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -77,9 +77,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: e2e report ${{ matrix.arch }}
|
name: e2e report
|
||||||
path: apps/server-e2e/test-output
|
path: apps/server-e2e/test-output
|
||||||
|
|
||||||
- name: Kill the server
|
- name: Kill the server
|
||||||
|
|||||||
37
.github/workflows/release.yml
vendored
@@ -11,28 +11,8 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sanity-check:
|
|
||||||
name: Sanity Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- name: Set up node & dependencies
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --filter source --frozen-lockfile --ignore-scripts
|
|
||||||
|
|
||||||
- name: Check version consistency
|
|
||||||
run: pnpm tsx ${{ github.workspace }}/scripts/check-version-consistency.ts ${{ github.ref_name }}
|
|
||||||
make-electron:
|
make-electron:
|
||||||
name: Make Electron
|
name: Make Electron
|
||||||
needs:
|
|
||||||
- sanity-check
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -65,7 +45,7 @@ jobs:
|
|||||||
forge_platform: linux
|
forge_platform: linux
|
||||||
runs-on: ${{ matrix.os.image }}
|
runs-on: ${{ matrix.os.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -90,19 +70,16 @@ 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 }}
|
||||||
WINDOWS_SIGN_ERROR_LOG: ${{ vars.WINDOWS_SIGN_ERROR_LOG }}
|
|
||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Upload the artifact
|
- name: Upload the artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
||||||
path: apps/desktop/upload/*.*
|
path: apps/desktop/upload/*.*
|
||||||
|
|
||||||
build_server:
|
build_server:
|
||||||
name: Build Linux Server
|
name: Build Linux Server
|
||||||
needs:
|
|
||||||
- sanity-check
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -114,7 +91,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@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-server
|
uses: ./.github/actions/build-server
|
||||||
@@ -123,7 +100,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload the artifact
|
- name: Upload the artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: release-server-linux-${{ matrix.arch }}
|
name: release-server-linux-${{ matrix.arch }}
|
||||||
path: upload/*.*
|
path: upload/*.*
|
||||||
@@ -137,20 +114,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: mkdir upload
|
- run: mkdir upload
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
docs/Release Notes
|
docs/Release Notes
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
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.5.0
|
uses: softprops/action-gh-release@v2.4.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
|
||||||
|
|||||||
69
.github/workflows/web-clipper.yml
vendored
@@ -1,69 +0,0 @@
|
|||||||
name: Deploy web clipper extension
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "apps/web-clipper/**"
|
|
||||||
tags:
|
|
||||||
- "web-clipper-v*"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "apps/web-clipper/**"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
discussions: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Build web clipper extension
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- name: Set up node & dependencies
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --filter web-clipper --frozen-lockfile --ignore-scripts
|
|
||||||
|
|
||||||
- name: Build the web clipper extension
|
|
||||||
run: |
|
|
||||||
pnpm --filter web-clipper zip
|
|
||||||
pnpm --filter web-clipper zip:firefox
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/web-clipper-v') }}
|
|
||||||
with:
|
|
||||||
name: web-clipper-extension
|
|
||||||
path: apps/web-clipper/.output/*.zip
|
|
||||||
include-hidden-files: true
|
|
||||||
if-no-files-found: error
|
|
||||||
compression-level: 0
|
|
||||||
|
|
||||||
- name: Release web clipper extension
|
|
||||||
uses: softprops/action-gh-release@v2.5.0
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/web-clipper-v') }}
|
|
||||||
with:
|
|
||||||
draft: false
|
|
||||||
fail_on_unmatched_files: true
|
|
||||||
files: apps/web-clipper/.output/*.zip
|
|
||||||
discussion_category_name: Releases
|
|
||||||
make_latest: false
|
|
||||||
token: ${{ secrets.RELEASE_PAT }}
|
|
||||||
4
.github/workflows/website.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
pull-requests: write # For PR preview comments
|
pull-requests: write # For PR preview comments
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --filter website --frozen-lockfile --ignore-scripts
|
run: pnpm install --filter website --frozen-lockfile
|
||||||
|
|
||||||
- name: Build the website
|
- name: Build the website
|
||||||
run: pnpm website:build
|
run: pnpm website:build
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -44,11 +44,8 @@ upload
|
|||||||
.rollup.cache
|
.rollup.cache
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
/.direnv
|
|
||||||
/result
|
/result
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
|
|
||||||
# docs
|
# docs
|
||||||
site/
|
site/
|
||||||
apps/*/coverage
|
|
||||||
scripts/translation/.language*.json
|
|
||||||
|
|||||||
3
.vscode/extensions.json
vendored
@@ -9,6 +9,7 @@
|
|||||||
"tobermory.es6-string-html",
|
"tobermory.es6-string-html",
|
||||||
"vitest.explorer",
|
"vitest.explorer",
|
||||||
"yzhang.markdown-all-in-one",
|
"yzhang.markdown-all-in-one",
|
||||||
"usernamehw.errorlens"
|
"svelte.svelte-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
57
.vscode/launch.json
vendored
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch client (Chrome)",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "chrome",
|
|
||||||
"url": "http://localhost:8080",
|
|
||||||
"webRoot": "${workspaceFolder}/apps/client"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Launch server",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder}/apps/server/src/main.ts",
|
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "development",
|
|
||||||
"TRILIUM_ENV": "dev",
|
|
||||||
"TRILIUM_DATA_DIR": "${input:trilium_data_dir}",
|
|
||||||
"TRILIUM_RESOURCE_DIR": "${workspaceFolder}/apps/server/src"
|
|
||||||
},
|
|
||||||
"autoAttachChildProcesses": true,
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Launch Vitest with current test file",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"autoAttachChildProcesses": true,
|
|
||||||
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
|
|
||||||
"args": ["run", "${relativeFile}"],
|
|
||||||
"smartStep": true,
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"compounds": [
|
|
||||||
{
|
|
||||||
"name": "Launch client (Chrome) and server",
|
|
||||||
"configurations": ["Launch server","Launch client (Chrome)"],
|
|
||||||
"stopAll": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "trilium_data_dir",
|
|
||||||
"type": "promptString",
|
|
||||||
"description": "Select Trilum Notes data directory",
|
|
||||||
"default": "${workspaceFolder}/apps/server/data"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
13
.vscode/settings.json
vendored
@@ -36,14 +36,5 @@
|
|||||||
"docs/**/*.png": true,
|
"docs/**/*.png": true,
|
||||||
"apps/server/src/assets/doc_notes/**": true,
|
"apps/server/src/assets/doc_notes/**": true,
|
||||||
"apps/edit-docs/demo/**": true
|
"apps/edit-docs/demo/**": true
|
||||||
},
|
}
|
||||||
"editor.codeActionsOnSave": {
|
}
|
||||||
"source.fixAll.eslint": "explicit"
|
|
||||||
},
|
|
||||||
"eslint.rules.customizations": [
|
|
||||||
{ "rule": "*", "severity": "warn" }
|
|
||||||
],
|
|
||||||
"cSpell.words": [
|
|
||||||
"Trilium"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
74
README.md
@@ -16,14 +16,13 @@
|
|||||||

|

|
||||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
<!-- translate:off -->
|
[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)
|
||||||
<!-- LANGUAGE SWITCHER -->
|
|
||||||
[Chinese (Simplified Han script)](./docs/README-ZH_CN.md) | [Chinese (Traditional Han script)](./docs/README-ZH_TW.md) | [English](./docs/README.md) | [French](./docs/README-fr.md) | [German](./docs/README-de.md) | [Greek](./docs/README-el.md) | [Italian](./docs/README-it.md) | [Japanese](./docs/README-ja.md) | [Romanian](./docs/README-ro.md) | [Spanish](./docs/README-es.md)
|
|
||||||
<!-- translate:on -->
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
<img src="./docs/app.png" alt="Trilium Screenshot" width="1000">
|
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
|
||||||
|
|
||||||
|
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
|
||||||
|
|
||||||
## ⏬ Download
|
## ⏬ Download
|
||||||
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – stable version, recommended for most users.
|
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – stable version, recommended for most users.
|
||||||
@@ -40,39 +39,39 @@ Our documentation is available in multiple formats:
|
|||||||
|
|
||||||
### Quick Links
|
### Quick Links
|
||||||
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
||||||
- [Installation Instructions](https://docs.triliumnotes.org/user-guide/setup)
|
- [Installation Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
||||||
- [Docker Setup](https://docs.triliumnotes.org/user-guide/setup/server/installation/docker)
|
- [Docker Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
|
||||||
- [Upgrading TriliumNext](https://docs.triliumnotes.org/user-guide/setup/upgrading)
|
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
|
||||||
- [Basic Concepts and Features](https://docs.triliumnotes.org/user-guide/concepts/notes)
|
- [Basic Concepts and Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
|
||||||
- [Patterns of Personal Knowledge Base](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
|
- [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://docs.triliumnotes.org/user-guide/concepts/notes/cloning))
|
* 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))
|
||||||
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://docs.triliumnotes.org/user-guide/note-types/text) with markdown [autoformat](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
|
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
|
||||||
* Support for editing [notes with source code](https://docs.triliumnotes.org/user-guide/note-types/code), including syntax highlighting
|
* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting
|
||||||
* Fast and easy [navigation between notes](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation), full text search and [note hoisting](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
|
* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
|
||||||
* Seamless [note versioning](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
|
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
|
||||||
* Note [attributes](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes) can be used for note organization, querying and advanced [scripting](https://docs.triliumnotes.org/user-guide/scripts)
|
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
|
||||||
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
|
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
|
||||||
* Direct [OpenID and TOTP integration](https://docs.triliumnotes.org/user-guide/setup/server/mfa) for more secure login
|
* Direct [OpenID and TOTP integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) for more secure login
|
||||||
* [Synchronization](https://docs.triliumnotes.org/user-guide/setup/synchronization) with self-hosted sync server
|
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
|
||||||
* there are [3rd party services for hosting synchronisation server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
|
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
|
||||||
* [Sharing](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing) (publishing) notes to public internet
|
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
|
||||||
* Strong [note encryption](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes) with per-note granularity
|
* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity
|
||||||
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
|
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
|
||||||
* [Relation maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and [note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map) for visualizing notes and their relations
|
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations
|
||||||
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
||||||
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with location pins and GPX tracks
|
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with location pins and GPX tracks
|
||||||
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced showcases](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
|
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
|
||||||
* [REST API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) for automation
|
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
|
||||||
* Scales well in both usability and performance upwards of 100 000 notes
|
* Scales well in both usability and performance upwards of 100 000 notes
|
||||||
* Touch optimized [mobile frontend](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend) for smartphones and tablets
|
* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets
|
||||||
* Built-in [dark theme](https://docs.triliumnotes.org/user-guide/concepts/themes), support for user themes
|
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support for user themes
|
||||||
* [Evernote](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote) and [Markdown import & export](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
|
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
|
||||||
* [Web Clipper](https://docs.triliumnotes.org/user-guide/setup/web-clipper) for easy saving of web content
|
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content
|
||||||
* Customizable UI (sidebar buttons, user-defined widgets, ...)
|
* Customizable UI (sidebar buttons, user-defined widgets, ...)
|
||||||
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics), along with a Grafana Dashboard.
|
* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along with a [Grafana Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json)
|
||||||
|
|
||||||
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
|
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
|
||||||
|
|
||||||
@@ -132,7 +131,7 @@ Note: It is best to disable automatic updates on your server installation (see b
|
|||||||
|
|
||||||
### 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://docs.triliumnotes.org/user-guide/setup/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).
|
||||||
|
|
||||||
|
|
||||||
## 💻 Contribute
|
## 💻 Contribute
|
||||||
@@ -165,17 +164,6 @@ pnpm install
|
|||||||
pnpm edit-docs:edit-docs
|
pnpm edit-docs:edit-docs
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, if you have Nix installed:
|
|
||||||
```shell
|
|
||||||
# Run directly
|
|
||||||
nix run .#edit-docs
|
|
||||||
|
|
||||||
# Or install to your profile
|
|
||||||
nix profile install .#edit-docs
|
|
||||||
trilium-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
|
||||||
@@ -210,7 +198,7 @@ Trilium would not be possible without the technologies behind it:
|
|||||||
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
|
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
|
||||||
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections.
|
* [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.
|
* [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://docs.triliumnotes.org/user-guide/note-types/relation-map) and [link maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
|
* [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
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ Description above is a general rule and may be altered on case by case basis.
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
* For low severity vulnerabilities, they can be reported as GitHub issues.
|
You can report low severity vulnerabilities as GitHub issues, more severe vulnerabilities should be reported to the email [contact@eliandoran.me](mailto:contact@eliandoran.me)
|
||||||
* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories).
|
|
||||||
|
|||||||
7
_regroup/bin/create-anonymization-script.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import anonymizationService from "../src/services/anonymization.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript());
|
||||||
52
_regroup/bin/create-icons.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if ! command -v magick &> /dev/null; then
|
||||||
|
echo "This tool requires ImageMagick to be installed in order to create the icons."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v inkscape &> /dev/null; then
|
||||||
|
echo "This tool requires Inkscape to be render sharper SVGs than ImageMagick."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v icnsutil &> /dev/null; then
|
||||||
|
echo "This tool requires icnsutil to be installed in order to generate macOS icons."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
script_dir=$(realpath $(dirname $0))
|
||||||
|
cd "${script_dir}/../images/app-icons"
|
||||||
|
inkscape -w 180 -h 180 "../icon-color.svg" -o "./ios/apple-touch-icon.png"
|
||||||
|
|
||||||
|
# Build PNGs
|
||||||
|
inkscape -w 128 -h 128 "../icon-color.svg" -o "./png/128x128.png"
|
||||||
|
inkscape -w 256 -h 256 "../icon-color.svg" -o "./png/256x256.png"
|
||||||
|
|
||||||
|
# Build dev icons (including tray)
|
||||||
|
inkscape -w 16 -h 16 "../icon-purple.svg" -o "./png/16x16-dev.png"
|
||||||
|
inkscape -w 32 -h 32 "../icon-purple.svg" -o "./png/32x32-dev.png"
|
||||||
|
inkscape -w 256 -h 256 "../icon-purple.svg" -o "./png/256x256-dev.png"
|
||||||
|
|
||||||
|
# Build Mac .icns
|
||||||
|
declare -a sizes=("16" "32" "512" "1024")
|
||||||
|
for size in "${sizes[@]}"; do
|
||||||
|
inkscape -w $size -h $size "../icon-color.svg" -o "./png/${size}x${size}.png"
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p fakeapp.app
|
||||||
|
npx iconsur set fakeapp.app -l -i "png/1024x1024.png" -o "mac/1024x1024.png" -s 0.8
|
||||||
|
declare -a sizes=("16x16" "32x32" "128x128" "512x512")
|
||||||
|
for size in "${sizes[@]}"; do
|
||||||
|
magick "mac/1024x1024.png" -resize "${size}" "mac/${size}.png"
|
||||||
|
done
|
||||||
|
icnsutil compose -f "mac/icon.icns" ./mac/*.png
|
||||||
|
|
||||||
|
# Build Windows icon
|
||||||
|
magick -background none "../icon-color.svg" -define icon:auto-resize=16,32,48,64,128,256 "./icon.ico"
|
||||||
|
|
||||||
|
# Build Windows setup icon
|
||||||
|
magick -background none "../icon-installer.svg" -define icon:auto-resize=16,32,48,64,128,256 "./win/setup.ico"
|
||||||
|
|
||||||
|
# Build Squirrel splash image
|
||||||
|
magick "./png/256x256.png" -background "#ffffff" -gravity center -extent 640x480 "./win/setup-banner.gif"
|
||||||
7
_regroup/bin/export-schema.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
SCHEMA_FILE_PATH=db/schema.sql
|
||||||
|
|
||||||
|
sqlite3 ./data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH"
|
||||||
|
|
||||||
|
echo "DB schema exported to $SCHEMA_FILE_PATH"
|
||||||
@@ -6,11 +6,10 @@
|
|||||||
import sqlInit from "../src/services/sql_init.js";
|
import sqlInit from "../src/services/sql_init.js";
|
||||||
import noteService from "../src/services/notes.js";
|
import noteService from "../src/services/notes.js";
|
||||||
import attributeService from "../src/services/attributes.js";
|
import attributeService from "../src/services/attributes.js";
|
||||||
import cloningService from "../src/services/cloning.js";
|
|
||||||
import { loremIpsum } from "lorem-ipsum";
|
|
||||||
import "../src/becca/entity_constructor.js";
|
|
||||||
import { initializeTranslations } from "../src/services/i18n.js";
|
|
||||||
import cls from "../src/services/cls.js";
|
import cls from "../src/services/cls.js";
|
||||||
|
import cloningService from "../src/services/cloning.js";
|
||||||
|
import loremIpsum from "lorem-ipsum";
|
||||||
|
import "../src/becca/entity_constructor.js";
|
||||||
|
|
||||||
const noteCount = parseInt(process.argv[2]);
|
const noteCount = parseInt(process.argv[2]);
|
||||||
|
|
||||||
@@ -28,18 +27,8 @@ function getRandomNoteId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
if (!sqlInit.isDbInitialized()) {
|
|
||||||
console.error("Database not initialized.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await initializeTranslations();
|
|
||||||
|
|
||||||
sqlInit.initializeDb();
|
|
||||||
await sqlInit.dbReady;
|
|
||||||
|
|
||||||
for (let i = 0; i < noteCount; i++) {
|
for (let i = 0; i < noteCount; i++) {
|
||||||
const title = loremIpsum({
|
const title = loremIpsum.loremIpsum({
|
||||||
count: 1,
|
count: 1,
|
||||||
units: "sentences",
|
units: "sentences",
|
||||||
sentenceLowerBound: 1,
|
sentenceLowerBound: 1,
|
||||||
@@ -47,7 +36,7 @@ async function start() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const paragraphCount = Math.floor(Math.random() * Math.random() * 100);
|
const paragraphCount = Math.floor(Math.random() * Math.random() * 100);
|
||||||
const content = loremIpsum({
|
const content = loremIpsum.loremIpsum({
|
||||||
count: paragraphCount,
|
count: paragraphCount,
|
||||||
units: "paragraphs",
|
units: "paragraphs",
|
||||||
sentenceLowerBound: 1,
|
sentenceLowerBound: 1,
|
||||||
@@ -71,13 +60,13 @@ async function start() {
|
|||||||
const parentNoteId = getRandomNoteId();
|
const parentNoteId = getRandomNoteId();
|
||||||
const prefix = Math.random() > 0.8 ? "prefix" : "";
|
const prefix = Math.random() > 0.8 ? "prefix" : "";
|
||||||
|
|
||||||
const result = cloningService.cloneNoteToBranch(noteIdToClone, parentNoteId, prefix);
|
const result = await cloningService.cloneNoteToBranch(noteIdToClone, parentNoteId, prefix);
|
||||||
|
|
||||||
console.log(`Cloning ${i}:`, result.success ? "succeeded" : "FAILED");
|
console.log(`Cloning ${i}:`, result.success ? "succeeded" : "FAILED");
|
||||||
}
|
}
|
||||||
|
|
||||||
// does not have to be for the current note
|
// does not have to be for the current note
|
||||||
attributeService.createAttribute({
|
await attributeService.createAttribute({
|
||||||
noteId: getRandomNoteId(),
|
noteId: getRandomNoteId(),
|
||||||
type: "label",
|
type: "label",
|
||||||
name: "label",
|
name: "label",
|
||||||
@@ -85,7 +74,7 @@ async function start() {
|
|||||||
isInheritable: Math.random() > 0.1 // 10% are inheritable
|
isInheritable: Math.random() > 0.1 // 10% are inheritable
|
||||||
});
|
});
|
||||||
|
|
||||||
attributeService.createAttribute({
|
await attributeService.createAttribute({
|
||||||
noteId: getRandomNoteId(),
|
noteId: getRandomNoteId(),
|
||||||
type: "relation",
|
type: "relation",
|
||||||
name: "relation",
|
name: "relation",
|
||||||
@@ -101,4 +90,6 @@ async function start() {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.init(() => start());
|
// @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs
|
||||||
|
// see https://github.com/TriliumNext/Trilium/issues/1020
|
||||||
|
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err));
|
||||||
16
_regroup/bin/push-docker-image.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]] ; then
|
||||||
|
echo "Missing argument of new version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
SERIES=${VERSION:0:4}-latest
|
||||||
|
|
||||||
|
docker push zadam/trilium:$VERSION
|
||||||
|
docker push zadam/trilium:$SERIES
|
||||||
|
|
||||||
|
if [[ $1 != *"beta"* ]]; then
|
||||||
|
docker push zadam/trilium:latest
|
||||||
|
fi
|
||||||
57
_regroup/bin/release-flatpack.sh
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]] ; then
|
||||||
|
echo "Missing argument of new version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
|
||||||
|
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
||||||
|
then
|
||||||
|
echo "Version ${VERSION} isn't in format X.Y.Z"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_DATE=$(git log -1 --format=%aI "v${VERSION}" | cut -c -10)
|
||||||
|
VERSION_COMMIT=$(git rev-list -n 1 "v${VERSION}")
|
||||||
|
|
||||||
|
# expecting the directory at a specific path
|
||||||
|
cd ~/trilium-flathub || exit
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
echo "There are uncommitted changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE_BRANCH=main
|
||||||
|
|
||||||
|
if [[ "$VERSION" == *"beta"* ]]; then
|
||||||
|
BASE_BRANCH=beta
|
||||||
|
fi
|
||||||
|
|
||||||
|
git switch "${BASE_BRANCH}"
|
||||||
|
git pull
|
||||||
|
|
||||||
|
BRANCH=b${VERSION}
|
||||||
|
|
||||||
|
git branch "${BRANCH}"
|
||||||
|
git switch "${BRANCH}"
|
||||||
|
|
||||||
|
echo "Updating files with version ${VERSION}, date ${VERSION_DATE} and commit ${VERSION_COMMIT}"
|
||||||
|
|
||||||
|
flatpak-node-generator npm ../trilium/package-lock.json
|
||||||
|
|
||||||
|
xmlstarlet ed --inplace --update "/component/releases/release/@version" --value "${VERSION}" --update "/component/releases/release/@date" --value "${VERSION_DATE}" ./com.github.zadam.trilium.metainfo.xml
|
||||||
|
|
||||||
|
yq --inplace "(.modules[0].sources[0].tag = \"v${VERSION}\") | (.modules[0].sources[0].commit = \"${VERSION_COMMIT}\")" ./com.github.zadam.trilium.yml
|
||||||
|
|
||||||
|
git add ./generated-sources.json
|
||||||
|
git add ./com.github.zadam.trilium.metainfo.xml
|
||||||
|
git add ./com.github.zadam.trilium.yml
|
||||||
|
|
||||||
|
git commit -m "release $VERSION"
|
||||||
|
git push --set-upstream origin "${BRANCH}"
|
||||||
|
|
||||||
|
gh pr create --fill -B "${BASE_BRANCH}"
|
||||||
|
gh pr merge --auto --merge --delete-branch
|
||||||
49
_regroup/bin/release.sh
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]] ; then
|
||||||
|
echo "Missing argument of new version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo "Missing command: jq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
|
||||||
|
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
||||||
|
then
|
||||||
|
echo "Version ${VERSION} isn't in format X.Y.Z"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
echo "There are uncommitted changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Releasing Trilium $VERSION"
|
||||||
|
|
||||||
|
jq '.version = "'$VERSION'"' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
|
||||||
|
git add package.json
|
||||||
|
|
||||||
|
npm run chore:update-build-info
|
||||||
|
|
||||||
|
git add src/services/build.ts
|
||||||
|
|
||||||
|
TAG=v$VERSION
|
||||||
|
|
||||||
|
echo "Committing package.json version change"
|
||||||
|
|
||||||
|
git commit -m "chore(release): $VERSION"
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "Tagging commit with $TAG"
|
||||||
|
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 356 B |
|
Before Width: | Height: | Size: 357 B After Width: | Height: | Size: 357 B |
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 734 B After Width: | Height: | Size: 734 B |
10
_regroup/entitlements.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
51
_regroup/eslint.config.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import eslint from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
// consider using rules below, once we have a full TS codebase and can be more strict
|
||||||
|
// tseslint.configs.strictTypeChecked,
|
||||||
|
// tseslint.configs.stylisticTypeChecked,
|
||||||
|
// tseslint.configs.recommendedTypeChecked,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
"simple-import-sort": simpleImportSort
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// add rule overrides here
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
"simple-import-sort/exports": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"build/*",
|
||||||
|
"dist/*",
|
||||||
|
"docs/*",
|
||||||
|
"demo/*",
|
||||||
|
"src/public/app-dist/*",
|
||||||
|
"src/public/app/doc_notes/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
47
_regroup/eslint.format.config.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
|
||||||
|
// eslint config just for formatting rules
|
||||||
|
// potentially to be merged with the linting rules into one single config,
|
||||||
|
// once we have fixed the majority of lint errors
|
||||||
|
|
||||||
|
// Go to https://eslint.style/rules/default/${rule_without_prefix} to check the rule details
|
||||||
|
export const stylisticRules = {
|
||||||
|
"@stylistic/indent": [ "error", 4 ],
|
||||||
|
"@stylistic/quotes": [ "error", "double", { avoidEscape: true, allowTemplateLiterals: "always" } ],
|
||||||
|
"@stylistic/semi": [ "error", "always" ],
|
||||||
|
"@stylistic/quote-props": [ "error", "consistent-as-needed" ],
|
||||||
|
"@stylistic/max-len": [ "error", { code: 100 } ],
|
||||||
|
"@stylistic/comma-dangle": [ "error", "never" ],
|
||||||
|
"@stylistic/linebreak-style": [ "error", "unix" ],
|
||||||
|
"@stylistic/array-bracket-spacing": [ "error", "always" ],
|
||||||
|
"@stylistic/object-curly-spacing": [ "error", "always" ],
|
||||||
|
"@stylistic/padded-blocks": [ "error", { classes: "always" } ]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: [ "**/*.{js,ts,mjs,cjs}" ],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"@stylistic": stylistic
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...stylisticRules
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"build/*",
|
||||||
|
"dist/*",
|
||||||
|
"docs/*",
|
||||||
|
"demo/*",
|
||||||
|
// TriliumNextTODO: check if we want to format packages here as well - for now skipping it
|
||||||
|
"packages/*",
|
||||||
|
"src/public/app-dist/*",
|
||||||
|
"src/public/app/doc_notes/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
17
_regroup/integration-tests/auth.setup.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { test as setup, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
const authFile = "playwright/.auth/user.json";
|
||||||
|
|
||||||
|
const ROOT_URL = "http://localhost:8082";
|
||||||
|
const LOGIN_PASSWORD = "demo1234";
|
||||||
|
|
||||||
|
// Reference: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
|
||||||
|
|
||||||
|
setup("authenticate", async ({ page }) => {
|
||||||
|
await page.goto(ROOT_URL);
|
||||||
|
await expect(page).toHaveURL(`${ROOT_URL}/login`);
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
|
||||||
|
await page.getByRole("button", { name: "Login" }).click();
|
||||||
|
await page.context().storageState({ path: authFile });
|
||||||
|
});
|
||||||
9
_regroup/integration-tests/duplicate.spec.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("Can duplicate note with broken links", async ({ page }) => {
|
||||||
|
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
|
||||||
|
await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" });
|
||||||
|
await page.getByText("Duplicate subtree").click();
|
||||||
|
await expect(page.locator(".toast-body")).toBeHidden();
|
||||||
|
await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible();
|
||||||
|
});
|
||||||
18
_regroup/integration-tests/example.disabled.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("has title", async ({ page }) => {
|
||||||
|
await page.goto("https://playwright.dev/");
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Playwright/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get started link", async ({ page }) => {
|
||||||
|
await page.goto("https://playwright.dev/");
|
||||||
|
|
||||||
|
// Click the get started link.
|
||||||
|
await page.getByRole("link", { name: "Get started" }).click();
|
||||||
|
|
||||||
|
// Expects page to have a heading with the name of Installation.
|
||||||
|
await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
|
||||||
|
});
|
||||||
21
_regroup/integration-tests/settings.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import test, { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("Native Title Bar not displayed on web", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance");
|
||||||
|
await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Tray settings not displayed on web", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther");
|
||||||
|
await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Spellcheck settings not displayed on web", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck");
|
||||||
|
await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||||
|
await expect(page.getByText("These options apply only for desktop builds")).toBeVisible();
|
||||||
|
await expect(page.getByText("Enable spellcheck")).toBeHidden();
|
||||||
|
});
|
||||||
18
_regroup/integration-tests/tree.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import test, { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("Renders on desktop", async ({ page, context }) => {
|
||||||
|
await page.goto("http://localhost:8082");
|
||||||
|
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Renders on mobile", async ({ page, context }) => {
|
||||||
|
await context.addCookies([
|
||||||
|
{
|
||||||
|
url: "http://localhost:8082",
|
||||||
|
name: "trilium-device",
|
||||||
|
value: "mobile"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
await page.goto("http://localhost:8082");
|
||||||
|
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||||
|
});
|
||||||
12
_regroup/integration-tests/update_check.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
const expectedVersion = "0.90.3";
|
||||||
|
|
||||||
|
test("Displays update badge when there is a version available", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:8080");
|
||||||
|
await page.getByRole("button", { name: "" }).click();
|
||||||
|
await page.getByText(`Version ${expectedVersion} is available,`).click();
|
||||||
|
|
||||||
|
const page1 = await page.waitForEvent("popup");
|
||||||
|
expect(page1.url()).toBe(`https://github.com/TriliumNext/Trilium/releases/tag/v${expectedVersion}`);
|
||||||
|
});
|
||||||
56
_regroup/package.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"main": "./electron-main.js",
|
||||||
|
"bin": {
|
||||||
|
"trilium": "src/main.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"server:start-safe": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nodemon src/main.ts",
|
||||||
|
"server:start-no-dir": "cross-env TRILIUM_ENV=dev nodemon src/main.ts",
|
||||||
|
"server:start-test": "npm run server:switch && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
|
||||||
|
"server:qstart": "npm run server:switch && npm run server:start",
|
||||||
|
"server:switch": "rimraf ./node_modules/better-sqlite3 && npm install",
|
||||||
|
"electron:start-no-dir": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 electron --inspect=5858 .",
|
||||||
|
"electron:start-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
||||||
|
"electron:start-nix-no-dir": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
||||||
|
"electron:start-prod-no-dir": "npm run build:prepare-dist && cross-env TRILIUM_ENV=prod electron --inspect=5858 .",
|
||||||
|
"electron:start-prod-nix": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||||
|
"electron:start-prod-nix-no-dir": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||||
|
"electron:qstart": "npm run electron:switch && npm run electron:start",
|
||||||
|
"electron:switch": "electron-rebuild",
|
||||||
|
"docs:build": "typedoc",
|
||||||
|
"test": "npm run client:test && npm run server:test",
|
||||||
|
"client:test": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app",
|
||||||
|
"client:coverage": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app --coverage",
|
||||||
|
"test:playwright": "playwright test --workers 1",
|
||||||
|
"test:integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||||
|
"test:integration-mem-db": "cross-env nodemon src/main.ts",
|
||||||
|
"test:integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||||
|
"dev:watch-dist": "tsx ./bin/watch-dist.ts",
|
||||||
|
"dev:format-check": "eslint -c eslint.format.config.js .",
|
||||||
|
"dev:format-fix": "eslint -c eslint.format.config.js . --fix",
|
||||||
|
"dev:linter-check": "eslint .",
|
||||||
|
"dev:linter-fix": "eslint . --fix",
|
||||||
|
"chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
|
||||||
|
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.56.1",
|
||||||
|
"@stylistic/eslint-plugin": "5.5.0",
|
||||||
|
"@types/express": "5.0.5",
|
||||||
|
"@types/node": "24.10.1",
|
||||||
|
"@types/yargs": "17.0.34",
|
||||||
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
|
"eslint": "9.39.1",
|
||||||
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
|
"esm": "3.2.25",
|
||||||
|
"jsdoc": "4.0.5",
|
||||||
|
"lorem-ipsum": "2.0.8",
|
||||||
|
"rcedit": "5.0.0",
|
||||||
|
"rimraf": "6.1.0",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"appdmg": "0.6.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
_regroup/spec/etapi/app_info.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import etapi from "../support/etapi.js";
|
||||||
|
/* TriliumNextTODO: port to Vitest
|
||||||
|
etapi.describeEtapi("app_info", () => {
|
||||||
|
it("get", async () => {
|
||||||
|
const appInfo = await etapi.getEtapi("app-info");
|
||||||
|
expect(appInfo.clipperProtocolVersion).toEqual("1.0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
10
_regroup/spec/etapi/backup.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import etapi from "../support/etapi.js";
|
||||||
|
|
||||||
|
/* TriliumNextTODO: port to Vitest
|
||||||
|
etapi.describeEtapi("backup", () => {
|
||||||
|
it("create", async () => {
|
||||||
|
const response = await etapi.putEtapiContent("backup/etapi_test");
|
||||||
|
expect(response.status).toEqual(204);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
26
_regroup/spec/etapi/import.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import etapi from "../support/etapi.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
/* TriliumNextTODO: port to Vitest
|
||||||
|
etapi.describeEtapi("import", () => {
|
||||||
|
// temporarily skip this test since test-export.zip is missing
|
||||||
|
xit("import", async () => {
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const zipFileBuffer = fs.readFileSync(path.resolve(scriptDir, "test-export.zip"));
|
||||||
|
|
||||||
|
const response = await etapi.postEtapiContent("notes/root/import", zipFileBuffer);
|
||||||
|
expect(response.status).toEqual(201);
|
||||||
|
|
||||||
|
const { note, branch } = await response.json();
|
||||||
|
|
||||||
|
expect(note.title).toEqual("test-export");
|
||||||
|
expect(branch.parentNoteId).toEqual("root");
|
||||||
|
|
||||||
|
const content = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
||||||
|
expect(content).toContain("test export content");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
103
_regroup/spec/etapi/notes.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import etapi from "../support/etapi.js";
|
||||||
|
|
||||||
|
/* TriliumNextTODO: port to Vitest
|
||||||
|
etapi.describeEtapi("notes", () => {
|
||||||
|
it("create", async () => {
|
||||||
|
const { note, branch } = await etapi.postEtapi("create-note", {
|
||||||
|
parentNoteId: "root",
|
||||||
|
type: "text",
|
||||||
|
title: "Hello World!",
|
||||||
|
content: "Content",
|
||||||
|
prefix: "Custom prefix"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(note.title).toEqual("Hello World!");
|
||||||
|
expect(branch.parentNoteId).toEqual("root");
|
||||||
|
expect(branch.prefix).toEqual("Custom prefix");
|
||||||
|
|
||||||
|
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
||||||
|
expect(rNote.title).toEqual("Hello World!");
|
||||||
|
|
||||||
|
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
||||||
|
expect(rContent).toEqual("Content");
|
||||||
|
|
||||||
|
const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`);
|
||||||
|
expect(rBranch.parentNoteId).toEqual("root");
|
||||||
|
expect(rBranch.prefix).toEqual("Custom prefix");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("patch", async () => {
|
||||||
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
|
parentNoteId: "root",
|
||||||
|
type: "text",
|
||||||
|
title: "Hello World!",
|
||||||
|
content: "Content"
|
||||||
|
});
|
||||||
|
|
||||||
|
await etapi.patchEtapi(`notes/${note.noteId}`, {
|
||||||
|
title: "new title",
|
||||||
|
type: "code",
|
||||||
|
mime: "text/apl",
|
||||||
|
dateCreated: "2000-01-01 12:34:56.999+0200",
|
||||||
|
utcDateCreated: "2000-01-01 10:34:56.999Z"
|
||||||
|
});
|
||||||
|
|
||||||
|
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
||||||
|
expect(rNote.title).toEqual("new title");
|
||||||
|
expect(rNote.type).toEqual("code");
|
||||||
|
expect(rNote.mime).toEqual("text/apl");
|
||||||
|
expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200");
|
||||||
|
expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("update content", async () => {
|
||||||
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
|
parentNoteId: "root",
|
||||||
|
type: "text",
|
||||||
|
title: "Hello World!",
|
||||||
|
content: "Content"
|
||||||
|
});
|
||||||
|
|
||||||
|
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
|
||||||
|
|
||||||
|
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
||||||
|
expect(rContent).toEqual("new content");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("create / update binary content", async () => {
|
||||||
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
|
parentNoteId: "root",
|
||||||
|
type: "file",
|
||||||
|
title: "Hello World!",
|
||||||
|
content: "ZZZ"
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedContent = crypto.randomBytes(16);
|
||||||
|
|
||||||
|
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
|
||||||
|
|
||||||
|
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer();
|
||||||
|
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delete note", async () => {
|
||||||
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
|
parentNoteId: "root",
|
||||||
|
type: "text",
|
||||||
|
title: "Hello World!",
|
||||||
|
content: "Content"
|
||||||
|
});
|
||||||
|
|
||||||
|
await etapi.deleteEtapi(`notes/${note.noteId}`);
|
||||||
|
|
||||||
|
const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`);
|
||||||
|
expect(resp.status).toEqual(404);
|
||||||
|
|
||||||
|
const error = await resp.json();
|
||||||
|
expect(error.status).toEqual(404);
|
||||||
|
expect(error.code).toEqual("NOTE_NOT_FOUND");
|
||||||
|
expect(error.message).toEqual(`Note '${note.noteId}' not found.`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
152
_regroup/spec/support/etapi.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { describe, beforeAll, afterAll } from "vitest";
|
||||||
|
|
||||||
|
let etapiAuthToken: string | undefined;
|
||||||
|
|
||||||
|
const getEtapiAuthorizationHeader = (): string => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64");
|
||||||
|
|
||||||
|
const PORT: string = "9999";
|
||||||
|
const HOST: string = "http://localhost:" + PORT;
|
||||||
|
|
||||||
|
type SpecDefinitionsFunc = () => void;
|
||||||
|
|
||||||
|
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
|
||||||
|
describe(description, () => {
|
||||||
|
beforeAll(async () => {});
|
||||||
|
|
||||||
|
afterAll(() => {});
|
||||||
|
|
||||||
|
specDefinitions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEtapiResponse(url: string): Promise<Response> {
|
||||||
|
return await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEtapi(url: string): Promise<any> {
|
||||||
|
const response = await getEtapiResponse(url);
|
||||||
|
return await processEtapiResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEtapiContent(url: string): Promise<Response> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkStatus(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
return await processEtapiResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postEtapiContent(url: string, data: BodyInit): Promise<Response> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
},
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
|
||||||
|
checkStatus(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
return await processEtapiResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putEtapiContent(url: string, data?: BodyInit): Promise<Response> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
},
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
|
||||||
|
checkStatus(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function patchEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
return await processEtapiResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEtapi(url: string): Promise<any> {
|
||||||
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: getEtapiAuthorizationHeader()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await processEtapiResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processEtapiResponse(response: Response): Promise<any> {
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
throw new Error(`ETAPI error ${response.status}: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text?.trim() ? JSON.parse(text) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStatus(response: Response): void {
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
throw new Error(`ETAPI error ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
describeEtapi,
|
||||||
|
getEtapi,
|
||||||
|
getEtapiResponse,
|
||||||
|
getEtapiContent,
|
||||||
|
postEtapi,
|
||||||
|
postEtapiContent,
|
||||||
|
putEtapi,
|
||||||
|
putEtapiContent,
|
||||||
|
patchEtapi,
|
||||||
|
deleteEtapi
|
||||||
|
};
|
||||||
22
_regroup/tsconfig.webpack.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"declaration": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./build",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": ["./src/public/app/**/*"],
|
||||||
|
"files": [
|
||||||
|
"./src/public/app/types.d.ts",
|
||||||
|
"./src/public/app/types-lib.d.ts",
|
||||||
|
"./src/types.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,28 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "build-docs",
|
"name": "build-docs",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Build documentation from Trilium notes",
|
"description": "",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"bin": {
|
|
||||||
"trilium-build-docs": "dist/cli.js"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx .",
|
"start": "tsx ."
|
||||||
"cli": "tsx src/cli.ts",
|
|
||||||
"build": "tsx scripts/build.ts"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Elian Doran <contact@eliandoran.me>",
|
"author": "Elian Doran <contact@eliandoran.me>",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"packageManager": "pnpm@10.32.0",
|
"packageManager": "pnpm@10.22.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/cli": "2.20.2",
|
"@redocly/cli": "2.11.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"fs-extra": "11.3.4",
|
"fs-extra": "11.3.2",
|
||||||
"js-yaml": "4.1.1",
|
"react": "19.2.0",
|
||||||
"react": "19.2.4",
|
"react-dom": "19.2.0",
|
||||||
"react-dom": "19.2.4",
|
"typedoc": "0.28.14",
|
||||||
"typedoc": "0.28.17",
|
|
||||||
"typedoc-plugin-missing-exports": "4.1.2"
|
"typedoc-plugin-missing-exports": "4.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import BuildHelper from "../../../scripts/build-utils";
|
|
||||||
|
|
||||||
const build = new BuildHelper("apps/build-docs");
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// Build the CLI and other TypeScript files
|
|
||||||
await build.buildBackend([
|
|
||||||
"src/cli.ts",
|
|
||||||
"src/main.ts",
|
|
||||||
"src/build-docs.ts",
|
|
||||||
"src/swagger.ts",
|
|
||||||
"src/script-api.ts",
|
|
||||||
"src/context.ts"
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Copy HTML template
|
|
||||||
build.copy("src/index.html", "index.html");
|
|
||||||
|
|
||||||
// Copy node modules dependencies if needed
|
|
||||||
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -13,12 +13,8 @@
|
|||||||
* Make sure to keep in line with backend's `script_context.ts`.
|
* Make sure to keep in line with backend's `script_context.ts`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type {
|
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
|
||||||
default as AbstractBeccaEntity
|
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
|
||||||
} from "../../server/src/becca/entities/abstract_becca_entity.js";
|
|
||||||
export type {
|
|
||||||
default as BAttachment
|
|
||||||
} from "../../server/src/becca/entities/battachment.js";
|
|
||||||
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
|
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
|
||||||
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
|
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
|
||||||
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
|
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
|
||||||
@@ -35,7 +31,6 @@ export type { Api };
|
|||||||
const fakeNote = new BNote();
|
const fakeNote = new BNote();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `api` global variable allows access to the backend script API,
|
* The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
|
||||||
* which is documented in {@link Api}.
|
|
||||||
*/
|
*/
|
||||||
export const api: Api = new BackendScriptApi(fakeNote, {});
|
export const api: Api = new BackendScriptApi(fakeNote, {});
|
||||||
|
|||||||
@@ -1,90 +1,19 @@
|
|||||||
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
|
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
|
||||||
// Only set TRILIUM_RESOURCE_DIR if not already set (e.g., by Nix wrapper)
|
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
|
||||||
if (!process.env.TRILIUM_RESOURCE_DIR) {
|
|
||||||
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
|
|
||||||
}
|
|
||||||
process.env.NODE_ENV = "development";
|
process.env.NODE_ENV = "development";
|
||||||
|
|
||||||
import cls from "@triliumnext/server/src/services/cls.js";
|
import cls from "@triliumnext/server/src/services/cls.js";
|
||||||
import archiver from "archiver";
|
import { dirname, join, resolve } from "path";
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { WriteStream } from "fs";
|
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as fsExtra from "fs-extra";
|
import * as fsExtra from "fs-extra";
|
||||||
import yaml from "js-yaml";
|
import archiver from "archiver";
|
||||||
import { dirname, join, resolve } from "path";
|
import { WriteStream } from "fs";
|
||||||
|
import { execSync } from "child_process";
|
||||||
import BuildContext from "./context.js";
|
import BuildContext from "./context.js";
|
||||||
|
|
||||||
interface NoteMapping {
|
|
||||||
rootNoteId: string;
|
|
||||||
path: string;
|
|
||||||
format: "markdown" | "html" | "share";
|
|
||||||
ignoredFiles?: string[];
|
|
||||||
exportOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Config {
|
|
||||||
baseUrl: string;
|
|
||||||
noteMappings: NoteMapping[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const DOCS_ROOT = "../../../docs";
|
const DOCS_ROOT = "../../../docs";
|
||||||
const OUTPUT_DIR = "../../site";
|
const OUTPUT_DIR = "../../site";
|
||||||
|
|
||||||
// Load configuration from edit-docs-config.yaml
|
|
||||||
async function loadConfig(configPath?: string): Promise<Config | null> {
|
|
||||||
const pathsToTry = configPath
|
|
||||||
? [resolve(configPath)]
|
|
||||||
: [
|
|
||||||
join(process.cwd(), "edit-docs-config.yaml"),
|
|
||||||
join(__dirname, "../../../edit-docs-config.yaml")
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of pathsToTry) {
|
|
||||||
try {
|
|
||||||
const configContent = await fs.readFile(path, "utf-8");
|
|
||||||
const config = yaml.load(configContent) as Config;
|
|
||||||
|
|
||||||
// Resolve all paths relative to the config file's directory
|
|
||||||
const CONFIG_DIR = dirname(path);
|
|
||||||
config.noteMappings = config.noteMappings.map((mapping) => ({
|
|
||||||
...mapping,
|
|
||||||
path: resolve(CONFIG_DIR, mapping.path)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return config;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
throw error; // rethrow unexpected errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // No config file found
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportDocs(
|
|
||||||
noteId: string,
|
|
||||||
format: "markdown" | "html" | "share",
|
|
||||||
outputPath: string,
|
|
||||||
ignoredFiles?: string[]
|
|
||||||
) {
|
|
||||||
const zipFilePath = `output-${noteId}.zip`;
|
|
||||||
try {
|
|
||||||
const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js"))
|
|
||||||
.default;
|
|
||||||
await exportToZipFile(noteId, format, zipFilePath, {});
|
|
||||||
|
|
||||||
const ignoredSet = ignoredFiles ? new Set(ignoredFiles) : undefined;
|
|
||||||
await extractZip(zipFilePath, outputPath, ignoredSet);
|
|
||||||
} finally {
|
|
||||||
if (await fsExtra.exists(zipFilePath)) {
|
|
||||||
await fsExtra.rm(zipFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
||||||
const note = await importData(sourcePath);
|
const note = await importData(sourcePath);
|
||||||
|
|
||||||
@@ -92,18 +21,15 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
|||||||
const zipName = outputSubDir || "user-guide";
|
const zipName = outputSubDir || "user-guide";
|
||||||
const zipFilePath = `output-${zipName}.zip`;
|
const zipFilePath = `output-${zipName}.zip`;
|
||||||
try {
|
try {
|
||||||
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js"))
|
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
|
||||||
.default;
|
|
||||||
const branch = note.getParentBranches()[0];
|
const branch = note.getParentBranches()[0];
|
||||||
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js"))
|
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
|
||||||
.default(
|
"no-progress-reporting",
|
||||||
"no-progress-reporting",
|
"export",
|
||||||
"export",
|
null
|
||||||
null
|
);
|
||||||
);
|
|
||||||
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
|
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
|
||||||
await exportToZip(taskContext, branch, "share", fileOutputStream);
|
await exportToZip(taskContext, branch, "share", fileOutputStream);
|
||||||
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
|
|
||||||
await waitForStreamToFinish(fileOutputStream);
|
await waitForStreamToFinish(fileOutputStream);
|
||||||
|
|
||||||
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
|
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
|
||||||
@@ -116,7 +42,7 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildDocsInner(config?: Config) {
|
async function buildDocsInner() {
|
||||||
const i18n = await import("@triliumnext/server/src/services/i18n.js");
|
const i18n = await import("@triliumnext/server/src/services/i18n.js");
|
||||||
await i18n.initializeTranslations();
|
await i18n.initializeTranslations();
|
||||||
|
|
||||||
@@ -127,49 +53,18 @@ async function buildDocsInner(config?: Config) {
|
|||||||
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
|
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
|
||||||
await beccaLoader.beccaLoaded;
|
await beccaLoader.beccaLoaded;
|
||||||
|
|
||||||
if (config) {
|
// Build User Guide
|
||||||
// Config-based build (reads from edit-docs-config.yaml)
|
console.log("Building User Guide...");
|
||||||
console.log("Building documentation from config file...");
|
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
|
||||||
|
|
||||||
// Import all non-export-only mappings
|
// Build Developer Guide
|
||||||
for (const mapping of config.noteMappings) {
|
console.log("Building Developer Guide...");
|
||||||
if (!mapping.exportOnly) {
|
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
|
||||||
console.log(`Importing from ${mapping.path}...`);
|
|
||||||
await importData(mapping.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export all mappings
|
// Copy favicon.
|
||||||
for (const mapping of config.noteMappings) {
|
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
|
||||||
if (mapping.exportOnly) {
|
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
|
||||||
console.log(`Exporting ${mapping.format} to ${mapping.path}...`);
|
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
|
||||||
await exportDocs(
|
|
||||||
mapping.rootNoteId,
|
|
||||||
mapping.format,
|
|
||||||
mapping.path,
|
|
||||||
mapping.ignoredFiles
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Legacy hardcoded build (for backward compatibility)
|
|
||||||
console.log("Building User Guide...");
|
|
||||||
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
|
|
||||||
|
|
||||||
console.log("Building Developer Guide...");
|
|
||||||
await importAndExportDocs(
|
|
||||||
join(__dirname, DOCS_ROOT, "Developer Guide"),
|
|
||||||
"developer-guide"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy favicon.
|
|
||||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
|
|
||||||
join(OUTPUT_DIR, "favicon.ico"));
|
|
||||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
|
|
||||||
join(OUTPUT_DIR, "user-guide", "favicon.ico"));
|
|
||||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
|
|
||||||
join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Documentation built successfully!");
|
console.log("Documentation built successfully!");
|
||||||
}
|
}
|
||||||
@@ -196,13 +91,12 @@ async function createImportZip(path: string) {
|
|||||||
zlib: { level: 0 }
|
zlib: { level: 0 }
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Archive path is ", resolve(path));
|
console.log("Archive path is ", resolve(path))
|
||||||
archive.directory(path, "/");
|
archive.directory(path, "/");
|
||||||
|
|
||||||
const outputStream = fsExtra.createWriteStream(inputFile);
|
const outputStream = fsExtra.createWriteStream(inputFile);
|
||||||
archive.pipe(outputStream);
|
archive.pipe(outputStream);
|
||||||
archive.finalize();
|
archive.finalize();
|
||||||
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
|
|
||||||
await waitForStreamToFinish(outputStream);
|
await waitForStreamToFinish(outputStream);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -212,15 +106,15 @@ async function createImportZip(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForStreamToFinish(stream: WriteStream) {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
stream.on("finish", () => res());
|
||||||
|
stream.on("error", (err) => rej(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function extractZip(
|
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
|
||||||
zipFilePath: string,
|
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
|
||||||
outputPath: string,
|
|
||||||
ignoredFiles?: Set<string>
|
|
||||||
) {
|
|
||||||
const { readZipFile, readContent } = (await import(
|
|
||||||
"@triliumnext/server/src/services/import/zip.js"
|
|
||||||
));
|
|
||||||
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
|
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
|
||||||
// We ignore directories since they can appear out of order anyway.
|
// We ignore directories since they can appear out of order anyway.
|
||||||
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
|
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
|
||||||
@@ -235,27 +129,6 @@ export async function extractZip(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildDocsFromConfig(configPath?: string, gitRootDir?: string) {
|
|
||||||
const config = await loadConfig(configPath);
|
|
||||||
|
|
||||||
if (gitRootDir) {
|
|
||||||
// Build the share theme if we have a gitRootDir (for Trilium project)
|
|
||||||
execSync(`pnpm run --filter share-theme build`, {
|
|
||||||
stdio: "inherit",
|
|
||||||
cwd: gitRootDir
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger the actual build.
|
|
||||||
await new Promise((res, rej) => {
|
|
||||||
cls.init(() => {
|
|
||||||
buildDocsInner(config ?? undefined)
|
|
||||||
.catch(rej)
|
|
||||||
.then(res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function buildDocs({ gitRootDir }: BuildContext) {
|
export default async function buildDocs({ gitRootDir }: BuildContext) {
|
||||||
// Build the share theme.
|
// Build the share theme.
|
||||||
execSync(`pnpm run --filter share-theme build`, {
|
execSync(`pnpm run --filter share-theme build`, {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import packageJson from "../package.json" with { type: "json" };
|
|
||||||
import { buildDocsFromConfig } from "./build-docs.js";
|
|
||||||
|
|
||||||
// Parse command-line arguments
|
|
||||||
function parseArgs() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
let configPath: string | undefined;
|
|
||||||
let showHelp = false;
|
|
||||||
let showVersion = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
if (args[i] === "--config" || args[i] === "-c") {
|
|
||||||
configPath = args[i + 1];
|
|
||||||
if (!configPath) {
|
|
||||||
console.error("Error: --config/-c requires a path argument");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
i++; // Skip the next argument as it's the value
|
|
||||||
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
||||||
showHelp = true;
|
|
||||||
} else if (args[i] === "--version" || args[i] === "-v") {
|
|
||||||
showVersion = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { configPath, showHelp, showVersion };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersion(): string {
|
|
||||||
return packageJson.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
function printHelp() {
|
|
||||||
const version = getVersion();
|
|
||||||
console.log(`
|
|
||||||
Usage: trilium-build-docs [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-c, --config <path> Path to the configuration file
|
|
||||||
(default: edit-docs-config.yaml in current directory)
|
|
||||||
-h, --help Display this help message
|
|
||||||
-v, --version Display version information
|
|
||||||
|
|
||||||
Description:
|
|
||||||
Builds documentation from Trilium note structure and exports to various formats.
|
|
||||||
Configuration file should be in YAML format with the following structure:
|
|
||||||
|
|
||||||
baseUrl: "https://example.com"
|
|
||||||
noteMappings:
|
|
||||||
- rootNoteId: "noteId123"
|
|
||||||
path: "docs"
|
|
||||||
format: "markdown"
|
|
||||||
- rootNoteId: "noteId456"
|
|
||||||
path: "public/docs"
|
|
||||||
format: "share"
|
|
||||||
exportOnly: true
|
|
||||||
|
|
||||||
Version: ${version}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function printVersion() {
|
|
||||||
const version = getVersion();
|
|
||||||
console.log(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const { configPath, showHelp, showVersion } = parseArgs();
|
|
||||||
|
|
||||||
if (showHelp) {
|
|
||||||
printHelp();
|
|
||||||
process.exit(0);
|
|
||||||
} else if (showVersion) {
|
|
||||||
printVersion();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await buildDocsFromConfig(configPath);
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error building documentation:", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -13,19 +13,16 @@
|
|||||||
* Make sure to keep in line with frontend's `script_context.ts`.
|
* Make sure to keep in line with frontend's `script_context.ts`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
|
||||||
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
|
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
|
||||||
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
|
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
|
||||||
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
|
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
|
||||||
export type { default as FNote } from "../../client/src/entities/fnote.js";
|
export type { default as FNote } from "../../client/src/entities/fnote.js";
|
||||||
export type { Api } from "../../client/src/services/frontend_script_api.js";
|
export type { Api } from "../../client/src/services/frontend_script_api.js";
|
||||||
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
|
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
|
||||||
export type {
|
|
||||||
default as NoteContextAwareWidget
|
|
||||||
} from "../../client/src/widgets/note_context_aware_widget.js";
|
|
||||||
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
|
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
|
||||||
|
|
||||||
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
|
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
// @ts-expect-error - FrontendScriptApi is not directly exportable as Api without this simulation.
|
|
||||||
export const api: Api = new FrontendScriptApi();
|
export const api: Api = new FrontendScriptApi();
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import buildDocs from "./build-docs";
|
|
||||||
import BuildContext from "./context";
|
import BuildContext from "./context";
|
||||||
import buildScriptApi from "./script-api";
|
|
||||||
import buildSwagger from "./swagger";
|
import buildSwagger from "./swagger";
|
||||||
|
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
||||||
|
import buildDocs from "./build-docs";
|
||||||
|
import buildScriptApi from "./script-api";
|
||||||
|
|
||||||
const context: BuildContext = {
|
const context: BuildContext = {
|
||||||
gitRootDir: join(__dirname, "../../../"),
|
gitRootDir: join(__dirname, "../../../"),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import BuildContext from "./context";
|
import BuildContext from "./context";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
|
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
|
||||||
// Generate types
|
// Generate types
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import BuildContext from "./context";
|
||||||
|
import { join } from "path";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { mkdirSync } from "fs";
|
import { mkdirSync } from "fs";
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import BuildContext from "./context";
|
|
||||||
|
|
||||||
interface BuildInfo {
|
interface BuildInfo {
|
||||||
specPath: string;
|
specPath: string;
|
||||||
@@ -28,9 +27,6 @@ export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
|
|||||||
const absSpecPath = join(gitRootDir, specPath);
|
const absSpecPath = join(gitRootDir, specPath);
|
||||||
const targetDir = join(baseDir, outDir);
|
const targetDir = join(baseDir, outDir);
|
||||||
mkdirSync(targetDir, { recursive: true });
|
mkdirSync(targetDir, { recursive: true });
|
||||||
execSync(
|
execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
|
||||||
`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`,
|
|
||||||
{ stdio: "inherit" }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"include": [
|
"include": [],
|
||||||
"scripts/**/*.ts"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../server"
|
"path": "../server"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"entryPoints": [
|
"entryPoints": [
|
||||||
"src/backend_script_entrypoint.ts"
|
"src/backend_script_entrypoint.ts"
|
||||||
],
|
],
|
||||||
"tsconfig": "tsconfig.app.json",
|
|
||||||
"plugin": [
|
"plugin": [
|
||||||
"typedoc-plugin-missing-exports"
|
"typedoc-plugin-missing-exports"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"entryPoints": [
|
"entryPoints": [
|
||||||
"src/frontend_script_entrypoint.ts"
|
"src/frontend_script_entrypoint.ts"
|
||||||
],
|
],
|
||||||
"tsconfig": "tsconfig.app.json",
|
|
||||||
"plugin": [
|
"plugin": [
|
||||||
"typedoc-plugin-missing-exports"
|
"typedoc-plugin-missing-exports"
|
||||||
]
|
]
|
||||||
|
|||||||
5
apps/client/eslint.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import baseConfig from "../../eslint.config.mjs";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig
|
||||||
|
];
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
||||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
|
||||||
<title>Trilium Notes</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body id="trilium-app">
|
|
||||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
|
||||||
|
|
||||||
<div id="context-menu-cover"></div>
|
|
||||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
|
||||||
|
|
||||||
<!-- Required to match the PWA's top bar color with the theme -->
|
|
||||||
<!-- This works even when the user directly changes --root-background in CSS -->
|
|
||||||
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
|
|
||||||
|
|
||||||
<script src="./src/index.ts" type="module"></script>
|
|
||||||
|
|
||||||
<!-- Required for correct loading of scripts in Electron -->
|
|
||||||
<script>
|
|
||||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/client",
|
"name": "@triliumnext/client",
|
||||||
"version": "0.102.1",
|
"version": "0.99.5",
|
||||||
"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",
|
||||||
@@ -12,85 +12,72 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest --coverage",
|
|
||||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@eslint/js": "9.39.1",
|
||||||
"@excalidraw/excalidraw": "0.18.0",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@fullcalendar/core": "6.1.20",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.20",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
"@fullcalendar/interaction": "6.1.20",
|
"@fullcalendar/interaction": "6.1.19",
|
||||||
"@fullcalendar/list": "6.1.20",
|
"@fullcalendar/list": "6.1.19",
|
||||||
"@fullcalendar/multimonth": "6.1.20",
|
"@fullcalendar/multimonth": "6.1.19",
|
||||||
"@fullcalendar/rrule": "6.1.20",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@fullcalendar/timegrid": "6.1.20",
|
|
||||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||||
"@mermaid-js/layout-elk": "0.2.1",
|
"@mermaid-js/layout-elk": "0.2.0",
|
||||||
"@mind-elixir/node-menu": "5.0.1",
|
"@mind-elixir/node-menu": "5.0.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@preact/signals": "2.8.2",
|
|
||||||
"@triliumnext/ckeditor5": "workspace:*",
|
"@triliumnext/ckeditor5": "workspace:*",
|
||||||
"@triliumnext/codemirror": "workspace:*",
|
"@triliumnext/codemirror": "workspace:*",
|
||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/highlightjs": "workspace:*",
|
"@triliumnext/highlightjs": "workspace:*",
|
||||||
"@triliumnext/share-theme": "workspace:*",
|
"@triliumnext/share-theme": "workspace:*",
|
||||||
"@triliumnext/split.js": "workspace:*",
|
"@triliumnext/split.js": "workspace:*",
|
||||||
"@univerjs/preset-sheets-conditional-formatting": "0.16.1",
|
|
||||||
"@univerjs/preset-sheets-core": "0.16.1",
|
|
||||||
"@univerjs/preset-sheets-data-validation": "0.16.1",
|
|
||||||
"@univerjs/preset-sheets-filter": "0.16.1",
|
|
||||||
"@univerjs/preset-sheets-find-replace": "0.16.1",
|
|
||||||
"@univerjs/preset-sheets-note": "0.16.1",
|
|
||||||
"@univerjs/preset-sheets-sort": "0.16.1",
|
|
||||||
"@univerjs/presets": "0.16.1",
|
|
||||||
"@zumer/snapdom": "2.1.0",
|
|
||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"clsx": "2.1.1",
|
"color": "5.0.2",
|
||||||
"color": "5.0.3",
|
"dayjs": "1.11.19",
|
||||||
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
"debounce": "3.0.0",
|
"debounce": "3.0.0",
|
||||||
"draggabilly": "3.0.0",
|
"draggabilly": "3.0.0",
|
||||||
"force-graph": "1.51.1",
|
"force-graph": "1.51.0",
|
||||||
"globals": "17.4.0",
|
"globals": "16.5.0",
|
||||||
"i18next": "25.8.17",
|
"i18next": "25.6.2",
|
||||||
"i18next-http-backend": "3.0.2",
|
"i18next-http-backend": "3.0.2",
|
||||||
"jquery": "4.0.0",
|
"jquery": "3.7.1",
|
||||||
"jquery.fancytree": "2.38.5",
|
"jquery.fancytree": "2.38.5",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.38",
|
"katex": "0.16.25",
|
||||||
"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": "17.0.4",
|
"marked": "16.4.2",
|
||||||
"mermaid": "11.12.3",
|
"mermaid": "11.12.1",
|
||||||
"mind-elixir": "5.9.3",
|
"mind-elixir": "5.3.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.29.0",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.5.6",
|
"react-i18next": "16.3.1",
|
||||||
"react-window": "2.2.7",
|
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"rrule": "2.8.1",
|
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.4.0",
|
"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": "5.0.0",
|
||||||
"@prefresh/vite": "2.4.12",
|
"@preact/preset-vite": "2.10.2",
|
||||||
"@types/bootstrap": "5.2.10",
|
"@types/bootstrap": "5.2.10",
|
||||||
"@types/jquery": "4.0.0",
|
"@types/jquery": "3.5.33",
|
||||||
"@types/leaflet": "1.9.21",
|
"@types/leaflet": "1.9.21",
|
||||||
"@types/leaflet-gpx": "1.3.8",
|
"@types/leaflet-gpx": "1.3.8",
|
||||||
"@types/mark.js": "8.11.12",
|
"@types/mark.js": "8.11.12",
|
||||||
"@types/reveal.js": "5.2.2",
|
"@types/reveal.js": "5.2.1",
|
||||||
"@types/tabulator-tables": "6.3.1",
|
"@types/tabulator-tables": "6.3.0",
|
||||||
"copy-webpack-plugin": "14.0.0",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"happy-dom": "20.8.3",
|
"happy-dom": "20.0.10",
|
||||||
"lightningcss": "1.32.0",
|
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"vite-plugin-static-copy": "3.2.0"
|
"vite-plugin-static-copy": "3.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,39 @@
|
|||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
|
||||||
import { SqlExecuteResponse } from "@triliumnext/commons";
|
|
||||||
import type { NativeImage, TouchBar } from "electron";
|
|
||||||
import { ColumnComponent } from "tabulator-tables";
|
|
||||||
|
|
||||||
import type { Attribute } from "../services/attribute_parser.js";
|
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import { initLocale, t } from "../services/i18n.js";
|
import RootCommandExecutor from "./root_command_executor.js";
|
||||||
|
import Entrypoints from "./entrypoints.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import utils, { hasTouchBar } from "../services/utils.js";
|
||||||
|
import zoomComponent from "./zoom.js";
|
||||||
|
import TabManager from "./tab_manager.js";
|
||||||
|
import Component from "./component.js";
|
||||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||||
import linkService, { type ViewScope } from "../services/link.js";
|
import linkService, { type ViewScope } from "../services/link.js";
|
||||||
import type LoadResults from "../services/load_results.js";
|
|
||||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import toast from "../services/toast.js";
|
|
||||||
import utils, { hasTouchBar } from "../services/utils.js";
|
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
|
||||||
import type RootContainer from "../widgets/containers/root_container.js";
|
|
||||||
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
|
||||||
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
|
||||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
|
||||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
|
||||||
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
|
||||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
|
||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
|
||||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
|
||||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
|
||||||
import Component from "./component.js";
|
|
||||||
import Entrypoints from "./entrypoints.js";
|
|
||||||
import MainTreeExecutors from "./main_tree_executors.js";
|
|
||||||
import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js";
|
import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js";
|
||||||
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
import MainTreeExecutors from "./main_tree_executors.js";
|
||||||
import RootCommandExecutor from "./root_command_executor.js";
|
import toast from "../services/toast.js";
|
||||||
import ShortcutComponent from "./shortcut_component.js";
|
import ShortcutComponent from "./shortcut_component.js";
|
||||||
import { StartupChecks } from "./startup_checks.js";
|
import { t, initLocale } from "../services/i18n.js";
|
||||||
import TabManager from "./tab_manager.js";
|
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||||
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
|
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||||
|
import type LoadResults from "../services/load_results.js";
|
||||||
|
import type { Attribute } from "../services/attribute_parser.js";
|
||||||
|
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
|
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
||||||
|
import type { NativeImage, TouchBar } from "electron";
|
||||||
import TouchBarComponent from "./touch_bar.js";
|
import TouchBarComponent from "./touch_bar.js";
|
||||||
import zoomComponent from "./zoom.js";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
|
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";
|
||||||
|
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||||
|
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||||
|
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||||
|
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
@@ -101,6 +99,8 @@ export type CommandMappings = {
|
|||||||
showRevisions: CommandData & {
|
showRevisions: CommandData & {
|
||||||
noteId?: string | null;
|
noteId?: string | null;
|
||||||
};
|
};
|
||||||
|
showLlmChat: CommandData;
|
||||||
|
createAiChat: CommandData;
|
||||||
showOptions: CommandData & {
|
showOptions: CommandData & {
|
||||||
section: string;
|
section: string;
|
||||||
};
|
};
|
||||||
@@ -124,7 +124,7 @@ export type CommandMappings = {
|
|||||||
isNewNote?: boolean;
|
isNewNote?: boolean;
|
||||||
};
|
};
|
||||||
showPromptDialog: PromptDialogOptions;
|
showPromptDialog: PromptDialogOptions;
|
||||||
showInfoDialog: InfoProps;
|
showInfoDialog: ConfirmWithMessageOptions;
|
||||||
showConfirmDialog: ConfirmWithMessageOptions;
|
showConfirmDialog: ConfirmWithMessageOptions;
|
||||||
showRecentChanges: CommandData & { ancestorNoteId: string };
|
showRecentChanges: CommandData & { ancestorNoteId: string };
|
||||||
showImportDialog: CommandData & { noteId: string };
|
showImportDialog: CommandData & { noteId: string };
|
||||||
@@ -152,7 +152,6 @@ export type CommandMappings = {
|
|||||||
};
|
};
|
||||||
openInTab: ContextMenuCommandData;
|
openInTab: ContextMenuCommandData;
|
||||||
openNoteInSplit: ContextMenuCommandData;
|
openNoteInSplit: ContextMenuCommandData;
|
||||||
openNoteInWindow: ContextMenuCommandData;
|
|
||||||
openNoteInPopup: ContextMenuCommandData;
|
openNoteInPopup: ContextMenuCommandData;
|
||||||
toggleNoteHoisting: ContextMenuCommandData;
|
toggleNoteHoisting: ContextMenuCommandData;
|
||||||
insertNoteAfter: ContextMenuCommandData;
|
insertNoteAfter: ContextMenuCommandData;
|
||||||
@@ -265,7 +264,7 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
reEvaluateRightPaneVisibility: CommandData;
|
reEvaluateRightPaneVisibility: CommandData;
|
||||||
runActiveNote: CommandData;
|
runActiveNote: CommandData;
|
||||||
scrollContainerTo: CommandData & {
|
scrollContainerToCommand: CommandData & {
|
||||||
position: number;
|
position: number;
|
||||||
};
|
};
|
||||||
scrollToEnd: CommandData;
|
scrollToEnd: CommandData;
|
||||||
@@ -381,8 +380,7 @@ export type CommandMappings = {
|
|||||||
reloadTextEditor: CommandData;
|
reloadTextEditor: CommandData;
|
||||||
chooseNoteType: CommandData & {
|
chooseNoteType: CommandData & {
|
||||||
callback: ChooseNoteTypeCallback
|
callback: ChooseNoteTypeCallback
|
||||||
};
|
}
|
||||||
customDownload: CommandData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventMappings = {
|
type EventMappings = {
|
||||||
@@ -408,7 +406,7 @@ type EventMappings = {
|
|||||||
addNewLabel: CommandData;
|
addNewLabel: CommandData;
|
||||||
addNewRelation: CommandData;
|
addNewRelation: CommandData;
|
||||||
sqlQueryResults: CommandData & {
|
sqlQueryResults: CommandData & {
|
||||||
response: SqlExecuteResponse;
|
results: SqlExecuteResults;
|
||||||
};
|
};
|
||||||
readOnlyTemporarilyDisabled: {
|
readOnlyTemporarilyDisabled: {
|
||||||
noteContext: NoteContext;
|
noteContext: NoteContext;
|
||||||
@@ -447,8 +445,6 @@ type EventMappings = {
|
|||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
searchRefreshed: { ntxId?: string | null };
|
searchRefreshed: { ntxId?: string | null };
|
||||||
textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor };
|
|
||||||
contentElRefreshed: { ntxId?: string | null, contentEl: HTMLElement };
|
|
||||||
hoistedNoteChanged: {
|
hoistedNoteChanged: {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
ntxId: string | null;
|
ntxId: string | null;
|
||||||
@@ -473,11 +469,6 @@ type EventMappings = {
|
|||||||
noteContextRemoved: {
|
noteContextRemoved: {
|
||||||
ntxIds: string[];
|
ntxIds: string[];
|
||||||
};
|
};
|
||||||
contextDataChanged: {
|
|
||||||
noteContext: NoteContext;
|
|
||||||
key: string;
|
|
||||||
value: unknown;
|
|
||||||
};
|
|
||||||
exportSvg: { ntxId: string | null | undefined; };
|
exportSvg: { ntxId: string | null | undefined; };
|
||||||
exportPng: { ntxId: string | null | undefined; };
|
exportPng: { ntxId: string | null | undefined; };
|
||||||
geoMapCreateChildNote: {
|
geoMapCreateChildNote: {
|
||||||
@@ -495,7 +486,7 @@ type EventMappings = {
|
|||||||
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
||||||
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
||||||
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
||||||
activeNoteChanged: {ntxId: string | null | undefined};
|
activeNoteChanged: {};
|
||||||
showAddLinkDialog: AddLinkOpts;
|
showAddLinkDialog: AddLinkOpts;
|
||||||
showIncludeDialog: IncludeNoteOpts;
|
showIncludeDialog: IncludeNoteOpts;
|
||||||
openBulkActionsDialog: {
|
openBulkActionsDialog: {
|
||||||
@@ -702,8 +693,10 @@ $(window).on("beforeunload", () => {
|
|||||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||||
allSaved = false;
|
allSaved = false;
|
||||||
}
|
}
|
||||||
} else if (!listener()) {
|
} else {
|
||||||
allSaved = false;
|
if (!listener()) {
|
||||||
|
allSaved = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,7 +706,7 @@ $(window).on("beforeunload", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(window).on("hashchange", () => {
|
$(window).on("hashchange", function () {
|
||||||
const { notePath, ntxId, viewScope, searchString } = linkService.parseNavigationStateFromUrl(window.location.href);
|
const { notePath, ntxId, viewScope, searchString } = linkService.parseNavigationStateFromUrl(window.location.href);
|
||||||
|
|
||||||
if (notePath || ntxId) {
|
if (notePath || ntxId) {
|
||||||
|
|||||||
@@ -57,18 +57,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a child component from this component's children array.
|
|
||||||
* This is used for cleanup when a widget is unmounted to prevent event listener accumulation.
|
|
||||||
*/
|
|
||||||
removeChild(component: ChildT) {
|
|
||||||
const index = this.children.indexOf(component);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.children.splice(index, 1);
|
|
||||||
component.parent = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
|
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
|
||||||
try {
|
try {
|
||||||
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
|
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
|
||||||
@@ -77,8 +65,8 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
// don't create promises if not needed (optimization)
|
// don't create promises if not needed (optimization)
|
||||||
return callMethodPromise && childrenPromise ? Promise.all([callMethodPromise, childrenPromise]) : callMethodPromise || childrenPromise;
|
return callMethodPromise && childrenPromise ? Promise.all([callMethodPromise, childrenPromise]) : callMethodPromise || childrenPromise;
|
||||||
} catch (e: unknown) {
|
} catch (e: any) {
|
||||||
console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error`, e);
|
console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error ${e.message} ${e.stack}`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
import bundleService from "../services/bundle.js";
|
|
||||||
import dateNoteService from "../services/date_notes.js";
|
import dateNoteService from "../services/date_notes.js";
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import linkService from "../services/link.js";
|
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import toastService from "../services/toast.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import ws from "../services/ws.js";
|
|
||||||
import appContext, { type NoteCommandData } from "./app_context.js";
|
import appContext, { type NoteCommandData } from "./app_context.js";
|
||||||
import Component from "./component.js";
|
import Component from "./component.js";
|
||||||
|
import toastService from "../services/toast.js";
|
||||||
|
import ws from "../services/ws.js";
|
||||||
|
import bundleService from "../services/bundle.js";
|
||||||
|
import froca from "../services/froca.js";
|
||||||
|
import linkService from "../services/link.js";
|
||||||
|
import { t } from "../services/i18n.js";
|
||||||
|
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
export default class Entrypoints extends Component {
|
export default class Entrypoints extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -188,8 +187,13 @@ export default class Entrypoints extends Component {
|
|||||||
} else if (note.mime.endsWith("env=backend")) {
|
} else if (note.mime.endsWith("env=backend")) {
|
||||||
await server.post(`script/run/${note.noteId}`);
|
await server.post(`script/run/${note.noteId}`);
|
||||||
} else if (note.mime === "text/x-sqlite;schema=trilium") {
|
} else if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||||
const response = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||||
await appContext.triggerEvent("sqlQueryResults", { ntxId, response });
|
|
||||||
|
if (!resp.success) {
|
||||||
|
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results });
|
||||||
}
|
}
|
||||||
|
|
||||||
toastService.showMessage(t("entrypoints.note-executed"));
|
toastService.showMessage(t("entrypoints.note-executed"));
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
|
||||||
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import { closeActiveDialog } from "../services/dialog.js";
|
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import hoistedNoteService from "../services/hoisted_note.js";
|
|
||||||
import type { ViewScope } from "../services/link.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import treeService from "../services/tree.js";
|
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
|
||||||
import type { HeadingContext } from "../widgets/sidebar/TableOfContents.js";
|
|
||||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||||
|
import treeService from "../services/tree.js";
|
||||||
import Component from "./component.js";
|
import Component from "./component.js";
|
||||||
|
import froca from "../services/froca.js";
|
||||||
|
import hoistedNoteService from "../services/hoisted_note.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import type { ViewScope } from "../services/link.js";
|
||||||
|
import type FNote from "../entities/fnote.js";
|
||||||
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
|
import { closeActiveDialog } from "../services/dialog.js";
|
||||||
|
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||||
|
|
||||||
export interface SetNoteOpts {
|
export interface SetNoteOpts {
|
||||||
triggerSwitchEvent?: unknown;
|
triggerSwitchEvent?: unknown;
|
||||||
@@ -23,31 +21,6 @@ export interface SetNoteOpts {
|
|||||||
|
|
||||||
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
|
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
|
||||||
|
|
||||||
export type SaveState = "saved" | "saving" | "unsaved" | "error";
|
|
||||||
|
|
||||||
export interface NoteContextDataMap {
|
|
||||||
toc: HeadingContext;
|
|
||||||
pdfPages: {
|
|
||||||
totalPages: number;
|
|
||||||
currentPage: number;
|
|
||||||
scrollToPage(page: number): void;
|
|
||||||
requestThumbnail(page: number): void;
|
|
||||||
};
|
|
||||||
pdfAttachments: {
|
|
||||||
attachments: PdfAttachment[];
|
|
||||||
downloadAttachment(filename: string): void;
|
|
||||||
};
|
|
||||||
pdfLayers: {
|
|
||||||
layers: PdfLayer[];
|
|
||||||
toggleLayer(layerId: string, visible: boolean): void;
|
|
||||||
};
|
|
||||||
saveState: {
|
|
||||||
state: SaveState;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContextDataKey = keyof NoteContextDataMap;
|
|
||||||
|
|
||||||
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
||||||
ntxId: string | null;
|
ntxId: string | null;
|
||||||
hoistedNoteId: string;
|
hoistedNoteId: string;
|
||||||
@@ -58,13 +31,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
parentNoteId?: string | null;
|
parentNoteId?: string | null;
|
||||||
viewScope?: ViewScope;
|
viewScope?: ViewScope;
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata storage for UI components (e.g., table of contents, PDF page list, code outline).
|
|
||||||
* This allows type widgets to publish data that sidebar/toolbar components can consume.
|
|
||||||
* Data is automatically cleared when navigating to a different note.
|
|
||||||
*/
|
|
||||||
private contextData: Map<string, unknown> = new Map();
|
|
||||||
|
|
||||||
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -124,22 +90,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
this.viewScope = opts.viewScope;
|
this.viewScope = opts.viewScope;
|
||||||
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
|
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
|
||||||
|
|
||||||
// Clear context data when switching notes and notify subscribers
|
|
||||||
const oldKeys = Array.from(this.contextData.keys());
|
|
||||||
this.contextData.clear();
|
|
||||||
if (oldKeys.length > 0) {
|
|
||||||
// Notify subscribers asynchronously to avoid blocking navigation
|
|
||||||
window.setTimeout(() => {
|
|
||||||
for (const key of oldKeys) {
|
|
||||||
this.triggerEvent("contextDataChanged", {
|
|
||||||
noteContext: this,
|
|
||||||
key,
|
|
||||||
value: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveToRecentNotes(resolvedNotePath);
|
this.saveToRecentNotes(resolvedNotePath);
|
||||||
|
|
||||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||||
@@ -371,10 +321,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.type === "search") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -439,7 +385,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
* If no content could be determined `null` is returned instead.
|
* If no content could be determined `null` is returned instead.
|
||||||
*/
|
*/
|
||||||
async getContentElement() {
|
async getContentElement() {
|
||||||
return this.timeout<JQuery<HTMLElement> | null>(
|
return this.timeout<JQuery<HTMLElement>>(
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
appContext.triggerCommand("executeWithContentElement", {
|
appContext.triggerCommand("executeWithContentElement", {
|
||||||
resolve,
|
resolve,
|
||||||
@@ -492,52 +438,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set metadata for this note context (e.g., table of contents, PDF pages, code outline).
|
|
||||||
* This data can be consumed by sidebar/toolbar components.
|
|
||||||
*
|
|
||||||
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages", "codeOutline")
|
|
||||||
* @param value - The data to store (will be cleared when switching notes)
|
|
||||||
*/
|
|
||||||
setContextData<K extends ContextDataKey>(key: K, value: NoteContextDataMap[K]): void {
|
|
||||||
this.contextData.set(key, value);
|
|
||||||
// Trigger event so subscribers can react
|
|
||||||
this.triggerEvent("contextDataChanged", {
|
|
||||||
noteContext: this,
|
|
||||||
key,
|
|
||||||
value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get metadata for this note context.
|
|
||||||
*
|
|
||||||
* @param key - The data key to retrieve
|
|
||||||
* @returns The stored data, or undefined if not found
|
|
||||||
*/
|
|
||||||
getContextData<K extends ContextDataKey>(key: K): NoteContextDataMap[K] | undefined {
|
|
||||||
return this.contextData.get(key) as NoteContextDataMap[K] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if context data exists for a given key.
|
|
||||||
*/
|
|
||||||
hasContextData(key: ContextDataKey): boolean {
|
|
||||||
return this.contextData.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear specific context data.
|
|
||||||
*/
|
|
||||||
clearContextData(key: ContextDataKey): void {
|
|
||||||
this.contextData.delete(key);
|
|
||||||
this.triggerEvent("contextDataChanged", {
|
|
||||||
noteContext: this,
|
|
||||||
key,
|
|
||||||
value: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import dateNoteService from "../services/date_notes.js";
|
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import openService from "../services/open.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import protectedSessionService from "../services/protected_session.js";
|
|
||||||
import treeService from "../services/tree.js";
|
|
||||||
import utils, { openInReusableSplit } from "../services/utils.js";
|
|
||||||
import appContext, { type CommandListenerData } from "./app_context.js";
|
|
||||||
import Component from "./component.js";
|
import Component from "./component.js";
|
||||||
|
import appContext, { type CommandData, type CommandListenerData } from "./app_context.js";
|
||||||
|
import dateNoteService from "../services/date_notes.js";
|
||||||
|
import treeService from "../services/tree.js";
|
||||||
|
import openService from "../services/open.js";
|
||||||
|
import protectedSessionService from "../services/protected_session.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import froca from "../services/froca.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
|
import toastService from "../services/toast.js";
|
||||||
|
import noteCreateService from "../services/note_create.js";
|
||||||
|
|
||||||
export default class RootCommandExecutor extends Component {
|
export default class RootCommandExecutor extends Component {
|
||||||
editReadOnlyNoteCommand() {
|
editReadOnlyNoteCommand() {
|
||||||
@@ -191,19 +193,6 @@ export default class RootCommandExecutor extends Component {
|
|||||||
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleRibbonTabNoteMapCommand(data: CommandListenerData<"toggleRibbonTabNoteMap">) {
|
|
||||||
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
|
|
||||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
|
||||||
if (!isNewLayout) {
|
|
||||||
this.triggerEvent("toggleRibbonTabNoteMap", data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeContext = appContext.tabManager.getActiveContext();
|
|
||||||
if (!activeContext?.notePath) return;
|
|
||||||
openInReusableSplit(activeContext.notePath, "note-map");
|
|
||||||
}
|
|
||||||
|
|
||||||
firstTabCommand() {
|
firstTabCommand() {
|
||||||
this.#goToTab(1);
|
this.#goToTab(1);
|
||||||
}
|
}
|
||||||
@@ -246,4 +235,34 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createAiChatCommand() {
|
||||||
|
try {
|
||||||
|
// Create a new AI Chat note at the root level
|
||||||
|
const rootNoteId = "root";
|
||||||
|
|
||||||
|
const result = await noteCreateService.createNote(rootNoteId, {
|
||||||
|
title: "New AI Chat",
|
||||||
|
type: "aiChat",
|
||||||
|
content: JSON.stringify({
|
||||||
|
messages: [],
|
||||||
|
title: "New AI Chat"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.note) {
|
||||||
|
toastService.showError("Failed to create AI Chat note");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await appContext.tabManager.openTabWithNoteWithHoisting(result.note.noteId, {
|
||||||
|
activate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
toastService.showMessage("Created new AI Chat note");
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error("Error creating AI Chat note:", e);
|
||||||
|
toastService.showError("Failed to create AI Chat note: " + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export default class TabManager extends Component {
|
|||||||
const activeNoteContext = this.getActiveContext();
|
const activeNoteContext = this.getActiveContext();
|
||||||
this.updateDocumentTitle(activeNoteContext);
|
this.updateDocumentTitle(activeNoteContext);
|
||||||
|
|
||||||
this.triggerEvent("activeNoteChanged", {ntxId:activeNoteContext?.ntxId}); // trigger this even in on popstate event
|
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateHash(): string {
|
calculateHash(): string {
|
||||||
@@ -647,32 +647,7 @@ export default class TabManager extends Component {
|
|||||||
...this.noteContexts.slice(-noteContexts.length),
|
...this.noteContexts.slice(-noteContexts.length),
|
||||||
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
||||||
];
|
];
|
||||||
|
this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) });
|
||||||
// Update mainNtxId if the restored pane is the main pane in the split pane
|
|
||||||
const { oldMainNtxId, newMainNtxId } = (() => {
|
|
||||||
if (noteContexts.length !== 1) {
|
|
||||||
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainNtxId = noteContexts[0]?.mainNtxId;
|
|
||||||
const index = this.noteContexts.findIndex(c => c.ntxId === mainNtxId);
|
|
||||||
|
|
||||||
// No need to update if the restored position is after mainNtxId
|
|
||||||
if (index === -1 || lastClosedTab.position > index) {
|
|
||||||
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
oldMainNtxId: this.noteContexts[index].ntxId ?? undefined,
|
|
||||||
newMainNtxId: noteContexts[0]?.ntxId ?? undefined
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
this.triggerCommand("noteContextReorder", {
|
|
||||||
ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null),
|
|
||||||
oldMainNtxId,
|
|
||||||
newMainNtxId
|
|
||||||
});
|
|
||||||
|
|
||||||
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
||||||
if (mainNtx) {
|
if (mainNtx) {
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import "autocomplete.js/index_jquery.js";
|
|
||||||
|
|
||||||
import type ElectronRemote from "@electron/remote";
|
|
||||||
import type Electron from "electron";
|
|
||||||
|
|
||||||
import appContext from "./components/app_context.js";
|
import appContext from "./components/app_context.js";
|
||||||
import electronContextMenu from "./menus/electron_context_menu.js";
|
import utils from "./services/utils.js";
|
||||||
|
import noteTooltipService from "./services/note_tooltip.js";
|
||||||
import bundleService from "./services/bundle.js";
|
import bundleService from "./services/bundle.js";
|
||||||
|
import toastService from "./services/toast.js";
|
||||||
|
import noteAutocompleteService from "./services/note_autocomplete.js";
|
||||||
|
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 noteAutocompleteService from "./services/note_autocomplete.js";
|
|
||||||
import noteTooltipService from "./services/note_tooltip.js";
|
|
||||||
import options from "./services/options.js";
|
import options from "./services/options.js";
|
||||||
import toastService from "./services/toast.js";
|
import type ElectronRemote from "@electron/remote";
|
||||||
import utils from "./services/utils.js";
|
import type Electron from "electron";
|
||||||
|
import "boxicons/css/boxicons.min.css";
|
||||||
|
import "autocomplete.js/index_jquery.js";
|
||||||
|
|
||||||
await appContext.earlyInit();
|
await appContext.earlyInit();
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ bundleService.getWidgetBundlesByParent().then(async (widgetBundles) => {
|
|||||||
appContext.setLayout(new DesktopLayout(widgetBundles));
|
appContext.setLayout(new DesktopLayout(widgetBundles));
|
||||||
appContext.start().catch((e) => {
|
appContext.start().catch((e) => {
|
||||||
toastService.showPersistent({
|
toastService.showPersistent({
|
||||||
id: "critical-error",
|
|
||||||
title: t("toast.critical-error.title"),
|
title: t("toast.critical-error.title"),
|
||||||
icon: "alert",
|
icon: "alert",
|
||||||
message: t("toast.critical-error.message", { message: e.message })
|
message: t("toast.critical-error.message", { message: e.message })
|
||||||
@@ -46,6 +44,10 @@ 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));
|
||||||
@@ -56,14 +58,10 @@ function initOnElectron() {
|
|||||||
|
|
||||||
initDarkOrLightMode(style);
|
initDarkOrLightMode(style);
|
||||||
initTransparencyEffects(style, currentWindow);
|
initTransparencyEffects(style, currentWindow);
|
||||||
initFullScreenDetection(currentWindow);
|
|
||||||
|
|
||||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||||
initTitleBarButtons(style, currentWindow);
|
initTitleBarButtons(style, currentWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear navigation history on frontend refresh.
|
|
||||||
currentWindow.webContents.navigationHistory.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||||
@@ -89,28 +87,16 @@ function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFullScreenDetection(currentWindow: Electron.BrowserWindow) {
|
|
||||||
currentWindow.on("enter-full-screen", () => document.body.classList.add("full-screen"));
|
|
||||||
currentWindow.on("leave-full-screen", () => document.body.classList.remove("full-screen"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||||
const material = style.getPropertyValue("--background-material").trim();
|
|
||||||
if (window.glob.platform === "win32") {
|
if (window.glob.platform === "win32") {
|
||||||
|
const material = style.getPropertyValue("--background-material");
|
||||||
|
// TriliumNextTODO: find a nicer way to make TypeScript happy – unfortunately TS did not like Array.includes here
|
||||||
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
|
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
|
||||||
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
|
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
|
||||||
if (foundBgMaterialOption) {
|
if (foundBgMaterialOption) {
|
||||||
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
|
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.glob.platform === "darwin") {
|
|
||||||
const bgMaterialOptions = [ "popover", "tooltip", "titlebar", "selection", "menu", "sidebar", "header", "sheet", "window", "hud", "fullscreen-ui", "content", "under-window", "under-page" ] as const;
|
|
||||||
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
|
|
||||||
if (foundBgMaterialOption) {
|
|
||||||
currentWindow.setVibrancy(foundBgMaterialOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,3 +116,20 @@ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,41 @@
|
|||||||
import { getNoteIcon } from "@triliumnext/commons";
|
import server from "../services/server.js";
|
||||||
|
|
||||||
import cssClassManager from "../services/css_class_manager.js";
|
|
||||||
import type { Froca } from "../services/froca-interface.js";
|
|
||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import search from "../services/search.js";
|
import cssClassManager from "../services/css_class_manager.js";
|
||||||
import server from "../services/server.js";
|
import type { Froca } from "../services/froca-interface.js";
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import type FAttachment from "./fattachment.js";
|
import type FAttachment from "./fattachment.js";
|
||||||
import type { AttributeType, default as FAttribute } from "./fattribute.js";
|
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
|
import search from "../services/search.js";
|
||||||
|
|
||||||
const LABEL = "label";
|
const LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
|
|
||||||
|
const NOTE_TYPE_ICONS = {
|
||||||
|
file: "bx bx-file",
|
||||||
|
image: "bx bx-image",
|
||||||
|
code: "bx bx-code",
|
||||||
|
render: "bx bx-extension",
|
||||||
|
search: "bx bx-file-find",
|
||||||
|
relationMap: "bx bxs-network-chart",
|
||||||
|
book: "bx bx-book",
|
||||||
|
noteMap: "bx bxs-network-chart",
|
||||||
|
mermaid: "bx bx-selection",
|
||||||
|
canvas: "bx bx-pen",
|
||||||
|
webView: "bx bx-globe-alt",
|
||||||
|
launcher: "bx bx-link",
|
||||||
|
doc: "bx bxs-file-doc",
|
||||||
|
contentWidget: "bx bxs-widget",
|
||||||
|
mindMap: "bx bx-sitemap",
|
||||||
|
aiChat: "bx bx-bot"
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* There are many different Note types, some of which are entirely opaque to the
|
* There are many different Note types, some of which are entirely opaque to the
|
||||||
* end user. Those types should be used only for checking against, they are
|
* end user. Those types should be used only for checking against, they are
|
||||||
* not for direct use.
|
* not for direct use.
|
||||||
*/
|
*/
|
||||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
|
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat";
|
||||||
|
|
||||||
export interface NotePathRecord {
|
export interface NotePathRecord {
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
@@ -223,7 +240,7 @@ export default class FNote {
|
|||||||
|
|
||||||
const aNote = this.froca.getNoteFromCache(aNoteId);
|
const aNote = this.froca.getNoteFromCache(aNoteId);
|
||||||
|
|
||||||
if (!aNote || aNote.isArchived || aNote.isHiddenCompletely()) {
|
if (aNote.isArchived || aNote.isHiddenCompletely()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,9 +257,7 @@ export default class FNote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getChildNoteIdsWithArchiveFiltering(includeArchived = false) {
|
async getChildNoteIdsWithArchiveFiltering(includeArchived = false) {
|
||||||
const isHiddenNote = this.noteId.startsWith("_");
|
if (!includeArchived) {
|
||||||
const isSearchNote = this.type === "search";
|
|
||||||
if (!includeArchived && !isHiddenNote && !isSearchNote) {
|
|
||||||
const unorderedIds = new Set(await search.searchForNoteIds(`note.parents.noteId="${this.noteId}" #!archived`));
|
const unorderedIds = new Set(await search.searchForNoteIds(`note.parents.noteId="${this.noteId}" #!archived`));
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
for (const id of this.children) {
|
for (const id of this.children) {
|
||||||
@@ -251,12 +266,13 @@ export default class FNote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
} else {
|
||||||
|
return this.children;
|
||||||
}
|
}
|
||||||
return this.children;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubtreeNoteIds(includeArchived = false) {
|
async getSubtreeNoteIds(includeArchived = false) {
|
||||||
const noteIds: (string | string[])[] = [];
|
let noteIds: (string | string[])[] = [];
|
||||||
for (const child of await this.getChildNotes()) {
|
for (const child of await this.getChildNotes()) {
|
||||||
if (child.isArchived && !includeArchived) continue;
|
if (child.isArchived && !includeArchived) continue;
|
||||||
|
|
||||||
@@ -453,8 +469,9 @@ export default class FNote {
|
|||||||
return a.isHidden ? 1 : -1;
|
return a.isHidden ? 1 : -1;
|
||||||
} else if (a.isSearch !== b.isSearch) {
|
} else if (a.isSearch !== b.isSearch) {
|
||||||
return a.isSearch ? 1 : -1;
|
return a.isSearch ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return a.notePath.length - b.notePath.length;
|
||||||
}
|
}
|
||||||
return a.notePath.length - b.notePath.length;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return notePaths;
|
return notePaths;
|
||||||
@@ -566,15 +583,26 @@ export default class FNote {
|
|||||||
const iconClassLabels = this.getLabels("iconClass");
|
const iconClassLabels = this.getLabels("iconClass");
|
||||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||||
|
|
||||||
const icon = getNoteIcon({
|
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||||
noteId: this.noteId,
|
return iconClassLabels[0].value;
|
||||||
type: this.type,
|
} else if (workspaceIconClass) {
|
||||||
mime: this.mime,
|
return workspaceIconClass;
|
||||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
} else if (this.noteId === "root") {
|
||||||
workspaceIconClass,
|
return "bx bx-home-alt-2";
|
||||||
isFolder: this.isFolder.bind(this)
|
}
|
||||||
});
|
if (this.noteId === "_share") {
|
||||||
return `tn-icon ${icon}`;
|
return "bx bx-share-alt";
|
||||||
|
} else if (this.type === "text") {
|
||||||
|
if (this.isFolder()) {
|
||||||
|
return "bx bx-folder";
|
||||||
|
} else {
|
||||||
|
return "bx bx-note";
|
||||||
|
}
|
||||||
|
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
|
||||||
|
return "bx bx-data";
|
||||||
|
} else {
|
||||||
|
return NOTE_TYPE_ICONS[this.type];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorClass() {
|
getColorClass() {
|
||||||
@@ -583,13 +611,11 @@ export default class FNote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isFolder() {
|
isFolder() {
|
||||||
if (this.isLabelTruthy("subtreeHidden")) return false;
|
return this.type === "search" || this.getFilteredChildBranches().length > 0;
|
||||||
if (this.type === "search") return true;
|
|
||||||
return this.getFilteredChildBranches().length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilteredChildBranches() {
|
getFilteredChildBranches() {
|
||||||
const childBranches = this.getChildBranches();
|
let childBranches = this.getChildBranches();
|
||||||
|
|
||||||
if (!childBranches) {
|
if (!childBranches) {
|
||||||
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
|
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
|
||||||
@@ -700,15 +726,6 @@ export default class FNote {
|
|||||||
return this.hasAttribute(LABEL, name);
|
return this.hasAttribute(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import).
|
|
||||||
* @param name the name of the label to look for.
|
|
||||||
* @returns `true` if the label exists, or its version with the `disabled:` prefix.
|
|
||||||
*/
|
|
||||||
hasLabelOrDisabled(name: string) {
|
|
||||||
return this.hasLabel(name) || this.hasLabel(`disabled:${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @returns true if label exists (including inherited) and does not have "false" value.
|
* @returns true if label exists (including inherited) and does not have "false" value.
|
||||||
@@ -787,16 +804,6 @@ export default class FNote {
|
|||||||
return this.getAttributeValue(LABEL, name);
|
return this.getAttributeValue(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelOrRelation(nameWithPrefix: string) {
|
|
||||||
if (nameWithPrefix.startsWith("#")) {
|
|
||||||
return this.getLabelValue(nameWithPrefix.substring(1));
|
|
||||||
} else if (nameWithPrefix.startsWith("~")) {
|
|
||||||
return this.getRelationValue(nameWithPrefix.substring(1));
|
|
||||||
}
|
|
||||||
return this.getLabelValue(nameWithPrefix);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation value if relation exists, null otherwise
|
* @returns relation value if relation exists, null otherwise
|
||||||
@@ -859,10 +866,10 @@ export default class FNote {
|
|||||||
promotedAttrs.sort((a, b) => {
|
promotedAttrs.sort((a, b) => {
|
||||||
if (a.noteId === b.noteId) {
|
if (a.noteId === b.noteId) {
|
||||||
return a.position < b.position ? -1 : 1;
|
return a.position < b.position ? -1 : 1;
|
||||||
|
} else {
|
||||||
|
// inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||||
|
return a.noteId < b.noteId ? -1 : 1;
|
||||||
}
|
}
|
||||||
// inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
|
||||||
return a.noteId < b.noteId ? -1 : 1;
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return promotedAttrs;
|
return promotedAttrs;
|
||||||
@@ -974,10 +981,6 @@ export default class FNote {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isJsx() {
|
|
||||||
return (this.type === "code" && this.mime === "text/jsx");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns true if this note is HTML */
|
/** @returns true if this note is HTML */
|
||||||
isHtml() {
|
isHtml() {
|
||||||
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
|
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
|
||||||
@@ -985,7 +988,7 @@ export default class FNote {
|
|||||||
|
|
||||||
/** @returns JS script environment - either "frontend" or "backend" */
|
/** @returns JS script environment - either "frontend" or "backend" */
|
||||||
getScriptEnv() {
|
getScriptEnv() {
|
||||||
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend")) || this.isJsx()) {
|
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) {
|
||||||
return "frontend";
|
return "frontend";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,7 +1010,7 @@ export default class FNote {
|
|||||||
* @returns a promise that resolves when the script has been run. Additionally, for front-end notes, the promise will contain the value that is returned by the script.
|
* @returns a promise that resolves when the script has been run. Additionally, for front-end notes, the promise will contain the value that is returned by the script.
|
||||||
*/
|
*/
|
||||||
async executeScript() {
|
async executeScript() {
|
||||||
if (!(this.isJavaScript() || this.isJsx())) {
|
if (!this.isJavaScript()) {
|
||||||
throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`);
|
throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
async function bootstrap() {
|
|
||||||
showSplash();
|
|
||||||
await setupGlob();
|
|
||||||
await Promise.all([
|
|
||||||
initJQuery(),
|
|
||||||
loadBootstrapCss()
|
|
||||||
]);
|
|
||||||
loadStylesheets();
|
|
||||||
loadIcons();
|
|
||||||
setBodyAttributes();
|
|
||||||
await loadScripts();
|
|
||||||
hideSplash();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initJQuery() {
|
|
||||||
const $ = (await import("jquery")).default;
|
|
||||||
window.$ = $;
|
|
||||||
window.jQuery = $;
|
|
||||||
|
|
||||||
// Polyfill removed jQuery methods for autocomplete.js compatibility
|
|
||||||
($ as any).isArray = Array.isArray;
|
|
||||||
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
|
|
||||||
($ as any).isPlainObject = function(obj: any) {
|
|
||||||
if (obj == null || typeof obj !== 'object') { return false; }
|
|
||||||
const proto = Object.getPrototypeOf(obj);
|
|
||||||
if (proto === null) { return true; }
|
|
||||||
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
|
|
||||||
return typeof Ctor === 'function' && Ctor === Object;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupGlob() {
|
|
||||||
const response = await fetch(`./bootstrap${window.location.search}`);
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
|
||||||
window.glob = {
|
|
||||||
...json,
|
|
||||||
activeDialog: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadBootstrapCss() {
|
|
||||||
// We have to selectively import Bootstrap CSS based on text direction.
|
|
||||||
if (glob.isRtl) {
|
|
||||||
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
|
|
||||||
} else {
|
|
||||||
await import("bootstrap/dist/css/bootstrap.min.css");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadStylesheets() {
|
|
||||||
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
|
||||||
|
|
||||||
const cssToLoad: string[] = [];
|
|
||||||
if (device !== "print") {
|
|
||||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
|
||||||
cssToLoad.push(`api/fonts`);
|
|
||||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
|
||||||
if (themeCssUrl) {
|
|
||||||
cssToLoad.push(themeCssUrl);
|
|
||||||
}
|
|
||||||
if (themeUseNextAsBase === "next") {
|
|
||||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
|
||||||
} else if (themeUseNextAsBase === "next-dark") {
|
|
||||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
|
||||||
} else if (themeUseNextAsBase === "next-light") {
|
|
||||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
|
||||||
}
|
|
||||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const href of cssToLoad) {
|
|
||||||
const linkEl = document.createElement("link");
|
|
||||||
linkEl.href = href;
|
|
||||||
linkEl.rel = "stylesheet";
|
|
||||||
document.head.appendChild(linkEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadIcons() {
|
|
||||||
const styleEl = document.createElement("style");
|
|
||||||
styleEl.innerText = window.glob.iconPackCss;
|
|
||||||
document.head.appendChild(styleEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBodyAttributes() {
|
|
||||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
|
|
||||||
const classesToSet = [
|
|
||||||
device,
|
|
||||||
`heading-style-${headingStyle}`,
|
|
||||||
`layout-${layoutOrientation}`,
|
|
||||||
`platform-${platform}`,
|
|
||||||
isElectron && "electron",
|
|
||||||
hasNativeTitleBar && "native-titlebar",
|
|
||||||
hasBackgroundEffects && "background-effects"
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
|
|
||||||
for (const classToSet of classesToSet) {
|
|
||||||
document.body.classList.add(classToSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.lang = currentLocale.id;
|
|
||||||
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadScripts() {
|
|
||||||
switch (glob.device) {
|
|
||||||
case "mobile":
|
|
||||||
await import("./mobile.js");
|
|
||||||
break;
|
|
||||||
case "print":
|
|
||||||
await import("./print.js");
|
|
||||||
break;
|
|
||||||
case "desktop":
|
|
||||||
default:
|
|
||||||
await import("./desktop.js");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSplash() {
|
|
||||||
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
|
|
||||||
document.body.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSplash() {
|
|
||||||
document.body.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap();
|
|
||||||
|
|||||||
@@ -1,57 +1,50 @@
|
|||||||
import type { AppContext } from "../components/app_context.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import ApiLog from "../widgets/api_log.jsx";
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
|
import FindWidget from "../widgets/find.js";
|
||||||
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||||
|
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||||
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
import RightPaneToggle from "../widgets/buttons/right_pane_toggle.jsx";
|
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
|
||||||
import ContentHeader from "../widgets/containers/content_header.js";
|
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
|
||||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
|
||||||
import FindWidget from "../widgets/find.js";
|
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
|
||||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
|
||||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
|
||||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
|
||||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
|
||||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
|
||||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
|
||||||
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import options from "../services/options.js";
|
||||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||||
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
|
|
||||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
|
||||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
import SharedInfo from "../widgets/shared_info.jsx";
|
import SharedInfo from "../widgets/shared_info.jsx";
|
||||||
import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx";
|
import OriginInfo from "../widgets/note_origin.jsx";
|
||||||
|
import SpacerWidget from "../widgets/spacer.js";
|
||||||
|
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||||
|
import SqlResults from "../widgets/sql_result.js";
|
||||||
|
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
|
||||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||||
import TocWidget from "../widgets/toc.js";
|
import TocWidget from "../widgets/toc.js";
|
||||||
|
import type { AppContext } from "../components/app_context.js";
|
||||||
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||||
import { applyModals } from "./layout_commons.js";
|
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -77,20 +70,17 @@ export default class DesktopLayout {
|
|||||||
*/
|
*/
|
||||||
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
||||||
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
||||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
|
||||||
|
|
||||||
const rootContainer = new RootContainer(true)
|
const rootContainer = new RootContainer(true)
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
|
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
|
||||||
.optChild(
|
.optChild(
|
||||||
fullWidthTabBar,
|
fullWidthTabBar,
|
||||||
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, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||||
.child(<TabHistoryNavigationButtons />)
|
|
||||||
.child(new TabRowWidget().class("full-width"))
|
.child(new TabRowWidget().class("full-width"))
|
||||||
.optChild(isNewLayout, <RightPaneToggle />)
|
|
||||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||||
.css("height", "40px")
|
.css("height", "40px")
|
||||||
.css("background-color", "var(--launcher-pane-background-color)")
|
.css("background-color", "var(--launcher-pane-background-color)")
|
||||||
@@ -112,17 +102,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,
|
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
||||||
new FlexContainer("row")
|
|
||||||
.class("tab-row-container")
|
|
||||||
.child(<TabHistoryNavigationButtons />)
|
|
||||||
.child(new TabRowWidget())
|
|
||||||
.optChild(isNewLayout, <RightPaneToggle />)
|
|
||||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
|
||||||
.css("height", "40px")
|
|
||||||
.css("align-items", "center")
|
|
||||||
)
|
|
||||||
.optChild(isNewLayout, <FixedFormattingToolbar />)
|
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.filling()
|
.filling()
|
||||||
@@ -136,56 +116,59 @@ export default class DesktopLayout {
|
|||||||
.child(
|
.child(
|
||||||
new SplitNoteContainer(() =>
|
new SplitNoteContainer(() =>
|
||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
.child(new FlexContainer("row")
|
.child(
|
||||||
.class("title-row note-split-title")
|
new FlexContainer("row")
|
||||||
.cssBlock(".title-row > * { margin: 5px; }")
|
.class("title-row")
|
||||||
.child(<NoteIconWidget />)
|
.css("height", "50px")
|
||||||
.child(<NoteTitleWidget />)
|
.css("min-height", "50px")
|
||||||
.optChild(isNewLayout, <NoteBadges />)
|
.css("align-items", "center")
|
||||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.optChild(!isNewLayout, <MovePaneButton direction="left" />)
|
.child(<NoteIconWidget />)
|
||||||
.optChild(!isNewLayout, <MovePaneButton direction="right" />)
|
.child(<NoteTitleWidget />)
|
||||||
.optChild(!isNewLayout, <ClosePaneButton />)
|
.child(new SpacerWidget(0, 1))
|
||||||
.optChild(!isNewLayout, <CreatePaneButton />)
|
.child(<MovePaneButton direction="left" />)
|
||||||
.optChild(isNewLayout, <NoteActions />))
|
.child(<MovePaneButton direction="right" />)
|
||||||
.optChild(!isNewLayout, <Ribbon />)
|
.child(<ClosePaneButton />)
|
||||||
|
.child(<CreatePaneButton />)
|
||||||
|
)
|
||||||
|
.child(<Ribbon />)
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.optChild(!isNewLayout, <FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.optChild(isNewLayout, <InlineTitle />)
|
.child(new ContentHeader()
|
||||||
.optChild(isNewLayout, <NoteTitleActions />)
|
|
||||||
.optChild(!isNewLayout, new ContentHeader()
|
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
|
.child(<OriginInfo />)
|
||||||
.child(<SharedInfo />)
|
.child(<SharedInfo />)
|
||||||
)
|
)
|
||||||
.optChild(!isNewLayout, <PromotedAttributes />)
|
.child(new PromotedAttributesWidget())
|
||||||
|
.child(<SqlTableSchemas />)
|
||||||
.child(<NoteDetail />)
|
.child(<NoteDetail />)
|
||||||
.child(<NoteList media="screen" />)
|
.child(<NoteList media="screen" />)
|
||||||
.child(<SearchResult />)
|
.child(<SearchResult />)
|
||||||
|
.child(<SqlResults />)
|
||||||
.child(<ScrollPadding />)
|
.child(<ScrollPadding />)
|
||||||
)
|
)
|
||||||
.child(<ApiLog />)
|
.child(<ApiLog />)
|
||||||
.child(new FindWidget())
|
.child(new FindWidget())
|
||||||
.child(...this.customWidgets.get("note-detail-pane"))
|
.child(
|
||||||
|
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||||
|
...this.customWidgets.get("note-detail-pane")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(...this.customWidgets.get("center-pane"))
|
.child(...this.customWidgets.get("center-pane"))
|
||||||
|
|
||||||
)
|
)
|
||||||
.optChild(!isNewLayout,
|
.child(
|
||||||
new RightPaneContainer()
|
new RightPaneContainer()
|
||||||
.child(new TocWidget())
|
.child(new TocWidget())
|
||||||
.child(new HighlightsListWidget())
|
.child(new HighlightsListWidget())
|
||||||
.child(...this.customWidgets.get("right-pane"))
|
.child(...this.customWidgets.get("right-pane"))
|
||||||
)
|
)
|
||||||
.optChild(isNewLayout, <RightPanelContainer widgetsByParent={this.customWidgets} />)
|
|
||||||
)
|
)
|
||||||
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
|
||||||
.child(<CloseZenModeButton />)
|
.child(<CloseZenModeButton />)
|
||||||
|
|
||||||
// Desktop-specific dialogs.
|
// Desktop-specific dialogs.
|
||||||
@@ -203,14 +186,14 @@ export default class DesktopLayout {
|
|||||||
launcherPane = new FlexContainer("row")
|
launcherPane = new FlexContainer("row")
|
||||||
.css("height", "53px")
|
.css("height", "53px")
|
||||||
.class("horizontal")
|
.class("horizontal")
|
||||||
.child(<LauncherContainer isHorizontalLayout={true} />)
|
.child(new LauncherContainer(true))
|
||||||
.child(<GlobalMenu isHorizontalLayout={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(<GlobalMenu isHorizontalLayout={false} />)
|
||||||
.child(<LauncherContainer isHorizontalLayout={false} />)
|
.child(new LauncherContainer(false))
|
||||||
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,16 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
|||||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||||
import InfoDialog from "../widgets/dialogs/info.js";
|
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 FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
|
import NoteIconWidget from "../widgets/note_icon";
|
||||||
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||||
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
import ToastContainer from "../widgets/Toast.jsx";
|
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
|
||||||
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
|
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||||
|
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||||
|
|
||||||
export function applyModals(rootContainer: RootContainer) {
|
export function applyModals(rootContainer: RootContainer) {
|
||||||
rootContainer
|
rootContainer
|
||||||
@@ -50,7 +57,16 @@ export function applyModals(rootContainer: RootContainer) {
|
|||||||
.child(<ConfirmDialog />)
|
.child(<ConfirmDialog />)
|
||||||
.child(<PromptDialog />)
|
.child(<PromptDialog />)
|
||||||
.child(<IncorrectCpuArchDialog />)
|
.child(<IncorrectCpuArchDialog />)
|
||||||
.child(<PopupEditorDialog />)
|
.child(new PopupEditorDialog()
|
||||||
.child(<CallToActionDialog />)
|
.child(new FlexContainer("row")
|
||||||
.child(<ToastContainer />);
|
.class("title-row")
|
||||||
|
.css("align-items", "center")
|
||||||
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
|
.child(<NoteIconWidget />)
|
||||||
|
.child(<NoteTitleWidget />))
|
||||||
|
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||||
|
.child(new PromotedAttributesWidget())
|
||||||
|
.child(<NoteDetail />)
|
||||||
|
.child(<NoteList media="screen" displayOnlyCollections />))
|
||||||
|
.child(<CallToActionDialog />);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
#background-color-tracker {
|
|
||||||
color: var(--main-background-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.keyboard-shortcut,
|
|
||||||
kbd {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
font-size: larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.25em;
|
|
||||||
padding-inline-start: 0.5em;
|
|
||||||
padding-inline-end: 0.5em;
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
.quick-search {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.quick-search .dropdown-menu {
|
|
||||||
max-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* #region Tree */
|
|
||||||
.tree-wrapper {
|
|
||||||
max-height: 100%;
|
|
||||||
margin-top: 0px;
|
|
||||||
overflow-y: auto;
|
|
||||||
contain: content;
|
|
||||||
padding-inline-start: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancytree-title {
|
|
||||||
margin-inline-start: 0.6em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancytree-node {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.fancytree-expander {
|
|
||||||
width: 24px !important;
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancytree-loading span.fancytree-expander {
|
|
||||||
width: 24px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancytree-loading span.fancytree-expander:after {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin-top: 4px;
|
|
||||||
border-width: 2px;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-wrapper .collapse-tree-button,
|
|
||||||
.tree-wrapper .scroll-to-active-note-button,
|
|
||||||
.tree-wrapper .tree-settings-button {
|
|
||||||
position: fixed;
|
|
||||||
margin-inline-end: 16px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-wrapper .unhoist-button {
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
|
||||||
/* #endregion */
|
|
||||||
@@ -1,38 +1,126 @@
|
|||||||
import "./mobile_layout.css";
|
import { applyModals } from "./layout_commons.js";
|
||||||
|
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
import type AppContext from "../components/app_context.js";
|
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
import FindWidget from "../widgets/find.js";
|
|
||||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
|
||||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
|
||||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
|
||||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
|
||||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
|
||||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
import ScrollPadding from "../widgets/scroll_padding";
|
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||||
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
|
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
|
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||||
|
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||||
|
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||||
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
|
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||||
|
import type AppContext from "../components/app_context.js";
|
||||||
|
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||||
import { applyModals } from "./layout_commons.js";
|
|
||||||
|
const MOBILE_CSS = `
|
||||||
|
<style>
|
||||||
|
kbd {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25em;
|
||||||
|
padding-inline-start: 0.5em;
|
||||||
|
padding-inline-end: 0.5em;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
.quick-search {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.quick-search .dropdown-menu {
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
|
||||||
|
const FANCYTREE_CSS = `
|
||||||
|
<style>
|
||||||
|
.tree-wrapper {
|
||||||
|
max-height: 100%;
|
||||||
|
margin-top: 0px;
|
||||||
|
overflow-y: auto;
|
||||||
|
contain: content;
|
||||||
|
padding-inline-start: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-custom-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-inline-start: 0.6em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-node {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-node .fancytree-expander:before {
|
||||||
|
font-size: 2em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.fancytree-expander {
|
||||||
|
width: 24px !important;
|
||||||
|
margin-inline-end: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-loading span.fancytree-expander {
|
||||||
|
width: 24px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-loading span.fancytree-expander:after {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-wrapper .collapse-tree-button,
|
||||||
|
.tree-wrapper .scroll-to-active-note-button,
|
||||||
|
.tree-wrapper .tree-settings-button {
|
||||||
|
position: fixed;
|
||||||
|
margin-inline-end: 16px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-wrapper .unhoist-button {
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
|
||||||
export default class MobileLayout {
|
export default class MobileLayout {
|
||||||
getRootWidget(appContext: typeof AppContext) {
|
getRootWidget(appContext: typeof AppContext) {
|
||||||
const rootContainer = new RootContainer(true)
|
const rootContainer = new RootContainer(true)
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
.class("horizontal-layout")
|
.class("horizontal-layout")
|
||||||
|
.cssBlock(MOBILE_CSS)
|
||||||
.child(new FlexContainer("column").id("mobile-sidebar-container"))
|
.child(new FlexContainer("column").id("mobile-sidebar-container"))
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
@@ -46,40 +134,40 @@ export default class MobileLayout {
|
|||||||
.css("padding-inline-start", "0")
|
.css("padding-inline-start", "0")
|
||||||
.css("padding-inline-end", "0")
|
.css("padding-inline-end", "0")
|
||||||
.css("contain", "content")
|
.css("contain", "content")
|
||||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget()))
|
.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", "row")
|
||||||
.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 SplitNoteContainer(() =>
|
new NoteWrapperWidget()
|
||||||
new NoteWrapperWidget()
|
.child(
|
||||||
.child(
|
new FlexContainer("row")
|
||||||
new FlexContainer("row")
|
.contentSized()
|
||||||
.class("title-row note-split-title")
|
.css("font-size", "larger")
|
||||||
.contentSized()
|
.css("align-items", "center")
|
||||||
.css("align-items", "center")
|
.child(<ToggleSidebarButton />)
|
||||||
.child(<ToggleSidebarButton />)
|
.child(<NoteTitleWidget />)
|
||||||
.child(<NoteIconWidget />)
|
.child(<MobileDetailMenu />)
|
||||||
.child(<NoteTitleWidget />)
|
)
|
||||||
.child(<NoteBadges />)
|
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||||
.child(<MobileDetailMenu />)
|
.child(new PromotedAttributesWidget())
|
||||||
)
|
.child(
|
||||||
.child(
|
new ScrollingContainer()
|
||||||
new ScrollingContainer()
|
.filling()
|
||||||
.filling()
|
.contentSized()
|
||||||
.contentSized()
|
.child(new ContentHeader()
|
||||||
.child(<InlineTitle />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
.child(<NoteTitleActions />)
|
.child(<SharedInfoWidget />)
|
||||||
.child(<NoteDetail />)
|
)
|
||||||
.child(<NoteList media="screen" />)
|
.child(<NoteDetail />)
|
||||||
.child(<SearchResult />)
|
.child(<NoteList media="screen" />)
|
||||||
.child(<ScrollPadding />)
|
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||||
)
|
.child(<SearchResult />)
|
||||||
.child(<MobileEditorToolbar />)
|
.child(<FilePropertiesWrapper />)
|
||||||
.child(new FindWidget())
|
)
|
||||||
)
|
.child(<MobileEditorToolbar />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -87,10 +175,11 @@ export default class MobileLayout {
|
|||||||
new FlexContainer("column")
|
new FlexContainer("column")
|
||||||
.contentSized()
|
.contentSized()
|
||||||
.id("mobile-bottom-bar")
|
.id("mobile-bottom-bar")
|
||||||
|
.child(new TabRowWidget().css("height", "40px"))
|
||||||
.child(new FlexContainer("row")
|
.child(new FlexContainer("row")
|
||||||
.class("horizontal")
|
.class("horizontal")
|
||||||
.css("height", "53px")
|
.css("height", "53px")
|
||||||
.child(<LauncherContainer isHorizontalLayout />)
|
.child(new LauncherContainer(true))
|
||||||
.child(<GlobalMenuWidget isHorizontalLayout />)
|
.child(<GlobalMenuWidget isHorizontalLayout />)
|
||||||
.id("launcher-pane"))
|
.id("launcher-pane"))
|
||||||
)
|
)
|
||||||
@@ -99,3 +188,13 @@ export default class MobileLayout {
|
|||||||
return rootContainer;
|
return rootContainer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilePropertiesWrapper() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
import { h, JSX, render } from "preact";
|
|
||||||
|
|
||||||
import keyboardActionService, { getActionSync } 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;
|
||||||
@@ -16,11 +15,6 @@ export interface ContextMenuOptions<T> {
|
|||||||
onHide?: () => void;
|
onHide?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMenuItem {
|
|
||||||
kind: "custom",
|
|
||||||
componentFn: () => JSX.Element | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MenuSeparatorItem {
|
export interface MenuSeparatorItem {
|
||||||
kind: "separator";
|
kind: "separator";
|
||||||
}
|
}
|
||||||
@@ -57,23 +51,23 @@ export interface MenuCommandItem<T> {
|
|||||||
columns?: number;
|
columns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T> = MenuCommandItem<T> | CustomMenuItem | MenuSeparatorItem | MenuHeader;
|
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
|
||||||
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;
|
||||||
|
|
||||||
class ContextMenu {
|
class ContextMenu {
|
||||||
private $widget: JQuery<HTMLElement>;
|
private $widget: JQuery<HTMLElement>;
|
||||||
private $cover?: JQuery<HTMLElement>;
|
private $cover: JQuery<HTMLElement>;
|
||||||
private options?: ContextMenuOptions<any>;
|
private options?: ContextMenuOptions<any>;
|
||||||
private isMobile: boolean;
|
private isMobile: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.$widget = $("#context-menu-container");
|
this.$widget = $("#context-menu-container");
|
||||||
|
this.$cover = $("#context-menu-cover");
|
||||||
this.$widget.addClass("dropend");
|
this.$widget.addClass("dropend");
|
||||||
this.isMobile = utils.isMobile();
|
this.isMobile = utils.isMobile();
|
||||||
|
|
||||||
if (this.isMobile) {
|
if (this.isMobile) {
|
||||||
this.$cover = $("#context-menu-cover");
|
|
||||||
this.$cover.on("click", () => this.hide());
|
this.$cover.on("click", () => this.hide());
|
||||||
} else {
|
} else {
|
||||||
$(document).on("click", (e) => this.hide());
|
$(document).on("click", (e) => this.hide());
|
||||||
@@ -92,7 +86,7 @@ class ContextMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
||||||
this.$cover?.addClass("show");
|
this.$cover.addClass("show");
|
||||||
$("body").addClass("context-menu-shown");
|
$("body").addClass("context-menu-shown");
|
||||||
|
|
||||||
this.$widget.empty();
|
this.$widget.empty();
|
||||||
@@ -141,14 +135,16 @@ class ContextMenu {
|
|||||||
} else {
|
} else {
|
||||||
left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
|
left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
|
||||||
}
|
}
|
||||||
} else if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
|
|
||||||
// Overflow: right
|
|
||||||
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
|
|
||||||
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
|
|
||||||
// Overflow: left
|
|
||||||
left = CONTEXT_MENU_PADDING;
|
|
||||||
} else {
|
} else {
|
||||||
left = this.options.x - CONTEXT_MENU_OFFSET;
|
if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
|
||||||
|
// Overflow: right
|
||||||
|
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
|
||||||
|
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
|
||||||
|
// Overflow: left
|
||||||
|
left = CONTEXT_MENU_PADDING;
|
||||||
|
} else {
|
||||||
|
left = this.options.x - CONTEXT_MENU_OFFSET;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$widget
|
this.$widget
|
||||||
@@ -164,19 +160,16 @@ class ContextMenu {
|
|||||||
let $group = $parent; // The current group or parent element to which items are being appended
|
let $group = $parent; // The current group or parent element to which items are being appended
|
||||||
let shouldStartNewGroup = false; // If true, the next item will start a new group
|
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
|
let shouldResetGroup = false; // If true, the next item will be the last one from the group
|
||||||
let prevItemKind: string = "";
|
|
||||||
|
|
||||||
for (let index = 0; index < items.length; index++) {
|
for (let index = 0; index < items.length; index++) {
|
||||||
const item = items[index];
|
const item = items[index];
|
||||||
const itemKind = ("kind" in item) ? item.kind : "";
|
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current item is a header, start a new group. This group will contain the
|
// If the current item is a header, start a new group. This group will contain the
|
||||||
// header and the next item that follows the header.
|
// header and the next item that follows the header.
|
||||||
if (itemKind === "header") {
|
if ("kind" in item && item.kind === "header") {
|
||||||
if (multicolumn && !shouldResetGroup) {
|
if (multicolumn && !shouldResetGroup) {
|
||||||
shouldStartNewGroup = true;
|
shouldStartNewGroup = true;
|
||||||
}
|
}
|
||||||
@@ -202,25 +195,125 @@ class ContextMenu {
|
|||||||
shouldStartNewGroup = false;
|
shouldStartNewGroup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemKind === "separator") {
|
if ("kind" in item && item.kind === "separator") {
|
||||||
if (prevItemKind === "separator") {
|
|
||||||
// Skip consecutive separators
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$group.append($("<div>").addClass("dropdown-divider"));
|
$group.append($("<div>").addClass("dropdown-divider"));
|
||||||
shouldResetGroup = true; // End the group after the next item
|
shouldResetGroup = true; // End the group after the next item
|
||||||
} else if (itemKind === "header") {
|
} else if ("kind" in item && item.kind === "header") {
|
||||||
$group.append($("<h6>").addClass("dropdown-header").text((item as MenuHeader).title));
|
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
||||||
shouldResetGroup = true;
|
shouldResetGroup = true;
|
||||||
} else {
|
} else {
|
||||||
if (itemKind === "custom") {
|
const $icon = $("<span>");
|
||||||
// Custom menu item
|
|
||||||
$group.append(this.createCustomMenuItem(item as CustomMenuItem));
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
} else {
|
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||||
// Standard menu item
|
if (icon) {
|
||||||
$group.append(this.createMenuItem(item as MenuCommandItem<any>));
|
$icon.addClass(icon);
|
||||||
|
} else {
|
||||||
|
$icon.append(" ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const $link = $("<span>")
|
||||||
|
.append($icon)
|
||||||
|
.append(" ") // some space between icon and text
|
||||||
|
.append(item.title);
|
||||||
|
|
||||||
|
if ("badges" in item && item.badges) {
|
||||||
|
for (let badge of item.badges) {
|
||||||
|
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||||
|
|
||||||
|
if (badge.className) {
|
||||||
|
badgeElement.addClass(badge.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
$link.append(badgeElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
const $item = $("<li>")
|
||||||
|
.addClass("dropdown-item")
|
||||||
|
.append($link)
|
||||||
|
.on("contextmenu", (e) => false)
|
||||||
|
// important to use mousedown instead of click since the former does not change focus
|
||||||
|
// (especially important for focused text for spell check)
|
||||||
|
.on("mousedown", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.which !== 1) {
|
||||||
|
// only left click triggers menu items
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMobile && "items" in item && item.items) {
|
||||||
|
const $item = $(e.target).closest(".dropdown-item");
|
||||||
|
|
||||||
|
$item.toggleClass("submenu-open");
|
||||||
|
$item.find("ul.dropdown-menu").toggleClass("show");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("handler" in item && item.handler) {
|
||||||
|
item.handler(item, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options?.selectMenuItemHandler(item, e);
|
||||||
|
|
||||||
|
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
||||||
|
// might be handled again by top-level menu
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$item.on("mouseup", (e) => {
|
||||||
|
// Prevent submenu from failing to expand on mobile
|
||||||
|
if (!this.isMobile || !("items" in item && item.items)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
||||||
|
this.hide();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||||
|
$item.addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("items" in item && item.items) {
|
||||||
|
$item.addClass("dropdown-submenu");
|
||||||
|
$link.addClass("dropdown-toggle");
|
||||||
|
|
||||||
|
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||||
|
const hasColumns = !!item.columns && item.columns > 1;
|
||||||
|
if (!this.isMobile && hasColumns) {
|
||||||
|
$subMenu.css("column-count", item.columns!);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addItems($subMenu, item.items, hasColumns);
|
||||||
|
|
||||||
|
$item.append($subMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
$group.append($item);
|
||||||
|
|
||||||
// After adding a menu item, if the previous item was a separator or header,
|
// 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.
|
// reset the group so that the next item will be appended directly to the parent.
|
||||||
if (shouldResetGroup) {
|
if (shouldResetGroup) {
|
||||||
@@ -228,130 +321,13 @@ class ContextMenu {
|
|||||||
shouldResetGroup = false;
|
shouldResetGroup = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
prevItemKind = itemKind;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createCustomMenuItem(item: CustomMenuItem) {
|
|
||||||
const element = document.createElement("li");
|
|
||||||
element.classList.add("dropdown-custom-item");
|
|
||||||
element.onclick = () => this.hide();
|
|
||||||
render(h(item.componentFn, {}), element);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createMenuItem(item: MenuCommandItem<any>) {
|
|
||||||
const $icon = $("<span>");
|
|
||||||
|
|
||||||
if ("uiIcon" in item || "checked" in item) {
|
|
||||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
|
||||||
if (icon) {
|
|
||||||
$icon.addClass([icon, "tn-icon"]);
|
|
||||||
} else {
|
|
||||||
$icon.append(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const $link = $("<span>")
|
|
||||||
.append($icon)
|
|
||||||
.append(" ") // some space between icon and text
|
|
||||||
.append(item.title);
|
|
||||||
|
|
||||||
if ("badges" in item && item.badges) {
|
|
||||||
for (const badge of item.badges) {
|
|
||||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
|
||||||
|
|
||||||
if (badge.className) {
|
|
||||||
badgeElement.addClass(badge.className);
|
|
||||||
}
|
|
||||||
|
|
||||||
$link.append(badgeElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
const $item = $("<li>")
|
|
||||||
.addClass("dropdown-item")
|
|
||||||
.append($link)
|
|
||||||
.on("contextmenu", (e) => false)
|
|
||||||
// important to use mousedown instead of click since the former does not change focus
|
|
||||||
// (especially important for focused text for spell check)
|
|
||||||
.on("mousedown", (e) => {
|
|
||||||
if (e.which !== 1) {
|
|
||||||
// only left click triggers menu items
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMobile && "items" in item && item.items) {
|
|
||||||
const $item = $(e.target).closest(".dropdown-item");
|
|
||||||
|
|
||||||
$item.toggleClass("submenu-open");
|
|
||||||
$item.find("ul.dropdown-menu").toggleClass("show");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent submenu from failing to expand on mobile
|
|
||||||
if (!("items" in item && item.items)) {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("handler" in item && item.handler) {
|
|
||||||
item.handler(item, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options?.selectMenuItemHandler(item, e);
|
|
||||||
|
|
||||||
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
|
||||||
// might be handled again by top-level menu
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
|
||||||
$item.addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("items" in item && item.items) {
|
|
||||||
$item.addClass("dropdown-submenu");
|
|
||||||
$link.addClass("dropdown-toggle");
|
|
||||||
|
|
||||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
|
||||||
const hasColumns = !!item.columns && item.columns > 1;
|
|
||||||
if (!this.isMobile && hasColumns) {
|
|
||||||
$subMenu.css("column-count", item.columns!);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addItems($subMenu, item.items, hasColumns);
|
|
||||||
|
|
||||||
$item.append($subMenu);
|
|
||||||
}
|
|
||||||
return $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
async hide() {
|
async hide() {
|
||||||
this.options?.onHide?.();
|
this.options?.onHide?.();
|
||||||
this.$widget.removeClass("show");
|
this.$widget.removeClass("show");
|
||||||
this.$cover?.removeClass("show");
|
this.$cover.removeClass("show");
|
||||||
$("body").removeClass("context-menu-shown");
|
$("body").removeClass("context-menu-shown");
|
||||||
this.$widget.hide();
|
this.$widget.hide();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { t } from "../services/i18n"
|
|
||||||
import attributes from "../services/attributes"
|
|
||||||
import FNote from "../entities/fnote"
|
|
||||||
|
|
||||||
export function getArchiveMenuItem(note: FNote) {
|
|
||||||
if (!note.isArchived) {
|
|
||||||
return {
|
|
||||||
title: t("board_view.archive-note"),
|
|
||||||
uiIcon: "bx bx-archive",
|
|
||||||
handler: () => attributes.addLabel(note.noteId, "archived")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: t("board_view.unarchive-note"),
|
|
||||||
uiIcon: "bx bx-archive-out",
|
|
||||||
handler: async () => {
|
|
||||||
attributes.removeOwnedLabelByName(note, "archived")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
:root {
|
|
||||||
--note-color-picker-clear-color-cell-background: var(--primary-button-background-color);
|
|
||||||
--note-color-picker-clear-color-cell-color: var(--main-background-color);
|
|
||||||
--note-color-picker-clear-color-cell-selection-outline-color: var(--primary-button-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell {
|
|
||||||
--color-picker-cell-size: 14px;
|
|
||||||
|
|
||||||
width: var(--color-picker-cell-size);
|
|
||||||
height: var(--color-picker-cell-size);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell:not(.selected):hover {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell.disabled-color-cell {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell.selected {
|
|
||||||
outline: 2px solid var(--outline-color, var(--color));
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* RESET COLOR CELL
|
|
||||||
*/
|
|
||||||
|
|
||||||
.note-color-picker .color-cell-reset {
|
|
||||||
--color: var(--note-color-picker-clear-color-cell-background);
|
|
||||||
--outline-color: var(--note-color-picker-clear-color-cell-selection-outline-color);
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell-reset svg {
|
|
||||||
width: var(--color-picker-cell-size);
|
|
||||||
height: var(--color-picker-cell-size);
|
|
||||||
fill: var(--note-color-picker-clear-color-cell-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* CUSTOM COLOR CELL
|
|
||||||
*/
|
|
||||||
|
|
||||||
.note-color-picker .custom-color-cell::before {
|
|
||||||
position: absolute;
|
|
||||||
content: "\ed35";
|
|
||||||
display: flex;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
font-size: calc(var(--color-picker-cell-size) * 1.3);
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-family: boxicons;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .custom-color-cell {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .custom-color-cell.custom-color-cell-empty {
|
|
||||||
background-image: url(./custom-color.png);
|
|
||||||
background-size: cover;
|
|
||||||
--foreground: transparent;
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import "./NoteColorPicker.css";
|
|
||||||
import { t } from "../../services/i18n";
|
|
||||||
import { useCallback, useEffect, useRef, useState} from "preact/hooks";
|
|
||||||
import {ComponentChildren} from "preact";
|
|
||||||
import attributes from "../../services/attributes";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import Color, { ColorInstance } from "color";
|
|
||||||
import Debouncer from "../../utils/debouncer";
|
|
||||||
import FNote from "../../entities/fnote";
|
|
||||||
import froca from "../../services/froca";
|
|
||||||
import { isMobile } from "../../services/utils";
|
|
||||||
|
|
||||||
const COLOR_PALETTE = [
|
|
||||||
"#e64d4d", "#e6994d", "#e5e64d", "#99e64d", "#4de64d", "#4de699",
|
|
||||||
"#4de5e6", "#4d99e6", "#4d4de6", "#994de6", "#e64db3"
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface NoteColorPickerProps {
|
|
||||||
/** The target Note instance or its ID string. */
|
|
||||||
note: FNote | string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NoteColorPicker(props: NoteColorPickerProps) {
|
|
||||||
if (!props.note) return null;
|
|
||||||
|
|
||||||
const [note, setNote] = useState<FNote | null>(null);
|
|
||||||
const [currentColor, setCurrentColor] = useState<string | null>(null);
|
|
||||||
const [isCustomColor, setIsCustomColor] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const retrieveNote = async (noteId: string) => {
|
|
||||||
const noteInstance = await froca.getNote(noteId, true);
|
|
||||||
if (noteInstance) {
|
|
||||||
setNote(noteInstance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof props.note === "string") {
|
|
||||||
retrieveNote(props.note); // Get the note from the given ID string
|
|
||||||
} else {
|
|
||||||
setNote(props.note);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const colorLabel = note?.getLabel("color")?.value ?? null;
|
|
||||||
if (colorLabel) {
|
|
||||||
let color = tryParseColor(colorLabel);
|
|
||||||
if (color) {
|
|
||||||
setCurrentColor(color.hex().toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [note]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsCustomColor(currentColor !== null && COLOR_PALETTE.indexOf(currentColor) === -1);
|
|
||||||
}, [currentColor])
|
|
||||||
|
|
||||||
const onColorCellClicked = useCallback((color: string | null) => {
|
|
||||||
if (note) {
|
|
||||||
if (color !== null) {
|
|
||||||
attributes.setLabel(note.noteId, "color", color);
|
|
||||||
} else {
|
|
||||||
attributes.removeOwnedLabelByName(note, "color");
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentColor(color);
|
|
||||||
}
|
|
||||||
}, [note, currentColor]);
|
|
||||||
|
|
||||||
return <div className="note-color-picker">
|
|
||||||
|
|
||||||
<ColorCell className="color-cell-reset"
|
|
||||||
tooltip={t("note-color.clear-color")}
|
|
||||||
color={null}
|
|
||||||
isSelected={(currentColor === null)}
|
|
||||||
isDisabled={(note === null)}
|
|
||||||
onSelect={onColorCellClicked}>
|
|
||||||
|
|
||||||
{/* https://pictogrammers.com/library/mdi/icon/close/ */}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
|
||||||
</svg>
|
|
||||||
</ColorCell>
|
|
||||||
|
|
||||||
|
|
||||||
{COLOR_PALETTE.map((color) => (
|
|
||||||
<ColorCell key={color}
|
|
||||||
tooltip={t("note-color.set-color")}
|
|
||||||
color={color}
|
|
||||||
isSelected={(color === currentColor)}
|
|
||||||
isDisabled={(note === null)}
|
|
||||||
onSelect={onColorCellClicked} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<CustomColorCell tooltip={t("note-color.set-custom-color")}
|
|
||||||
color={currentColor}
|
|
||||||
isSelected={isCustomColor}
|
|
||||||
isDisabled={(note === null)}
|
|
||||||
onSelect={onColorCellClicked} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColorCellProps {
|
|
||||||
children?: ComponentChildren,
|
|
||||||
className?: string,
|
|
||||||
tooltip?: string,
|
|
||||||
color: string | null,
|
|
||||||
isSelected: boolean,
|
|
||||||
isDisabled?: boolean,
|
|
||||||
onSelect?: (color: string | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColorCell(props: ColorCellProps) {
|
|
||||||
return <div className={clsx(props.className, {
|
|
||||||
"color-cell": true,
|
|
||||||
"selected": props.isSelected,
|
|
||||||
"disabled-color-cell": props.isDisabled
|
|
||||||
})}
|
|
||||||
style={`${(props.color !== null) ? `--color: ${props.color}` : ""}`}
|
|
||||||
title={props.tooltip}
|
|
||||||
onClick={() => props.onSelect?.(props.color)}>
|
|
||||||
{props.children}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CustomColorCell(props: ColorCellProps) {
|
|
||||||
const [pickedColor, setPickedColor] = useState<string | null>(null);
|
|
||||||
const colorInput = useRef<HTMLInputElement>(null);
|
|
||||||
const colorInputDebouncer = useRef<Debouncer<string | null> | null>(null);
|
|
||||||
const callbackRef = useRef(props.onSelect);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
colorInputDebouncer.current = new Debouncer(250, (color) => {
|
|
||||||
callbackRef.current?.(color);
|
|
||||||
setPickedColor(color);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
colorInputDebouncer.current?.destroy();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.isSelected && pickedColor === null) {
|
|
||||||
setPickedColor(props.color);
|
|
||||||
}
|
|
||||||
}, [props.isSelected])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
callbackRef.current = props.onSelect;
|
|
||||||
}, [props.onSelect]);
|
|
||||||
|
|
||||||
const onSelect = useCallback(() => {
|
|
||||||
if (pickedColor !== null) {
|
|
||||||
callbackRef.current?.(pickedColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
colorInput.current?.click();
|
|
||||||
}, [pickedColor]);
|
|
||||||
|
|
||||||
return <div style={`--foreground: ${getForegroundColor(props.color)};`}
|
|
||||||
onClick={isMobile() ? (e) => {
|
|
||||||
// The color picker dropdown will close on some browser if the parent context menu is
|
|
||||||
// dismissed, so stop the click propagation to prevent dismissing the menu.
|
|
||||||
e.stopPropagation();
|
|
||||||
} : undefined}>
|
|
||||||
<ColorCell {...props}
|
|
||||||
color={pickedColor}
|
|
||||||
className={clsx("custom-color-cell", {
|
|
||||||
"custom-color-cell-empty": (pickedColor === null)
|
|
||||||
})}
|
|
||||||
onSelect={onSelect}>
|
|
||||||
|
|
||||||
<input ref={colorInput}
|
|
||||||
type="color"
|
|
||||||
value={pickedColor ?? props.color ?? "#40bfbf"}
|
|
||||||
onChange={() => {colorInputDebouncer.current?.updateValue(colorInput.current?.value ?? null)}}
|
|
||||||
style="width: 0; height: 0; opacity: 0" />
|
|
||||||
</ColorCell>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function getForegroundColor(backgroundColor: string | null) {
|
|
||||||
if (backgroundColor === null) return "inherit";
|
|
||||||
|
|
||||||
const colorHsl = tryParseColor(backgroundColor)?.hsl();
|
|
||||||
if (colorHsl) {
|
|
||||||
let l = colorHsl.lightness();
|
|
||||||
return colorHsl.saturationl(0).lightness(l >= 50 ? 0 : 100).hex();
|
|
||||||
} else {
|
|
||||||
return "inherit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseColor(colorStr: string): ColorInstance | null {
|
|
||||||
try {
|
|
||||||
return new Color(colorStr);
|
|
||||||
} catch(ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -3,10 +3,8 @@ import options from "../services/options.js";
|
|||||||
import zoomService from "../components/zoom.js";
|
import zoomService from "../components/zoom.js";
|
||||||
import contextMenu, { type MenuItem } from "./context_menu.js";
|
import contextMenu, { type MenuItem } from "./context_menu.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import server from "../services/server.js";
|
|
||||||
import * as clipboardExt from "../services/clipboard_ext.js";
|
|
||||||
import type { BrowserWindow } from "electron";
|
import type { BrowserWindow } from "electron";
|
||||||
import type { CommandNames, AppContext } from "../components/app_context.js";
|
import type { CommandNames } from "../components/app_context.js";
|
||||||
|
|
||||||
function setupContextMenu() {
|
function setupContextMenu() {
|
||||||
const electron = utils.dynamicRequire("electron");
|
const electron = utils.dynamicRequire("electron");
|
||||||
@@ -15,8 +13,6 @@ function setupContextMenu() {
|
|||||||
// FIXME: Remove typecast once Electron is properly integrated.
|
// FIXME: Remove typecast once Electron is properly integrated.
|
||||||
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
||||||
|
|
||||||
let appContext: AppContext;
|
|
||||||
|
|
||||||
webContents.on("context-menu", (event, params) => {
|
webContents.on("context-menu", (event, params) => {
|
||||||
const { editFlags } = params;
|
const { editFlags } = params;
|
||||||
const hasText = params.selectionText.trim().length > 0;
|
const hasText = params.selectionText.trim().length > 0;
|
||||||
@@ -62,33 +58,6 @@ function setupContextMenu() {
|
|||||||
uiIcon: "bx bx-copy",
|
uiIcon: "bx bx-copy",
|
||||||
handler: () => webContents.copy()
|
handler: () => webContents.copy()
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push({
|
|
||||||
enabled: hasText,
|
|
||||||
title: t("electron_context_menu.copy-as-markdown"),
|
|
||||||
uiIcon: "bx bx-copy-alt",
|
|
||||||
handler: async () => {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || !selection.rangeCount) return '';
|
|
||||||
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.appendChild(range.cloneContents());
|
|
||||||
|
|
||||||
const htmlContent = div.innerHTML;
|
|
||||||
if (htmlContent) {
|
|
||||||
try {
|
|
||||||
const { markdownContent } = await server.post<{ markdownContent: string }>(
|
|
||||||
"other/to-markdown",
|
|
||||||
{ htmlContent }
|
|
||||||
);
|
|
||||||
await clipboardExt.copyTextWithToast(markdownContent);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to copy as markdown:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") {
|
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") {
|
||||||
@@ -150,20 +119,6 @@ function setupContextMenu() {
|
|||||||
uiIcon: "bx bx-search-alt",
|
uiIcon: "bx bx-search-alt",
|
||||||
handler: () => electron.shell.openExternal(searchUrl)
|
handler: () => electron.shell.openExternal(searchUrl)
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push({
|
|
||||||
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
|
|
||||||
uiIcon: "bx bx-search",
|
|
||||||
handler: async () => {
|
|
||||||
if (!appContext) {
|
|
||||||
appContext = (await import("../components/app_context.js")).default;
|
|
||||||
}
|
|
||||||
|
|
||||||
await appContext.triggerCommand("searchNotes", {
|
|
||||||
searchString: params.selectionText
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js";
|
|
||||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
|
||||||
import dialogService from "../services/dialog.js";
|
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import treeService from "../services/tree.js";
|
import treeService from "../services/tree.js";
|
||||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
import froca from "../services/froca.js";
|
||||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||||
|
import dialogService from "../services/dialog.js";
|
||||||
|
import server from "../services/server.js";
|
||||||
|
import { t } from "../services/i18n.js";
|
||||||
|
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||||
|
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
|
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
|
||||||
|
|
||||||
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
|
|||||||
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
|
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
|
||||||
const parentNoteId = this.node.getParent().data.noteId;
|
const parentNoteId = this.node.getParent().data.noteId;
|
||||||
|
|
||||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers";
|
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
|
||||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers";
|
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
|
||||||
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
|
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
|
||||||
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
|
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
|
||||||
const isItem = isVisibleItem || isAvailableItem;
|
const isItem = isVisibleItem || isAvailableItem;
|
||||||
|
|||||||
@@ -1,67 +1,49 @@
|
|||||||
import type { LeafletMouseEvent } from "leaflet";
|
|
||||||
|
|
||||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import type { ViewScope } from "../services/link.js";
|
|
||||||
import utils, { isMobile } from "../services/utils.js";
|
|
||||||
import { getClosestNtxId } from "../widgets/widget_utils.js";
|
|
||||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||||
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
|
import type { ViewScope } from "../services/link.js";
|
||||||
|
|
||||||
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
contextMenu.show({
|
contextMenu.show({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: getItems(e),
|
items: getItems(),
|
||||||
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, e, notePath, viewScope, hoistedNoteId)
|
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
|
function getItems(): MenuItem<CommandNames>[] {
|
||||||
const ntxId = getNtxId(e);
|
|
||||||
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||||
{ title: !isMobileSplitOpen ? t("link_context_menu.open_note_in_new_split") : t("link_context_menu.open_note_in_other_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
||||||
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
if (!hoistedNoteId) {
|
if (!hoistedNoteId) {
|
||||||
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === "openNoteInNewTab") {
|
if (command === "openNoteInNewTab") {
|
||||||
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
||||||
return true;
|
|
||||||
} else if (command === "openNoteInNewSplit") {
|
} else if (command === "openNoteInNewSplit") {
|
||||||
const ntxId = getNtxId(e);
|
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||||
if (!ntxId) return false;
|
|
||||||
|
if (!subContexts) {
|
||||||
|
logError("subContexts is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ntxId } = subContexts[subContexts.length - 1];
|
||||||
|
|
||||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||||
return true;
|
|
||||||
} else if (command === "openNoteInNewWindow") {
|
} else if (command === "openNoteInNewWindow") {
|
||||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||||
return true;
|
|
||||||
} else if (command === "openNoteInPopup") {
|
} else if (command === "openNoteInPopup") {
|
||||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
|
|
||||||
if (utils.isDesktop()) {
|
|
||||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
|
||||||
if (!subContexts) return null;
|
|
||||||
return subContexts[subContexts.length - 1].ntxId;
|
|
||||||
} else if (e.target instanceof HTMLElement) {
|
|
||||||
return getClosestNtxId(e.target);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
|
import treeService from "../services/tree.js";
|
||||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
|
||||||
import type FAttachment from "../entities/fattachment.js";
|
|
||||||
import attributes from "../services/attributes.js";
|
|
||||||
import { executeBulkActions } from "../services/bulk_action.js";
|
|
||||||
import clipboard from "../services/clipboard.js";
|
|
||||||
import dialogService from "../services/dialog.js";
|
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import { t } from "../services/i18n.js";
|
import clipboard from "../services/clipboard.js";
|
||||||
import noteCreateService from "../services/note_create.js";
|
import noteCreateService from "../services/note_create.js";
|
||||||
|
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||||
|
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
|
||||||
import noteTypesService from "../services/note_types.js";
|
import noteTypesService from "../services/note_types.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import toastService from "../services/toast.js";
|
import toastService from "../services/toast.js";
|
||||||
import treeService from "../services/tree.js";
|
import dialogService from "../services/dialog.js";
|
||||||
import utils from "../services/utils.js";
|
import { t } from "../services/i18n.js";
|
||||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
import type FAttachment from "../entities/fattachment.js";
|
||||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
import type { SelectMenuItemEventListener } from "../components/events.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 {
|
||||||
@@ -72,8 +71,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
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";
|
||||||
const hasSubtreeHidden = note?.isLabelTruthy("subtreeHidden") ?? false;
|
|
||||||
const isSpotlighted = this.node.extraClasses.includes("spotlighted-node");
|
|
||||||
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
|
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
|
||||||
const parentNotSearch = !parentNote || parentNote.type !== "search";
|
const parentNotSearch = !parentNote || parentNote.type !== "search";
|
||||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||||
@@ -81,18 +78,17 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
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", shortcut: "Ctrl+Click", 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-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", 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")}`,
|
||||||
command: "toggleNoteHoisting",
|
command: "toggleNoteHoisting",
|
||||||
keyboardShortcut: "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"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||||
@@ -115,7 +111,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
keyboardShortcut: "createNoteInto",
|
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 && !hasSubtreeHidden && !isSpotlighted,
|
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||||
columns: 2
|
columns: 2
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -143,27 +139,12 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
uiIcon: "bx bx-rename",
|
uiIcon: "bx bx-rename",
|
||||||
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
enabled: isNotRoot && parentNotSearch && 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 && selectedNotes.some(note => note.isEligibleForConversionToAttachment())
|
|
||||||
},
|
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
|
||||||
!hasSubtreeHidden && { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||||
!hasSubtreeHidden && { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||||
{
|
|
||||||
title: hasSubtreeHidden ? t("tree-context-menu.show-subtree") : t("tree-context-menu.hide-subtree"),
|
|
||||||
uiIcon: "bx bx-show",
|
|
||||||
handler: async () => {
|
|
||||||
const note = await froca.getNote(this.node.data.noteId);
|
|
||||||
if (!note) return;
|
|
||||||
attributes.setBooleanWithInheritance(note, "subtreeHidden", !hasSubtreeHidden);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("tree-context-menu.sort-by"),
|
title: t("tree-context-menu.sort-by"),
|
||||||
command: "sortChildNotes",
|
command: "sortChildNotes",
|
||||||
@@ -176,7 +157,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
|
|
||||||
{ 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 }
|
||||||
].filter(Boolean) as MenuItem<TreeCommandNames>[]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
@@ -260,15 +241,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
|
|
||||||
{ kind: "separator"},
|
|
||||||
|
|
||||||
(notOptionsOrHelp && selectedNotes.length === 1) ? {
|
|
||||||
kind: "custom",
|
|
||||||
componentFn: () => {
|
|
||||||
return NoteColorPicker({note});
|
|
||||||
}
|
|
||||||
} : null,
|
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
|
||||||
{ 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 },
|
||||||
@@ -304,30 +276,25 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNote(parentNotePath, {
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: this.node.data.branchId,
|
targetBranchId: this.node.data.branchId,
|
||||||
type,
|
type: type,
|
||||||
isProtected,
|
isProtected: isProtected,
|
||||||
templateNoteId
|
templateNoteId: templateNoteId
|
||||||
});
|
});
|
||||||
} else if (command === "insertChildNote") {
|
} else if (command === "insertChildNote") {
|
||||||
const parentNotePath = treeService.getNotePath(this.node);
|
const parentNotePath = treeService.getNotePath(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNote(parentNotePath, {
|
||||||
type,
|
type: type,
|
||||||
isProtected: this.node.data.isProtected,
|
isProtected: this.node.data.isProtected,
|
||||||
templateNoteId
|
templateNoteId: templateNoteId
|
||||||
});
|
});
|
||||||
} else if (command === "openNoteInSplit") {
|
} else if (command === "openNoteInSplit") {
|
||||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||||
|
|
||||||
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
||||||
} else if (command === "openNoteInWindow") {
|
|
||||||
appContext.triggerCommand("openInWindow", {
|
|
||||||
notePath,
|
|
||||||
hoistedNoteId: appContext.tabManager.getActiveContext()?.hoistedNoteId
|
|
||||||
});
|
|
||||||
} else if (command === "openNoteInPopup") {
|
} else if (command === "openNoteInPopup") {
|
||||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||||
} else if (command === "convertNoteToAttachment") {
|
} else if (command === "convertNoteToAttachment") {
|
||||||
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
||||||
return;
|
return;
|
||||||
@@ -349,11 +316,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
|
|
||||||
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
|
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
|
||||||
} else if (command === "copyNotePathToClipboard") {
|
} else if (command === "copyNotePathToClipboard") {
|
||||||
navigator.clipboard.writeText(`#${ notePath}`);
|
navigator.clipboard.writeText("#" + notePath);
|
||||||
} else if (command) {
|
} else if (command) {
|
||||||
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
|
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
|
||||||
node: this.node,
|
node: this.node,
|
||||||
notePath,
|
notePath: notePath,
|
||||||
noteId: this.node.data.noteId,
|
noteId: this.node.data.noteId,
|
||||||
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
|
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
|
||||||
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
|
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "autocomplete.js/index_jquery.js";
|
|
||||||
|
|
||||||
import appContext from "./components/app_context.js";
|
import appContext from "./components/app_context.js";
|
||||||
import glob from "./services/glob.js";
|
|
||||||
import noteAutocompleteService from "./services/note_autocomplete.js";
|
import noteAutocompleteService from "./services/note_autocomplete.js";
|
||||||
|
import glob from "./services/glob.js";
|
||||||
|
import "boxicons/css/boxicons.min.css";
|
||||||
|
import "autocomplete.js/index_jquery.js";
|
||||||
|
|
||||||
glob.setupGlobs();
|
glob.setupGlobs();
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,14 @@
|
|||||||
import { render } from "preact";
|
|
||||||
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
|
||||||
|
|
||||||
import FNote from "./entities/fnote";
|
import FNote from "./entities/fnote";
|
||||||
|
import { render } from "preact";
|
||||||
|
import { CustomNoteList } from "./widgets/collections/NoteList";
|
||||||
|
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
||||||
import content_renderer from "./services/content_renderer";
|
import content_renderer from "./services/content_renderer";
|
||||||
import { applyInlineMermaid } from "./services/content_renderer_text";
|
|
||||||
import { dynamicRequire, isElectron } from "./services/utils";
|
|
||||||
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
|
|
||||||
|
|
||||||
interface RendererProps {
|
interface RendererProps {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
onReady: (data: PrintReport) => void;
|
onReady: () => void;
|
||||||
onProgressChanged?: (progress: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PrintReport = {
|
|
||||||
type: "single-note";
|
|
||||||
} | {
|
|
||||||
type: "collection";
|
|
||||||
ignoredNoteIds: string[];
|
|
||||||
} | {
|
|
||||||
type: "error";
|
|
||||||
message: string;
|
|
||||||
stack?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const notePath = window.location.hash.substring(1);
|
const notePath = window.location.hash.substring(1);
|
||||||
const noteId = notePath.split("/").at(-1);
|
const noteId = notePath.split("/").at(-1);
|
||||||
@@ -33,32 +18,20 @@ async function main() {
|
|||||||
const froca = (await import("./services/froca")).default;
|
const froca = (await import("./services/froca")).default;
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
|
|
||||||
const bodyWrapper = document.createElement("div");
|
render(<App note={note} noteId={noteId} />, document.body);
|
||||||
render(<App note={note} noteId={noteId} />, bodyWrapper);
|
|
||||||
document.body.appendChild(bodyWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||||
const sentReadyEvent = useRef(false);
|
const sentReadyEvent = useRef(false);
|
||||||
const onProgressChanged = useCallback((progress: number) => {
|
const onReady = useCallback(() => {
|
||||||
if (isElectron()) {
|
|
||||||
const { ipcRenderer } = dynamicRequire('electron');
|
|
||||||
ipcRenderer.send("print-progress", progress);
|
|
||||||
} else {
|
|
||||||
window.dispatchEvent(new CustomEvent("note-load-progress", { detail: { progress } }));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const onReady = useCallback((printReport: PrintReport) => {
|
|
||||||
if (sentReadyEvent.current) return;
|
if (sentReadyEvent.current) return;
|
||||||
window.dispatchEvent(new CustomEvent("note-ready", {
|
window.dispatchEvent(new Event("note-ready"));
|
||||||
detail: printReport
|
window._noteReady = true;
|
||||||
}));
|
|
||||||
window._noteReady = printReport;
|
|
||||||
sentReadyEvent.current = true;
|
sentReadyEvent.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
const props: RendererProps | undefined | null = note && { note, onReady, onProgressChanged };
|
const props: RendererProps | undefined | null = note && { note, onReady };
|
||||||
|
|
||||||
if (!note || !props) return <Error404 noteId={noteId} />;
|
if (!note || !props) return <Error404 noteId={noteId} />
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
document.body.dataset.noteType = note.type;
|
document.body.dataset.noteType = note.type;
|
||||||
@@ -67,8 +40,8 @@ function App({ note, noteId }: { note: FNote | null | undefined, noteId: string
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{note.type === "book"
|
{note.type === "book"
|
||||||
? <CollectionRenderer {...props} />
|
? <CollectionRenderer {...props} />
|
||||||
: <SingleNoteRenderer {...props} />
|
: <SingleNoteRenderer {...props} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -98,18 +71,11 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize mermaid.
|
|
||||||
if (note.type === "text") {
|
|
||||||
await applyInlineMermaid(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check custom CSS.
|
// Check custom CSS.
|
||||||
await loadCustomCss(note);
|
await loadCustomCss(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
load().then(() => requestAnimationFrame(() => onReady({
|
load().then(() => requestAnimationFrame(onReady))
|
||||||
type: "single-note"
|
|
||||||
})));
|
|
||||||
}, [ note ]);
|
}, [ note ]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
@@ -118,21 +84,18 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
|||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps) {
|
function CollectionRenderer({ note, onReady }: RendererProps) {
|
||||||
const viewType = useNoteViewType(note);
|
|
||||||
return <CustomNoteList
|
return <CustomNoteList
|
||||||
viewType={viewType}
|
|
||||||
isEnabled
|
isEnabled
|
||||||
note={note}
|
note={note}
|
||||||
notePath={note.getBestNotePath().join("/")}
|
notePath={note.getBestNotePath().join("/")}
|
||||||
ntxId="print"
|
ntxId="print"
|
||||||
highlightedTokens={null}
|
highlightedTokens={null}
|
||||||
media="print"
|
media="print"
|
||||||
onReady={async (data: PrintReport) => {
|
onReady={async () => {
|
||||||
await loadCustomCss(note);
|
await loadCustomCss(note);
|
||||||
onReady(data);
|
onReady();
|
||||||
}}
|
}}
|
||||||
onProgressChanged={onProgressChanged}
|
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +105,12 @@ function Error404({ noteId }: { noteId: string }) {
|
|||||||
<p>The note you are trying to print could not be found.</p>
|
<p>The note you are trying to print could not be found.</p>
|
||||||
<small>{noteId}</small>
|
<small>{noteId}</small>
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomCss(note: FNote) {
|
async function loadCustomCss(note: FNote) {
|
||||||
const printCssNotes = await note.getRelationTargets("printCss");
|
const printCssNotes = await note.getRelationTargets("printCss");
|
||||||
const loadPromises: JQueryPromise<void>[] = [];
|
let loadPromises: JQueryPromise<void>[] = [];
|
||||||
|
|
||||||
for (const printCssNote of printCssNotes) {
|
for (const printCssNote of printCssNotes) {
|
||||||
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
|
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
|
||||||
|
|||||||
@@ -8,17 +8,6 @@ async function loadBootstrap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Polyfill removed jQuery methods for autocomplete.js compatibility
|
|
||||||
($ as any).isArray = Array.isArray;
|
|
||||||
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
|
|
||||||
($ as any).isPlainObject = function(obj: any) {
|
|
||||||
if (obj == null || typeof obj !== 'object') { return false; }
|
|
||||||
const proto = Object.getPrototypeOf(obj);
|
|
||||||
if (proto === null) { return true; }
|
|
||||||
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
|
|
||||||
return typeof Ctor === 'function' && Ctor === Object;
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).$ = $;
|
(window as any).$ = $;
|
||||||
(window as any).jQuery = $;
|
(window as any).jQuery = $;
|
||||||
await loadBootstrap();
|
await loadBootstrap();
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import { buildNote } from "../test/easy-froca";
|
|
||||||
import { setBooleanWithInheritance } from "./attributes";
|
|
||||||
import froca from "./froca";
|
|
||||||
import server from "./server.js";
|
|
||||||
|
|
||||||
// Spy on server methods to track calls
|
|
||||||
// @ts-expect-error the generic typing is causing issues here
|
|
||||||
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
|
|
||||||
// @ts-expect-error the generic typing is causing issues here
|
|
||||||
server.remove = vi.fn(async <T> (url: string) => ({} as T));
|
|
||||||
|
|
||||||
describe("Set boolean with inheritance", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't call server if value matches directly", async () => {
|
|
||||||
const noteWithLabel = buildNote({
|
|
||||||
title: "New note",
|
|
||||||
"#foo": ""
|
|
||||||
});
|
|
||||||
const noteWithoutLabel = buildNote({
|
|
||||||
title: "New note"
|
|
||||||
});
|
|
||||||
|
|
||||||
await setBooleanWithInheritance(noteWithLabel, "foo", true);
|
|
||||||
await setBooleanWithInheritance(noteWithoutLabel, "foo", false);
|
|
||||||
expect(server.put).not.toHaveBeenCalled();
|
|
||||||
expect(server.remove).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets boolean normally without inheritance", async () => {
|
|
||||||
const standaloneNote = buildNote({
|
|
||||||
title: "New note"
|
|
||||||
});
|
|
||||||
|
|
||||||
await setBooleanWithInheritance(standaloneNote, "foo", true);
|
|
||||||
expect(server.put).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/set-attribute`, {
|
|
||||||
type: "label",
|
|
||||||
name: "foo",
|
|
||||||
value: "",
|
|
||||||
isInheritable: false
|
|
||||||
}, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes boolean normally without inheritance", async () => {
|
|
||||||
const standaloneNote = buildNote({
|
|
||||||
title: "New note",
|
|
||||||
"#foo": ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const attributeId = standaloneNote.getLabel("foo")!.attributeId;
|
|
||||||
await setBooleanWithInheritance(standaloneNote, "foo", false);
|
|
||||||
expect(server.remove).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/attributes/${attributeId}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't call server if value matches inherited", async () => {
|
|
||||||
const parentNote = buildNote({
|
|
||||||
title: "Parent note",
|
|
||||||
"#foo(inheritable)": "",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
title: "Child note"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
|
||||||
expect(childNote.isLabelTruthy("foo")).toBe(true);
|
|
||||||
await setBooleanWithInheritance(childNote, "foo", true);
|
|
||||||
expect(server.put).not.toHaveBeenCalled();
|
|
||||||
expect(server.remove).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("overrides boolean with inheritance", async () => {
|
|
||||||
const parentNote = buildNote({
|
|
||||||
title: "Parent note",
|
|
||||||
"#foo(inheritable)": "",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
title: "Child note"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
|
||||||
expect(childNote.isLabelTruthy("foo")).toBe(true);
|
|
||||||
await setBooleanWithInheritance(childNote, "foo", false);
|
|
||||||
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
|
||||||
type: "label",
|
|
||||||
name: "foo",
|
|
||||||
value: "false",
|
|
||||||
isInheritable: false
|
|
||||||
}, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("overrides boolean with inherited false", async () => {
|
|
||||||
const parentNote = buildNote({
|
|
||||||
title: "Parent note",
|
|
||||||
"#foo(inheritable)": "false",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
title: "Child note"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
|
||||||
expect(childNote.isLabelTruthy("foo")).toBe(false);
|
|
||||||
await setBooleanWithInheritance(childNote, "foo", true);
|
|
||||||
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
|
||||||
type: "label",
|
|
||||||
name: "foo",
|
|
||||||
value: "",
|
|
||||||
isInheritable: false
|
|
||||||
}, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("deletes override boolean with inherited false with already existing value", async () => {
|
|
||||||
const parentNote = buildNote({
|
|
||||||
title: "Parent note",
|
|
||||||
"#foo(inheritable)": "false",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
title: "Child note",
|
|
||||||
"#foo": "false",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
|
||||||
expect(childNote.isLabelTruthy("foo")).toBe(false);
|
|
||||||
await setBooleanWithInheritance(childNote, "foo", true);
|
|
||||||
expect(server.put).toBeCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
|
||||||
type: "label",
|
|
||||||
name: "foo",
|
|
||||||
value: "",
|
|
||||||
isInheritable: false
|
|
||||||
}, undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +1,27 @@
|
|||||||
import { AttributeType } from "@triliumnext/commons";
|
|
||||||
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import froca from "./froca.js";
|
|
||||||
import type { AttributeRow } from "./load_results.js";
|
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
|
import froca from "./froca.js";
|
||||||
|
import type FNote from "../entities/fnote.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`, {
|
||||||
type: "label",
|
type: "label",
|
||||||
name,
|
name: name,
|
||||||
value,
|
value: value,
|
||||||
isInheritable
|
isInheritable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false, componentId?: string) {
|
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||||
await server.put(`notes/${noteId}/set-attribute`, {
|
await server.put(`notes/${noteId}/set-attribute`, {
|
||||||
type: "label",
|
type: "label",
|
||||||
name,
|
name: name,
|
||||||
value,
|
value: value,
|
||||||
isInheritable,
|
|
||||||
}, componentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
|
||||||
await server.put(`notes/${noteId}/set-attribute`, {
|
|
||||||
type: "relation",
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
isInheritable
|
isInheritable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
|
|
||||||
* value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
|
|
||||||
* from the inherited value, an owned label will be created or updated to reflect the desired value.
|
|
||||||
*
|
|
||||||
* When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
|
|
||||||
*
|
|
||||||
* @param note the note on which to set the boolean label.
|
|
||||||
* @param labelName the name of the label to set.
|
|
||||||
* @param value the boolean value to set for the label.
|
|
||||||
*/
|
|
||||||
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
|
|
||||||
const actualValue = note.isLabelTruthy(labelName);
|
|
||||||
if (actualValue === value) return;
|
|
||||||
const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);
|
|
||||||
|
|
||||||
if (hasInheritedValue) {
|
|
||||||
if (value) {
|
|
||||||
setLabel(note.noteId, labelName, "");
|
|
||||||
} else {
|
|
||||||
// Label is inherited - override to false.
|
|
||||||
setLabel(note.noteId, labelName, "false");
|
|
||||||
}
|
|
||||||
} else if (value) {
|
|
||||||
setLabel(note.noteId, labelName, "");
|
|
||||||
} else {
|
|
||||||
removeOwnedLabelByName(note, labelName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeAttributeById(noteId: string, attributeId: string) {
|
async function removeAttributeById(noteId: string, attributeId: string) {
|
||||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||||
}
|
}
|
||||||
@@ -91,23 +51,6 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a relation identified by its name from the given note, if it exists. Note that the relation must be owned, i.e.
|
|
||||||
* it will not remove inherited attributes.
|
|
||||||
*
|
|
||||||
* @param note the note from which to remove the relation.
|
|
||||||
* @param relationName the name of the relation to remove.
|
|
||||||
* @returns `true` if an attribute was identified and removed, `false` otherwise.
|
|
||||||
*/
|
|
||||||
function removeOwnedRelationByName(note: FNote, relationName: string) {
|
|
||||||
const relation = note.getOwnedRelation(relationName);
|
|
||||||
if (relation) {
|
|
||||||
removeAttributeById(note.noteId, relation.attributeId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
||||||
* For an attribute with an empty value, pass an empty string instead.
|
* For an attribute with an empty value, pass an empty string instead.
|
||||||
@@ -117,15 +60,15 @@ function removeOwnedRelationByName(note: FNote, relationName: string) {
|
|||||||
* @param name the name of the attribute to set.
|
* @param name the name of the attribute to set.
|
||||||
* @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, componentId?: string) {
|
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
// Create or update the attribute.
|
// Create or update the attribute.
|
||||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }, componentId);
|
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||||
} else {
|
} else {
|
||||||
// Remove the attribute if it exists on the server but we don't define a value for it.
|
// Remove the attribute if it exists on the server but we don't define a value for it.
|
||||||
const attributeId = note.getAttribute(type, name)?.attributeId;
|
const attributeId = note.getAttribute(type, name)?.attributeId;
|
||||||
if (attributeId) {
|
if (attributeId) {
|
||||||
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`, componentId);
|
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +100,9 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrRow.isInheritable) {
|
// TODO: This doesn't seem right.
|
||||||
|
//@ts-ignore
|
||||||
|
if (this.isInheritable) {
|
||||||
for (const owningNote of owningNotes) {
|
for (const owningNote of owningNotes) {
|
||||||
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -168,59 +113,11 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`.
|
|
||||||
*
|
|
||||||
* Note that this work for non-dangerous attributes as well.
|
|
||||||
*
|
|
||||||
* If there are multiple attributes with the same name, all of them will be toggled at the same time.
|
|
||||||
*
|
|
||||||
* @param note the note whose attribute to change.
|
|
||||||
* @param type the type of dangerous attribute (label or relation).
|
|
||||||
* @param name the name of the dangerous attribute.
|
|
||||||
* @param willEnable whether to enable or disable the attribute.
|
|
||||||
* @returns a promise that will resolve when the request to the server completes.
|
|
||||||
*/
|
|
||||||
async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) {
|
|
||||||
const attrs = [
|
|
||||||
...note.getOwnedAttributes(type, name),
|
|
||||||
...note.getOwnedAttributes(type, `disabled:${name}`)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const attr of attrs) {
|
|
||||||
const baseName = getNameWithoutDangerousPrefix(attr.name);
|
|
||||||
const newName = willEnable ? baseName : `disabled:${baseName}`;
|
|
||||||
if (newName === attr.name) continue;
|
|
||||||
|
|
||||||
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
|
|
||||||
if (attr.type === "label") {
|
|
||||||
await setLabel(note.noteId, newName, attr.value);
|
|
||||||
} else {
|
|
||||||
await setRelation(note.noteId, newName, attr.value);
|
|
||||||
}
|
|
||||||
await removeAttributeById(note.noteId, attr.attributeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled.
|
|
||||||
* @param name the name of an attribute.
|
|
||||||
* @returns the name without the `disabled:` prefix.
|
|
||||||
*/
|
|
||||||
function getNameWithoutDangerousPrefix(name: string) {
|
|
||||||
return name.startsWith("disabled:") ? name.substring(9) : name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
addLabel,
|
addLabel,
|
||||||
setLabel,
|
setLabel,
|
||||||
setRelation,
|
|
||||||
setAttribute,
|
setAttribute,
|
||||||
setBooleanWithInheritance,
|
|
||||||
removeAttributeById,
|
removeAttributeById,
|
||||||
removeOwnedLabelByName,
|
removeOwnedLabelByName,
|
||||||
removeOwnedRelationByName,
|
isAffecting
|
||||||
isAffecting,
|
|
||||||
toggleDangerousAttribute,
|
|
||||||
getNameWithoutDangerousPrefix
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import appContext from "../components/app_context.js";
|
import utils from "./utils.js";
|
||||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
import server from "./server.js";
|
||||||
|
import toastService, { type ToastOptions } from "./toast.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import { t } from "./i18n.js";
|
|
||||||
import server from "./server.js";
|
|
||||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
|
||||||
import utils from "./utils.js";
|
|
||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
|
import appContext from "../components/app_context.js";
|
||||||
|
import { t } from "./i18n.js";
|
||||||
|
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||||
|
|
||||||
// TODO: Deduplicate type with server
|
// TODO: Deduplicate type with server
|
||||||
interface Response {
|
interface Response {
|
||||||
@@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) {
|
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
|
||||||
const newParentBranch = froca.getBranch(newParentBranchId);
|
const newParentBranch = froca.getBranch(newParentBranchId);
|
||||||
if (!newParentBranch) {
|
if (!newParentBranch) {
|
||||||
return;
|
return;
|
||||||
@@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId);
|
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
toastService.showError(resp.message);
|
toastService.showError(resp.message);
|
||||||
@@ -103,7 +103,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
|||||||
* @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
|
* @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.
|
* @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, componentId?: string) {
|
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
|
||||||
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||||
|
|
||||||
if (branchIdsToDelete.length === 0) {
|
if (branchIdsToDelete.length === 0) {
|
||||||
@@ -139,9 +139,9 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
|
|||||||
const branch = froca.getBranch(branchIdToDelete);
|
const branch = froca.getBranch(branchIdToDelete);
|
||||||
|
|
||||||
if (deleteAllClones && branch) {
|
if (deleteAllClones && branch) {
|
||||||
await server.remove(`notes/${branch.noteId}${query}`, componentId);
|
await server.remove(`notes/${branch.noteId}${query}`);
|
||||||
} else {
|
} else {
|
||||||
await server.remove(`branches/${branchIdToDelete}${query}`, componentId);
|
await server.remove(`branches/${branchIdToDelete}${query}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +176,11 @@ async function moveNodeUpInHierarchy(node: Fancytree.FancytreeNode) {
|
|||||||
toastService.showError(resp.message);
|
toastService.showError(resp.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hoistedNoteService.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
||||||
|
node.getParent().folder = false;
|
||||||
|
node.getParent().renderTitle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterSearchBranches(branchIds: string[]) {
|
function filterSearchBranches(branchIds: string[]) {
|
||||||
@@ -195,11 +200,11 @@ function filterRootNote(branchIds: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
|
function makeToast(id: string, message: string): ToastOptions {
|
||||||
return {
|
return {
|
||||||
id,
|
id: id,
|
||||||
title: t("branches.delete-status"),
|
title: t("branches.delete-status"),
|
||||||
message,
|
message: message,
|
||||||
icon: "trash"
|
icon: "trash"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -216,7 +221,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
|
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
|
||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
|
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
|
||||||
toast.timeout = 5000;
|
toast.closeAfter = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
}
|
}
|
||||||
@@ -234,7 +239,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
|
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
|
||||||
} else if (message.type === "taskSucceeded") {
|
} else if (message.type === "taskSucceeded") {
|
||||||
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
|
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
|
||||||
toast.timeout = 5000;
|
toast.closeAfter = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
}
|
}
|
||||||
@@ -242,7 +247,7 @@ ws.subscribeToMessages(async (message) => {
|
|||||||
|
|
||||||
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
|
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
|
||||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
||||||
prefix
|
prefix: prefix
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
@@ -252,7 +257,7 @@ async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, pr
|
|||||||
|
|
||||||
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix?: string) {
|
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix?: string) {
|
||||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
||||||
prefix
|
prefix: prefix
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { Bundle, executeBundle } from "./bundle";
|
|
||||||
import { buildNote } from "../test/easy-froca";
|
|
||||||
|
|
||||||
describe("Script bundle", () => {
|
|
||||||
it("dayjs is available", async () => {
|
|
||||||
const script = /* js */`return api.dayjs().format("YYYY-MM-DD");`;
|
|
||||||
const bundle = getBundle(script);
|
|
||||||
const result = await executeBundle(bundle, null, $());
|
|
||||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dayjs is-same-or-before plugin exists", async () => {
|
|
||||||
const script = /* js */`return api.dayjs("2023-10-01").isSameOrBefore(api.dayjs("2023-10-02"));`;
|
|
||||||
const bundle = getBundle(script);
|
|
||||||
const result = await executeBundle(bundle, null, $());
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function getBundle(script: string) {
|
|
||||||
const id = buildNote({
|
|
||||||
title: "Script note"
|
|
||||||
}).noteId;
|
|
||||||
const bundle: Bundle = {
|
|
||||||
script: [
|
|
||||||
'',
|
|
||||||
`apiContext.modules['${id}'] = { exports: {} };`,
|
|
||||||
`return await ((async function(exports, module, require, api) {`,
|
|
||||||
`try {`,
|
|
||||||
`${script}`,
|
|
||||||
`;`,
|
|
||||||
`} catch (e) { throw new Error(\"Load of script note \\\"Client\\\" (${id}) failed with: \" + e.message); }`,
|
|
||||||
`for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];`,
|
|
||||||
`return module.exports;`,
|
|
||||||
`}).call({}, {}, apiContext.modules['${id}'], apiContext.require([]), apiContext.apis['${id}']));`,
|
|
||||||
''
|
|
||||||
].join('\n'),
|
|
||||||
html: "",
|
|
||||||
noteId: id,
|
|
||||||
allNoteIds: [ id ]
|
|
||||||
};
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { h, VNode } from "preact";
|
|
||||||
|
|
||||||
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
|
||||||
import RightPanelWidget from "../widgets/right_panel_widget.js";
|
|
||||||
import type { Entity } from "./frontend_script_api.js";
|
|
||||||
import { WidgetDefinitionWithType } from "./frontend_script_api_preact.js";
|
|
||||||
import { t } from "./i18n.js";
|
|
||||||
import ScriptContext from "./script_context.js";
|
import ScriptContext from "./script_context.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import toastService, { showErrorForScriptNote } from "./toast.js";
|
import toastService, { showError } from "./toast.js";
|
||||||
import utils, { getErrorMessage } from "./utils.js";
|
import froca from "./froca.js";
|
||||||
|
import utils from "./utils.js";
|
||||||
|
import { t } from "./i18n.js";
|
||||||
|
import type { Entity } from "./frontend_script_api.js";
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
// TODO: Deduplicate with server.
|
||||||
export interface Bundle {
|
export interface Bundle {
|
||||||
@@ -18,13 +14,9 @@ export interface Bundle {
|
|||||||
allNoteIds: string[];
|
allNoteIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyWidget = (BasicWidget | RightPanelWidget) & {
|
interface Widget {
|
||||||
parentWidget?: string;
|
parentWidget?: string;
|
||||||
};
|
}
|
||||||
type WithNoteId<T> = T & {
|
|
||||||
_noteId: string;
|
|
||||||
};
|
|
||||||
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
|
|
||||||
|
|
||||||
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
|
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
|
||||||
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
|
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
|
||||||
@@ -35,27 +27,25 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script =
|
|||||||
return await executeBundle(bundle, originEntity);
|
return await executeBundle(bundle, originEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParentName = "left-pane" | "center-pane" | "note-detail-pane" | "right-pane";
|
async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
||||||
|
|
||||||
export async function executeBundleWithoutErrorHandling(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
|
||||||
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
||||||
return await function () {
|
|
||||||
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
|
||||||
}.call(apiContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
|
||||||
try {
|
try {
|
||||||
return await executeBundleWithoutErrorHandling(bundle, originEntity, $container);
|
return await function () {
|
||||||
} catch (e: unknown) {
|
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
||||||
showErrorForScriptNote(bundle.noteId, t("toast.bundle-error.message", { message: getErrorMessage(e) }));
|
}.call(apiContext);
|
||||||
logError("Widget initialization failed: ", e);
|
} catch (e: any) {
|
||||||
|
const note = await froca.getNote(bundle.noteId);
|
||||||
|
|
||||||
|
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
|
||||||
|
showError(message);
|
||||||
|
logError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStartupBundles() {
|
async function executeStartupBundles() {
|
||||||
const isMobile = utils.isMobile();
|
const isMobile = utils.isMobile();
|
||||||
const scriptBundles = await server.get<Bundle[]>(`script/startup${ isMobile ? "?mobile=true" : ""}`);
|
const scriptBundles = await server.get<Bundle[]>("script/startup" + (isMobile ? "?mobile=true" : ""));
|
||||||
|
|
||||||
for (const bundle of scriptBundles) {
|
for (const bundle of scriptBundles) {
|
||||||
await executeBundle(bundle);
|
await executeBundle(bundle);
|
||||||
@@ -63,99 +53,67 @@ async function executeStartupBundles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class WidgetsByParent {
|
export class WidgetsByParent {
|
||||||
private legacyWidgets: Record<string, WithNoteId<LegacyWidget>[]>;
|
private byParent: Record<string, Widget[]>;
|
||||||
private preactWidgets: Record<string, WithNoteId<WidgetDefinitionWithType>[]>;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.legacyWidgets = {};
|
this.byParent = {};
|
||||||
this.preactWidgets = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(widget: Widget) {
|
add(widget: Widget) {
|
||||||
let hasParentWidget = false;
|
if (!widget.parentWidget) {
|
||||||
let isPreact = false;
|
console.log(`Custom widget does not have mandatory 'parentWidget' property defined`);
|
||||||
if ("type" in widget && widget.type === "preact-widget") {
|
return;
|
||||||
// React-based script.
|
|
||||||
const reactWidget = widget as WithNoteId<WidgetDefinitionWithType>;
|
|
||||||
this.preactWidgets[reactWidget.parent] = this.preactWidgets[reactWidget.parent] || [];
|
|
||||||
this.preactWidgets[reactWidget.parent].push(reactWidget);
|
|
||||||
isPreact = true;
|
|
||||||
hasParentWidget = !!reactWidget.parent;
|
|
||||||
} else if ("parentWidget" in widget && widget.parentWidget) {
|
|
||||||
this.legacyWidgets[widget.parentWidget] = this.legacyWidgets[widget.parentWidget] || [];
|
|
||||||
this.legacyWidgets[widget.parentWidget].push(widget);
|
|
||||||
hasParentWidget = !!widget.parentWidget;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasParentWidget) {
|
this.byParent[widget.parentWidget] = this.byParent[widget.parentWidget] || [];
|
||||||
showErrorForScriptNote(widget._noteId, t("toast.widget-missing-parent", {
|
this.byParent[widget.parentWidget].push(widget);
|
||||||
property: isPreact ? "parent" : "parentWidget"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get(parentName: ParentName) {
|
get(parentName: string) {
|
||||||
const widgets: (BasicWidget | VNode)[] = this.getLegacyWidgets(parentName);
|
if (!this.byParent[parentName]) {
|
||||||
for (const preactWidget of this.getPreactWidgets(parentName)) {
|
return [];
|
||||||
const el = h(preactWidget.render, {});
|
|
||||||
const widget = new ReactWrappedWidget(el);
|
|
||||||
widget.contentSized();
|
|
||||||
if (preactWidget.position) {
|
|
||||||
widget.position = preactWidget.position;
|
|
||||||
}
|
|
||||||
widgets.push(widget);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return widgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLegacyWidgets(parentName: ParentName): (BasicWidget | RightPanelWidget)[] {
|
|
||||||
if (!this.legacyWidgets[parentName]) return [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.legacyWidgets[parentName]
|
this.byParent[parentName]
|
||||||
// previously, custom widgets were provided as a single instance, but that has the disadvantage
|
// previously, custom widgets were provided as a single instance, but that has the disadvantage
|
||||||
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
|
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
|
||||||
// https://github.com/zadam/trilium/issues/4274
|
// https://github.com/zadam/trilium/issues/4274
|
||||||
.map((w: any) => (w.prototype ? new w() : w))
|
.map((w: any) => (w.prototype ? new w() : w))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreactWidgets(parentName: ParentName) {
|
|
||||||
return this.preactWidgets[parentName] ?? [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWidgetBundlesByParent() {
|
async function getWidgetBundlesByParent() {
|
||||||
|
const scriptBundles = await server.get<Bundle[]>("script/widgets");
|
||||||
|
|
||||||
const widgetsByParent = new WidgetsByParent();
|
const widgetsByParent = new WidgetsByParent();
|
||||||
|
|
||||||
try {
|
for (const bundle of scriptBundles) {
|
||||||
const scriptBundles = await server.get<Bundle[]>("script/widgets");
|
let widget;
|
||||||
|
|
||||||
for (const bundle of scriptBundles) {
|
try {
|
||||||
let widget;
|
widget = await executeBundle(bundle);
|
||||||
|
if (widget) {
|
||||||
try {
|
widget._noteId = bundle.noteId;
|
||||||
widget = await executeBundle(bundle);
|
widgetsByParent.add(widget);
|
||||||
if (widget) {
|
|
||||||
widget._noteId = bundle.noteId;
|
|
||||||
widgetsByParent.add(widget);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
const noteId = bundle.noteId;
|
|
||||||
showErrorForScriptNote(noteId, t("toast.bundle-error.message", { message: e.message }));
|
|
||||||
|
|
||||||
logError("Widget initialization failed: ", e);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const noteId = bundle.noteId;
|
||||||
|
const note = await froca.getNote(noteId);
|
||||||
|
toastService.showPersistent({
|
||||||
|
title: t("toast.bundle-error.title"),
|
||||||
|
icon: "alert",
|
||||||
|
message: t("toast.bundle-error.message", {
|
||||||
|
id: noteId,
|
||||||
|
title: note?.title,
|
||||||
|
message: e.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
logError("Widget initialization failed: ", e);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
toastService.showPersistent({
|
|
||||||
id: `custom-widget-list-failure`,
|
|
||||||
title: t("toast.widget-list-error.title"),
|
|
||||||
message: getErrorMessage(e),
|
|
||||||
icon: "bx bx-error-circle"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return widgetsByParent;
|
return widgetsByParent;
|
||||||
|
|||||||