Add customizable folder path templates for note organization

Implements flexible date-based subfolder system with 13 template variables:
- Date variables: {YYYY}, {YY}, {MM}, {M}, {MMM}, {MMMM}, {DD}, {D}, {DDD}, {DDDD}, {Q}
- Event variables: {source}, {event_title}
- Live preview in settings UI with validation
- Backwards compatible (empty template preserves current behavior)
- Comprehensive documentation and examples in README

Addresses GitHub issue #22 for organizing notes in date subfolders.
This commit is contained in:
Michalis Efstratiadis 2025-06-01 10:26:41 +03:00
parent 29c7aeeea7
commit ce620b2bfb
No known key found for this signature in database
5 changed files with 245 additions and 2 deletions

View file

@ -57,6 +57,7 @@ It showcases a list of your calendar events. When you click on an event, it crea
In the plugin settings, you can customize:
- Note location in your vault
- **Folder path template** for organizing notes in date-based subfolders
- Note title format
- Default template with variables like:
- {{event_title}}
@ -68,6 +69,39 @@ In the plugin settings, you can customize:
- {{location}}
- {{source}}
### Organizing Notes with Folder Templates
MemoChron supports flexible folder organization using customizable templates. You can organize your event notes into date-based subfolders automatically.
**Available Template Variables:**
- `{YYYY}` - 4-digit year (2025)
- `{YY}` - 2-digit year (25)
- `{MM}` - 2-digit month (06)
- `{M}` - 1-digit month (6)
- `{MMM}` - 3-letter month abbreviation (Jun)
- `{MMMM}` - Full month name (June)
- `{DD}` - 2-digit day (15)
- `{D}` - 1-digit day (15)
- `{DDD}` - 3-letter day abbreviation (Mon)
- `{DDDD}` - Full day name (Monday)
- `{Q}` - Quarter number (2)
- `{source}` - Calendar source name
- `{event_title}` - Event title (sanitized for file names)
**Example Templates:**
- `{YYYY}/{MMM}``2025/Jun/`
- `{YYYY}-{MM}``2025-06/`
- `{source}/{YYYY}/{MMM}``Work Calendar/2025/Jun/`
- `{YYYY}/Q{Q}``2025/Q2/`
- `{MMM} {YYYY}``Jun 2025/`
**How to Use:**
1. Go to Settings > MemoChron
2. Find the "Folder path template" setting
3. Enter your desired template pattern
4. Use the live preview to see how your template will look
5. Leave empty to save all notes in the same folder (default behavior)
## Configuration
### Settings
@ -77,6 +111,7 @@ In the plugin settings, you can customize:
- **Hide Calendar**: Show only the agenda view without the month calendar grid
- **Refresh Interval**: Set how often calendar data updates
- **Note Location**: Set the default folder for event notes
- **Folder Path Template**: Organize notes in date-based subfolders with customizable patterns
- **Note Title Format**: Customize how note titles are generated
- **Template**: Customize the default note template
- **Tags**: Set default tags for event notes

View file

@ -14,6 +14,22 @@ interface EventTemplateVariables {
description: string;
}
interface FolderTemplateVariables {
YYYY: string;
YY: string;
MM: string;
M: string;
MMM: string;
MMMM: string;
DD: string;
D: string;
DDD: string;
DDDD: string;
Q: string;
source: string;
event_title: string;
}
export class NoteService {
private static readonly NOTES_SECTION_MARKER = "## 📝 Notes";
private static readonly FRONTMATTER_DELIMITER = "---";
@ -63,10 +79,18 @@ export class NoteService {
}
private buildFilePath(event: CalendarEvent): string {
const { noteLocation, noteTitleFormat } = this.settings;
const { noteLocation, noteTitleFormat, folderPathTemplate } = this.settings;
const normalizedPath = normalizePath(noteLocation);
const title = this.formatTitle(noteTitleFormat, event);
return normalizePath(`${normalizedPath}/${title}.md`);
// If folderPathTemplate is empty, use the old behavior
if (!folderPathTemplate.trim()) {
return normalizePath(`${normalizedPath}/${title}.md`);
}
// Apply folder template to create subfolder structure
const subfolderPath = this.applyFolderTemplate(folderPathTemplate, event);
return normalizePath(`${normalizedPath}/${subfolderPath}/${title}.md`);
}
private async ensureParentFolder(filePath: string): Promise<void> {
@ -298,6 +322,56 @@ export class NoteService {
}
}
private applyFolderTemplate(template: string, event: CalendarEvent): string {
const variables = this.getFolderTemplateVariables(event);
return this.parseFolderTemplate(template, variables);
}
private getFolderTemplateVariables(event: CalendarEvent): FolderTemplateVariables {
const date = event.start;
const monthNames = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const monthAbbreviations = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
const dayNames = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
];
const dayAbbreviations = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const dayOfWeek = date.getDay();
const quarter = Math.floor(month / 3) + 1;
return {
YYYY: year.toString(),
YY: year.toString().slice(-2),
MM: (month + 1).toString().padStart(2, "0"),
M: (month + 1).toString(),
MMM: monthAbbreviations[month],
MMMM: monthNames[month],
DD: day.toString().padStart(2, "0"),
D: day.toString(),
DDD: dayAbbreviations[dayOfWeek],
DDDD: dayNames[dayOfWeek],
Q: quarter.toString(),
source: this.sanitizeFileName(event.source),
event_title: this.sanitizeFileName(event.title),
};
}
private parseFolderTemplate(template: string, variables: FolderTemplateVariables): string {
return Object.entries(variables).reduce((result, [key, value]) => {
const pattern = new RegExp(`\\{${key}\\}`, "g");
return result.replace(pattern, value);
}, template);
}
private sanitizeFileName(str: string): string {
return str.replace(/[\\/:*?"<>|]/g, "-");
}

View file

@ -52,6 +52,7 @@ export class SettingsTab extends PluginSettingTab {
new Setting(this.containerEl).setName("Notes").setHeading();
this.renderNoteLocation();
this.renderFolderPathTemplate();
this.renderNoteTitleFormat();
this.renderNoteDateFormat();
this.renderDefaultFrontmatter();
@ -318,6 +319,122 @@ export class SettingsTab extends PluginSettingTab {
});
}
private renderFolderPathTemplate(): void {
const templateSetting = new Setting(this.containerEl)
.setName("Folder path template")
.setDesc("Organize notes in date-based subfolders. Leave empty to save all notes in the same folder.");
templateSetting.descEl.createEl("br");
templateSetting.descEl.createEl("small", {
text: "Available variables: {YYYY}, {YY}, {MM}, {M}, {MMM}, {MMMM}, {DD}, {D}, {DDD}, {DDDD}, {Q}, {source}, {event_title}"
});
templateSetting.descEl.createEl("br");
templateSetting.descEl.createEl("small", {
text: "Examples: {YYYY}/{MM}, {YYYY}-{MMM}, {source}/{YYYY}/{MMM}"
});
templateSetting.addText((text) =>
text
.setPlaceholder("{YYYY}/{MMM}")
.setValue(this.plugin.settings.folderPathTemplate)
.onChange(async (value) => {
this.plugin.settings.folderPathTemplate = value;
await this.plugin.saveSettings();
})
);
// Add preview container
const previewContainer = templateSetting.controlEl.createDiv({
cls: "memochron-template-preview",
});
this.updateTemplatePreview(previewContainer, this.plugin.settings.folderPathTemplate);
// Update preview when input changes
const textInput = templateSetting.controlEl.querySelector('input') as HTMLInputElement;
if (textInput) {
textInput.addEventListener('input', () => {
this.updateTemplatePreview(previewContainer, textInput.value);
});
}
}
private updateTemplatePreview(container: HTMLElement, template: string): void {
container.empty();
if (!template.trim()) {
container.createEl("small", {
text: "Preview: Notes will be saved directly in the note location folder",
cls: "memochron-preview-text"
});
return;
}
// Create a sample date for preview
const sampleDate = new Date();
const sampleEvent = {
title: "Sample Meeting",
start: sampleDate,
end: sampleDate,
source: "Work Calendar"
};
try {
const previewPath = this.generatePreviewPath(template, sampleEvent);
container.createEl("small", {
text: `Preview: ${previewPath}/`,
cls: "memochron-preview-text"
});
} catch (error) {
container.createEl("small", {
text: "Invalid template format",
cls: "memochron-preview-error"
});
}
}
private generatePreviewPath(template: string, event: any): string {
const monthNames = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const monthAbbreviations = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
const dayNames = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
];
const dayAbbreviations = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const date = event.start;
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const dayOfWeek = date.getDay();
const quarter = Math.floor(month / 3) + 1;
const variables = {
YYYY: year.toString(),
YY: year.toString().slice(-2),
MM: (month + 1).toString().padStart(2, "0"),
M: (month + 1).toString(),
MMM: monthAbbreviations[month],
MMMM: monthNames[month],
DD: day.toString().padStart(2, "0"),
D: day.toString(),
DDD: dayAbbreviations[dayOfWeek],
DDDD: dayNames[dayOfWeek],
Q: quarter.toString(),
source: event.source.replace(/[\\/:*?"<>|]/g, "-"),
event_title: event.title.replace(/[\\/:*?"<>|]/g, "-"),
};
return Object.entries(variables).reduce((result, [key, value]) => {
const pattern = new RegExp(`\\{${key}\\}`, "g");
return result.replace(pattern, value);
}, template);
}
private renderDefaultTags(): void {
new Setting(this.containerEl)
.setName("Default tags")

View file

@ -29,6 +29,7 @@ export interface MemoChronSettings {
noteTemplate: string;
firstDayOfWeek: number; // 0 = Sunday, 1 = Monday, etc.
hideCalendar: boolean;
folderPathTemplate: string; // Template for organizing notes in date-based subfolders
}
export const DEFAULT_SETTINGS: MemoChronSettings = {
@ -54,4 +55,5 @@ export const DEFAULT_SETTINGS: MemoChronSettings = {
`,
firstDayOfWeek: DEFAULT_FIRST_DAY_OF_WEEK,
hideCalendar: false,
folderPathTemplate: "", // Empty by default for backwards compatibility
};

View file

@ -334,6 +334,21 @@
background: var(--background-modifier-hover);
}
/* Template Preview */
.memochron-template-preview {
margin-top: var(--memochron-padding-xs);
}
.memochron-preview-text {
color: var(--text-muted);
font-style: italic;
}
.memochron-preview-error {
color: var(--text-error);
font-style: italic;
}
/* Mobile Specific Styles */
@media screen and (max-width: 768px) {
.memochron-calendar-grid {