mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 23:14:24 +01:00
20 KiB
Vendored
20 KiB
Vendored
Collections Advanced Features
Master advanced collection features including dynamic queries, custom views, and complex data visualization.
Prerequisites
- Understanding of search queries
- Familiarity with basic collection types
- Knowledge of note attributes
Dynamic Collections
Query-Based Collections
Collections that automatically update based on search queries:
// Create dynamic task collection
const taskCollection = api.createNote({
parentNoteId: 'projects',
title: 'Active Tasks',
type: 'search',
content: JSON.stringify({
searchString: '#task #status!=completed',
orderBy: 'priority DESC, dueDate ASC',
limit: 50
})
});
// Add collection configuration
taskCollection.setLabel('searchString', '#task #status!=completed');
taskCollection.setLabel('refreshInterval', '300'); // Refresh every 5 minutes
taskCollection.setLabel('viewType', 'board'); // Display as board
Auto-Updating Collections
Configure collections to refresh automatically:
class DynamicCollection {
constructor(noteId, query, refreshInterval = 60000) {
this.noteId = noteId;
this.query = query;
this.refreshInterval = refreshInterval;
this.cache = new Map();
this.startAutoRefresh();
}
startAutoRefresh() {
this.refresh();
setInterval(() => this.refresh(), this.refreshInterval);
}
async refresh() {
const results = await api.searchForNotes(this.query);
const note = api.getNote(this.noteId);
// Update collection content
note.setContent(JSON.stringify({
query: this.query,
results: results.map(r => r.noteId),
lastUpdated: new Date().toISOString(),
count: results.length
}));
// Trigger UI update
api.refreshCollectionWidget(this.noteId);
}
// Advanced filtering
applyFilters(filters) {
let filteredQuery = this.query;
for (const [key, value] of Object.entries(filters)) {
if (value) {
filteredQuery += ` #${key}=${value}`;
}
}
return api.searchForNotes(filteredQuery);
}
}
Computed Collections
Collections with calculated membership:
// Collection that groups notes by calculated criteria
class ComputedCollection {
constructor(computeFn) {
this.computeFn = computeFn;
this.groups = new Map();
}
async compute() {
const allNotes = await api.getAllNotes();
this.groups.clear();
for (const note of allNotes) {
const groupKey = await this.computeFn(note);
if (groupKey) {
if (!this.groups.has(groupKey)) {
this.groups.set(groupKey, []);
}
this.groups.get(groupKey).push(note);
}
}
return this.groups;
}
async createCollectionNotes(parentNoteId) {
const groups = await this.compute();
for (const [groupName, notes] of groups) {
const collectionNote = await api.createNote({
parentNoteId,
title: groupName,
type: 'render',
content: this.renderGroup(groupName, notes)
});
// Link to members
for (const note of notes) {
collectionNote.addRelation('includes', note.noteId);
}
}
}
renderGroup(name, notes) {
return `
<h2>${name}</h2>
<div class="collection-grid">
${notes.map(n => `
<div class="collection-item">
<a href="#${n.noteId}">${n.title}</a>
<span class="meta">${n.dateCreated}</span>
</div>
`).join('')}
</div>
`;
}
}
// Usage: Group by month created
const monthlyCollection = new ComputedCollection(note => {
const date = new Date(note.dateCreated);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
});
Board View Customization
Advanced Board Configuration
Create sophisticated Kanban boards:
class KanbanBoard {
constructor(noteId, config) {
this.noteId = noteId;
this.config = {
columns: [],
swimlanes: null,
cardTemplate: null,
filters: {},
sorting: 'position',
...config
};
}
async setupBoard() {
const note = api.getNote(this.noteId);
// Define columns
note.setLabel('boardColumns', JSON.stringify([
{ id: 'todo', title: 'To Do', query: '#status=todo', limit: 10 },
{ id: 'progress', title: 'In Progress', query: '#status=progress', limit: 5 },
{ id: 'review', title: 'Review', query: '#status=review', limit: 5 },
{ id: 'done', title: 'Done', query: '#status=done', limit: 20 }
]));
// Configure card display
note.setLabel('cardTemplate', `
<div class="kanban-card">
<h4>{title}</h4>
<div class="priority-{priority}">{priority}</div>
<div class="assignee">{assignee}</div>
<div class="due-date">{dueDate}</div>
<div class="tags">{tags}</div>
</div>
`);
// Add WIP limits
note.setLabel('wipLimits', JSON.stringify({
'progress': 5,
'review': 3
}));
// Enable drag-drop
note.setLabel('enableDragDrop', 'true');
note.setLabel('onCardMove', 'updateCardStatus');
}
async moveCard(cardId, fromColumn, toColumn) {
const card = api.getNote(cardId);
// Update status
card.setLabel('status', toColumn);
// Log transition
card.addLabel('transition', `${fromColumn}->${toColumn}`, false);
card.setLabel('lastMoved', new Date().toISOString());
// Check WIP limits
const wipLimits = JSON.parse(api.getNote(this.noteId).getLabelValue('wipLimits'));
if (wipLimits[toColumn]) {
const columnCards = await api.searchForNotes(`#status=${toColumn}`);
if (columnCards.length > wipLimits[toColumn]) {
throw new Error(`WIP limit exceeded for ${toColumn}`);
}
}
// Trigger automations
await this.triggerColumnAutomations(cardId, toColumn);
}
async triggerColumnAutomations(cardId, column) {
const automations = {
'done': async (card) => {
card.setLabel('completedDate', new Date().toISOString());
card.setLabel('archived', 'false');
},
'review': async (card) => {
// Notify reviewers
const reviewers = card.getRelationTargets('reviewer');
for (const reviewer of reviewers) {
await this.notifyReviewer(reviewer, card);
}
}
};
if (automations[column]) {
await automations[column](api.getNote(cardId));
}
}
}
Swimlanes Implementation
Add horizontal grouping to boards:
class SwimlaneBoard {
constructor(boardNoteId) {
this.boardNoteId = boardNoteId;
}
async configureSwimlanes(groupBy) {
const note = api.getNote(this.boardNoteId);
// Configure swimlane grouping
note.setLabel('swimlaneGroupBy', groupBy);
// Define swimlane queries
const swimlanes = await this.generateSwimlanes(groupBy);
note.setLabel('swimlanes', JSON.stringify(swimlanes));
}
async generateSwimlanes(groupBy) {
switch (groupBy) {
case 'priority':
return [
{ id: 'critical', title: 'Critical', query: '#priority=critical' },
{ id: 'high', title: 'High', query: '#priority=high' },
{ id: 'medium', title: 'Medium', query: '#priority=medium' },
{ id: 'low', title: 'Low', query: '#priority=low' }
];
case 'assignee':
const assignees = await this.getUniqueAssignees();
return assignees.map(assignee => ({
id: assignee,
title: assignee,
query: `#assignee=${assignee}`
}));
case 'project':
const projects = await api.searchForNotes('#type=project');
return projects.map(project => ({
id: project.noteId,
title: project.title,
query: `~project.noteId=${project.noteId}`
}));
default:
return [];
}
}
async getUniqueAssignees() {
const notes = await api.searchForNotes('#assignee');
const assignees = new Set();
for (const note of notes) {
const assignee = note.getLabelValue('assignee');
if (assignee) assignees.add(assignee);
}
return Array.from(assignees);
}
}
Calendar Integration
Advanced Calendar Features
Create sophisticated calendar views:
class AdvancedCalendar {
constructor(noteId, config = {}) {
this.noteId = noteId;
this.config = {
view: 'month', // month|week|day|year
showWeekends: true,
firstDay: 1, // Monday
eventSources: [],
colorScheme: 'category',
...config
};
}
async setupCalendar() {
const note = api.getNote(this.noteId);
// Configure calendar
note.setLabel('calendarConfig', JSON.stringify(this.config));
// Add event sources
note.setLabel('eventSources', JSON.stringify([
{ query: '#task #dueDate', color: 'blue', type: 'task' },
{ query: '#meeting #date', color: 'green', type: 'meeting' },
{ query: '#deadline', color: 'red', type: 'deadline' },
{ query: '#milestone', color: 'purple', type: 'milestone' }
]));
// Configure event display
note.setLabel('eventTemplate', `
<div class="calendar-event {type}">
<span class="time">{time}</span>
<span class="title">{title}</span>
<span class="location">{location}</span>
</div>
`);
}
async getEvents(startDate, endDate) {
const sources = JSON.parse(
api.getNote(this.noteId).getLabelValue('eventSources')
);
const events = [];
for (const source of sources) {
const notes = await api.searchForNotes(source.query);
for (const note of notes) {
const event = this.noteToEvent(note, source);
if (event && this.isInRange(event, startDate, endDate)) {
events.push(event);
}
}
}
return this.sortEvents(events);
}
noteToEvent(note, source) {
const dateAttr = this.findDateAttribute(note);
if (!dateAttr) return null;
return {
id: note.noteId,
title: note.title,
start: dateAttr.value,
end: note.getLabelValue('endDate') || dateAttr.value,
allDay: !note.hasLabel('time'),
color: source.color,
type: source.type,
url: `#${note.noteId}`,
extendedProps: {
description: note.getContentPreview(100),
location: note.getLabelValue('location'),
attendees: note.getRelationTargets('attendee')
}
};
}
findDateAttribute(note) {
const dateAttrs = ['dueDate', 'date', 'eventDate', 'startDate'];
for (const attrName of dateAttrs) {
const value = note.getLabelValue(attrName);
if (value) {
return { name: attrName, value };
}
}
return null;
}
async createRecurringEvents(pattern, template) {
const recurrence = new RecurrencePattern(pattern);
const dates = recurrence.generate(new Date(), 365); // Next year
for (const date of dates) {
const event = await api.createNote({
...template,
title: `${template.title} - ${date.toLocaleDateString()}`
});
event.setLabel('date', date.toISOString());
event.setLabel('recurring', 'true');
event.setLabel('recurrencePattern', pattern);
}
}
}
Geo Map Configuration
Advanced Map Features
Create location-based collections:
class GeoMapCollection {
constructor(noteId) {
this.noteId = noteId;
}
async setupMap() {
const note = api.getNote(this.noteId);
// Configure map view
note.setLabel('mapConfig', JSON.stringify({
center: [40.7128, -74.0060], // New York
zoom: 10,
style: 'streets', // streets|satellite|hybrid|terrain
clustering: true,
heatmap: false
}));
// Configure data sources
note.setLabel('geoDataSources', JSON.stringify([
{ query: '#location #type=office', icon: 'building', color: 'blue' },
{ query: '#location #type=customer', icon: 'user', color: 'green' },
{ query: '#location #type=event', icon: 'calendar', color: 'red' }
]));
// Add layers
note.setLabel('mapLayers', JSON.stringify([
{ type: 'markers', visible: true },
{ type: 'routes', visible: false },
{ type: 'regions', visible: false }
]));
}
async getGeoData() {
const sources = JSON.parse(
api.getNote(this.noteId).getLabelValue('geoDataSources')
);
const features = [];
for (const source of sources) {
const notes = await api.searchForNotes(source.query);
for (const note of notes) {
const geoData = this.extractGeoData(note);
if (geoData) {
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [geoData.lng, geoData.lat]
},
properties: {
noteId: note.noteId,
title: note.title,
description: note.getContentPreview(200),
icon: source.icon,
color: source.color,
...this.extractProperties(note)
}
});
}
}
}
return {
type: 'FeatureCollection',
features
};
}
extractGeoData(note) {
// Try different location formats
const location = note.getLabelValue('location');
if (location) {
// Coordinates format: "lat,lng"
const coords = location.match(/(-?\d+\.?\d*),\s*(-?\d+\.?\d*)/);
if (coords) {
return { lat: parseFloat(coords[1]), lng: parseFloat(coords[2]) };
}
// Address format - would need geocoding service
return this.geocodeAddress(location);
}
// Check for separate lat/lng attributes
const lat = note.getLabelValue('latitude');
const lng = note.getLabelValue('longitude');
if (lat && lng) {
return { lat: parseFloat(lat), lng: parseFloat(lng) };
}
return null;
}
async createGeoFence(name, coordinates) {
const fence = await api.createNote({
parentNoteId: this.noteId,
title: name,
type: 'data',
mime: 'application/json'
});
fence.setContent(JSON.stringify({
type: 'Polygon',
coordinates: [coordinates]
}));
fence.setLabel('geoFence', 'true');
return fence;
}
}
Collection Query Optimization
Query Performance
Optimize collection queries for large datasets:
class OptimizedCollectionQuery {
constructor() {
this.cache = new Map();
this.indexes = new Map();
}
async buildIndexes() {
// Build attribute indexes
const attributeIndex = new Map();
const notes = await api.getAllNotes();
for (const note of notes) {
for (const attr of note.getAttributes()) {
const key = `${attr.type}:${attr.name}:${attr.value}`;
if (!attributeIndex.has(key)) {
attributeIndex.set(key, new Set());
}
attributeIndex.get(key).add(note.noteId);
}
}
this.indexes.set('attributes', attributeIndex);
}
async query(searchString, options = {}) {
const cacheKey = `${searchString}:${JSON.stringify(options)}`;
// Check cache
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < 60000) { // 1 minute cache
return cached.results;
}
}
// Parse and optimize query
const optimizedQuery = this.optimizeQuery(searchString);
// Execute query
const results = await this.executeOptimized(optimizedQuery, options);
// Cache results
this.cache.set(cacheKey, {
results,
timestamp: Date.now()
});
return results;
}
optimizeQuery(searchString) {
// Parse query into AST
const ast = this.parseQuery(searchString);
// Reorder for optimal execution
const optimized = this.reorderClauses(ast);
// Identify index usage opportunities
const indexed = this.identifyIndexes(optimized);
return indexed;
}
async executeOptimized(query, options) {
const { limit = 100, offset = 0, orderBy } = options;
// Use indexes where possible
let candidates = await this.getIndexedCandidates(query);
// Apply additional filters
candidates = this.applyFilters(candidates, query);
// Sort results
if (orderBy) {
candidates = this.sortResults(candidates, orderBy);
}
// Apply pagination
return candidates.slice(offset, offset + limit);
}
}
Incremental Updates
Update collections efficiently:
class IncrementalCollectionUpdater {
constructor(collectionNoteId) {
this.collectionNoteId = collectionNoteId;
this.lastUpdate = null;
this.changeBuffer = [];
}
async trackChanges() {
// Subscribe to entity changes
api.onEntityChange((change) => {
if (this.affectsCollection(change)) {
this.changeBuffer.push(change);
this.scheduleUpdate();
}
});
}
affectsCollection(change) {
const collection = api.getNote(this.collectionNoteId);
const query = collection.getLabelValue('searchString');
// Simple check - could be more sophisticated
return change.entityName === 'notes' ||
change.entityName === 'attributes';
}
scheduleUpdate() {
// Debounce updates
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(() => this.applyUpdates(), 500);
}
async applyUpdates() {
if (this.changeBuffer.length === 0) return;
const changes = [...this.changeBuffer];
this.changeBuffer = [];
const collection = api.getNote(this.collectionNoteId);
const currentMembers = new Set(JSON.parse(collection.getContent()));
for (const change of changes) {
if (change.action === 'create' || change.action === 'update') {
// Check if note should be in collection
const shouldInclude = await this.evaluateMembership(change.entityId);
if (shouldInclude && !currentMembers.has(change.entityId)) {
currentMembers.add(change.entityId);
} else if (!shouldInclude && currentMembers.has(change.entityId)) {
currentMembers.delete(change.entityId);
}
} else if (change.action === 'delete') {
currentMembers.delete(change.entityId);
}
}
// Update collection
collection.setContent(JSON.stringify(Array.from(currentMembers)));
this.lastUpdate = Date.now();
}
async evaluateMembership(noteId) {
const collection = api.getNote(this.collectionNoteId);
const query = collection.getLabelValue('searchString');
const results = await api.searchForNotes(`${query} #noteId=${noteId}`);
return results.length > 0;
}
}
Troubleshooting
Collection Not Updating
Symptom: Dynamic collection shows stale data.
Solutions:
- Check refresh interval setting
- Verify search query syntax
- Clear collection cache
- Check for query errors in logs
Performance Issues
Symptom: Collection loads slowly or times out.
Solutions:
- Optimize search queries
- Implement pagination
- Use indexed attributes
- Enable query caching
View Rendering Problems
Symptom: Collection view doesn't display correctly.
Solutions:
- Verify view configuration
- Check template syntax
- Review browser console for errors
- Test with simpler template
Best Practices
-
Optimize Queries
- Use indexed attributes
- Limit result sets
- Cache frequently used queries
-
Design for Scale
- Implement pagination
- Use incremental updates
- Consider data aggregation
-
Enhance User Experience
- Provide loading indicators
- Implement progressive loading
- Add filtering and sorting options
-
Maintain Performance
- Monitor query execution time
- Clean up old cache entries
- Use appropriate refresh intervals
-
Document Configuration
- Record query patterns
- Document view customizations
- Track performance optimizations