diff --git a/docs/product-specs/group-folder-paths.md b/docs/product-specs/group-folder-paths.md new file mode 100644 index 0000000..e5b3562 --- /dev/null +++ b/docs/product-specs/group-folder-paths.md @@ -0,0 +1,25 @@ +# Group Folder Paths + +**Status:** Shipped. + +## User Story + +> As a user with an established collection-based vault layout, I want to +> choose whether Raindrop sidebar Groups add another folder level so imports +> continue using the structure I expect. + +## Acceptance Criteria + +- [x] Group folders remain enabled by default to preserve the current layout. +- [x] Users can disable Group folders in Make It Rain's settings without + leaving Obsidian. +- [x] When enabled, a note uses `Group/Collection/Subcollection` folders. +- [x] When disabled, a note uses `Collection/Subcollection` folders. +- [x] `collectionGroup` remains available in templates and frontmatter in both + modes. +- [x] The setting explains the effect on existing pre-v1.10 layouts. + +## Out of Scope + +- Moving or deleting notes previously imported under another hierarchy mode. +- Configuring hierarchy behavior separately for individual imports or presets. diff --git a/docs/product-specs/index.md b/docs/product-specs/index.md index 0fb6bbd..85cc427 100644 --- a/docs/product-specs/index.md +++ b/docs/product-specs/index.md @@ -16,6 +16,7 @@ | [Safe sync](safe-sync.md) | Shipped (Issue #9) | Detect remote deletions and reconcile against the local vault with human review | | [Per-content-type templates](per-content-type-templates.md) | Shipped | Configure Markdown output per Raindrop content type (link, article, image, video, document, audio, book) | | [Folder notes](folder-notes.md) | Shipped | Auto-generate index notes for each collection folder | +| [Group folder paths](group-folder-paths.md) | Shipped | Choose whether Raindrop Groups add a root folder above the collection hierarchy | | [Binary attachment download](binary-attachment-download.md) | Shipped | Download native Raindrop file attachments (PDF, EPUB, image, video, audio) | | [Import presets](import-presets.md) | Shipped | Save named bulk-import configurations and re-run them from the modal or command palette | diff --git a/docs/user-guide/collections.md b/docs/user-guide/collections.md index fcefec3..f0887d1 100644 --- a/docs/user-guide/collections.md +++ b/docs/user-guide/collections.md @@ -80,6 +80,12 @@ Vault Root/ └── Note 1.md ``` +This is controlled by **Settings → Make It Rain → Import & Organization → +Include Raindrop group in folder path**. It is enabled by default to preserve +the current layout. Disable it to use the pre-v1.10 Collection-only layout for +future imports. The `collectionGroup` template variable and frontmatter field +remain available when Group folders are disabled. + ### Hierarchy Options - **Flat Structure**: All notes in one folder diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 5d81799..462cc6a 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -141,6 +141,10 @@ You can create specific templates for different types of content. - **API Token Security**: Your API token gives access to your Raindrop.io account. Never share it with others. - **Folder Structure**: Choose a default vault location that fits your knowledge management system. +- **Group Folder Paths**: Keep **Include Raindrop group in folder path** + enabled to mirror the Raindrop sidebar as `Group/Collection`. Disable it to + restore the pre-v1.10 `Collection`-only layout. This changes future import + paths only; `collectionGroup` metadata remains available. - **Filename Conflicts**: If you encounter filename conflicts, consider using unique identifiers in your filename template, such as `{{id}}-{{title}}`. - **Template Testing**: After creating custom templates, test them with a small batch of raindrops before importing your entire collection. diff --git a/src/main.ts b/src/main.ts index 41fca42..0c9aa71 100644 --- a/src/main.ts +++ b/src/main.ts @@ -679,7 +679,7 @@ export default class RaindropToObsidian extends Plugin implements IRaindropToObs } } - if (groupTitle) { + if (groupTitle && this.settings.includeGroupInFolderPath) { pathSegments.unshift(sanitizeFileName(groupTitle)); } diff --git a/src/settings.ts b/src/settings.ts index cbff606..441517d 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -236,6 +236,7 @@ export const DEFAULT_SETTINGS: MakeItRainSettings = { }, downloadFiles: false, createFolderNotes: false, + includeGroupInFolderPath: true, namedTemplates: { base: `--- title: "{{title}}" @@ -511,6 +512,17 @@ export class RaindropToObsidianSettingTab extends PluginSettingTab { }); }); + new Setting(orgContent) + .setName('Include Raindrop group in folder path') + .setDesc('When enabled, imports use Group/Collection folders to mirror the Raindrop sidebar. Disable this to use the pre-v1.10 Collection-only layout. This affects future imports and does not move existing notes. The collectionGroup template variable and frontmatter metadata remain available either way.') + .addToggle((toggle: ToggleComponent) => { + toggle.setValue(this.plugin.settings.includeGroupInFolderPath) + .onChange(async (value: boolean) => { + this.plugin.settings.includeGroupInFolderPath = value; + await this.plugin.saveSettings(); + }); + }); + // --- 3. Content Enhancements --- const contentSection = containerEl.createEl('details', { cls: 'make-it-rain-settings-section' }); contentSection.createEl('summary', { text: 'Content Enhancements', cls: 'make-it-rain-section-summary' }); diff --git a/src/types.ts b/src/types.ts index 67149c9..72d82e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,7 @@ export interface MakeItRainSettings { contentTypeTemplateToggles: ContentTypeToggles; downloadFiles: boolean; createFolderNotes: boolean; + includeGroupInFolderPath: boolean; archiveScraping: boolean; namedTemplates: Record; enableSafeSync: boolean; diff --git a/tests/unit/groupHierarchy.test.ts b/tests/unit/groupHierarchy.test.ts index fb78657..3fe05a0 100644 --- a/tests/unit/groupHierarchy.test.ts +++ b/tests/unit/groupHierarchy.test.ts @@ -22,7 +22,9 @@ describe('Group Hierarchy Support', () => { jest.clearAllMocks(); }); - it('should prepend Group name to collection path and target folder', async () => { + it('should prepend Group name to collection path and target folder by default', async () => { + expect(plugin.settings.includeGroupInFolderPath).toBe(true); + // Mock data const collections: RaindropCollection[] = [ { _id: 10, title: 'Parent Collection' }, @@ -107,6 +109,69 @@ describe('Group Hierarchy Support', () => { ); }); + it('should keep Group metadata without adding a Group folder when disabled', async () => { + plugin.settings.includeGroupInFolderPath = false; + + const collections: RaindropCollection[] = [ + { _id: 10, title: 'Parent Collection' }, + { _id: 20, title: 'Child Collection', parent: { $id: 10 } } + ]; + const raindrop: RaindropItem = { + _id: 123, + title: 'Test Bookmark', + link: 'https://example.com', + type: 'link', + collection: { $id: 20, title: 'Child Collection' }, + created: '2024-01-01T00:00:00Z', + lastUpdate: '2024-01-01T00:00:00Z' + }; + const options: ModalFetchOptions = { + collections: '20', + apiFilterTags: '', + includeSubcollections: false, + appendTagsToNotes: '', + useRaindropTitleForFileName: true, + tagMatchType: 'all', + filterType: 'all', + fetchOnlyNew: false, + updateExisting: true, + useDefaultTemplate: false, + overrideTemplates: false + }; + const collectionIdToNameMap = new Map([ + [10, 'Parent Collection'], + [20, 'Child Collection'] + ]); + const collectionToGroupMap = new Map([[10, 'MY GROUP']]); + const mockNotice = { setMessage: jest.fn(), hide: jest.fn() } as unknown as Notice; + const createSpy = jest.spyOn(plugin.app.vault, 'create').mockResolvedValue({} as any); + + await plugin.processRaindrops( + [raindrop], + 'Raindrops', + '', + mockNotice, + options, + { result: true, items: collections }, + collectionIdToNameMap, + new Set(), + collectionToGroupMap + ); + + expect(createSpy).toHaveBeenCalledWith( + 'Raindrops/Parent Collection/Child Collection/Test Bookmark.md', + expect.stringContaining('collectionGroup: "MY GROUP"') + ); + expect(createSpy).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('collectionPath: "Parent Collection/Child Collection"') + ); + expect(createSpy).not.toHaveBeenCalledWith( + expect.stringContaining('MY GROUP/Parent Collection'), + expect.any(String) + ); + }); + it('should include collectionGroup in manual frontmatter when templates are disabled', async () => { plugin.settings.isTemplateSystemEnabled = false; diff --git a/tests/unit/main.test.ts b/tests/unit/main.test.ts index 99b7f4d..4b9e1ec 100644 --- a/tests/unit/main.test.ts +++ b/tests/unit/main.test.ts @@ -24,6 +24,7 @@ describe('RaindropToObsidian', () => { it('should initialize with default settings', () => { expect(plugin.settings).toBeDefined(); expect(plugin.settings.fileNameTemplate).toBe('{{title}}'); + expect(plugin.settings.includeGroupInFolderPath).toBe(true); }); describe('onload', () => { @@ -134,6 +135,7 @@ describe('RaindropToObsidian', () => { expect(plugin.settings.apiToken).toBe('test-token'); expect(plugin.settings.showRibbonIcon).toBe(false); expect(plugin.settings.fileNameTemplate).toBe('{{title}}'); // Default preserved + expect(plugin.settings.includeGroupInFolderPath).toBe(true); // Added setting defaults on for existing data expect(saveSettingsSpy).toHaveBeenCalled(); }); }); diff --git a/tests/unit/settings.test.ts b/tests/unit/settings.test.ts index 64ac11b..b9df3d2 100644 --- a/tests/unit/settings.test.ts +++ b/tests/unit/settings.test.ts @@ -35,6 +35,10 @@ describe('RaindropToObsidianSettingTab', () => { expect(container.classList.contains('make-it-rain-settings-container')).toBe(true); expect(container.innerHTML).toContain('Connection & Core Setup'); expect(container.innerHTML).toContain('Import & Organization'); + expect(container.innerHTML).toContain('Include Raindrop group in folder path'); + expect(container.innerHTML).toContain('pre-v1.10 Collection-only layout'); + expect(container.innerHTML).toContain('affects future imports and does not move existing notes'); + expect(container.innerHTML).toContain('collectionGroup template variable and frontmatter metadata remain available'); expect(container.innerHTML).toContain('Template Engine'); });