From dfb653f2a77e0dc0475d1e657b7a35191c7bedd0 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Wed, 21 Jan 2026 15:17:44 -0800 Subject: [PATCH] fix "No Folder" filter --- .../models/vault-filter.model.spec.ts | 11 +++++++++++ .../vault-filter/models/vault-filter.model.ts | 15 ++++++++++++--- libs/vault/src/models/filter-function.spec.ts | 8 ++++++++ libs/vault/src/models/filter-function.ts | 6 +++++- .../models/routed-vault-filter-bridge.model.ts | 2 +- libs/vault/src/models/vault-filter.model.ts | 2 +- .../routed-vault-filter-bridge.service.ts | 2 +- 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts b/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts index a2f8aa7a352..b45472ad01c 100644 --- a/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts +++ b/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts @@ -143,6 +143,17 @@ describe("VaultFilter", () => { expect(result).toBe(true); }); + + it("should return true when filtering on unassigned folder via empty string id", () => { + const filterFunction = createFilterFunction({ + selectedFolder: true, + selectedFolderId: "", + }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); }); describe("given an organizational cipher (with organization and collections)", () => { diff --git a/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts b/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts index 83693c85239..e4c3526e7ae 100644 --- a/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts +++ b/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts @@ -57,10 +57,19 @@ export class VaultFilter { if (this.cipherType != null && cipherPassesFilter) { cipherPassesFilter = CipherViewLikeUtils.getType(cipher) === this.cipherType; } - if (this.selectedFolder && this.selectedFolderId == null && cipherPassesFilter) { - cipherPassesFilter = cipher.folderId == null; + if ( + this.selectedFolder && + (this.selectedFolderId == null || this.selectedFolderId === "") && + cipherPassesFilter + ) { + cipherPassesFilter = cipher.folderId == null || cipher.folderId === ""; } - if (this.selectedFolder && this.selectedFolderId != null && cipherPassesFilter) { + if ( + this.selectedFolder && + this.selectedFolderId != null && + this.selectedFolderId !== "" && + cipherPassesFilter + ) { cipherPassesFilter = cipher.folderId === this.selectedFolderId; } if (this.selectedCollection && this.selectedCollectionId == null && cipherPassesFilter) { diff --git a/libs/vault/src/models/filter-function.spec.ts b/libs/vault/src/models/filter-function.spec.ts index 1ffc1b119a8..de544a1a0d5 100644 --- a/libs/vault/src/models/filter-function.spec.ts +++ b/libs/vault/src/models/filter-function.spec.ts @@ -116,6 +116,14 @@ describe("createFilter", () => { expect(result).toBe(true); }); + + it("should return true when filtering on empty-string folder id", () => { + const filterFunction = createFilterFunction({ folderId: "" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); }); describe("given an organizational cipher (with organization and collections)", () => { diff --git a/libs/vault/src/models/filter-function.ts b/libs/vault/src/models/filter-function.ts index 0252ef13094..bbd4127863b 100644 --- a/libs/vault/src/models/filter-function.ts +++ b/libs/vault/src/models/filter-function.ts @@ -55,8 +55,11 @@ export function createFilterFunction( return false; } } + const isNoFolderFilter = filter.folderId === Unassigned || filter.folderId === ""; + const cipherHasFolder = cipher.folderId != null && cipher.folderId !== ""; + // No folder - if (filter.folderId === Unassigned && cipher.folderId != null) { + if (isNoFolderFilter && cipherHasFolder) { return false; } // Folder @@ -64,6 +67,7 @@ export function createFilterFunction( filter.folderId !== undefined && filter.folderId !== All && filter.folderId !== Unassigned && + filter.folderId !== "" && cipher.folderId !== filter.folderId ) { return false; diff --git a/libs/vault/src/models/routed-vault-filter-bridge.model.ts b/libs/vault/src/models/routed-vault-filter-bridge.model.ts index 43e508df3ea..32523684125 100644 --- a/libs/vault/src/models/routed-vault-filter-bridge.model.ts +++ b/libs/vault/src/models/routed-vault-filter-bridge.model.ts @@ -87,7 +87,7 @@ export class RoutedVaultFilterBridge implements VaultFilter { return this.legacyFilter.selectedFolderNode; } set selectedFolderNode(value: TreeNode) { - const folderId = value?.node.id ?? Unassigned; + const folderId = value?.node.id ? value.node.id : Unassigned; this.bridgeService.navigate({ ...this.routedFilter, folderId, diff --git a/libs/vault/src/models/vault-filter.model.ts b/libs/vault/src/models/vault-filter.model.ts index 4617102cebe..5452a9f5c38 100644 --- a/libs/vault/src/models/vault-filter.model.ts +++ b/libs/vault/src/models/vault-filter.model.ts @@ -134,7 +134,7 @@ export class VaultFilter { if (this.selectedFolderNode) { // No folder if (this.folderId === null && cipherPassesFilter) { - cipherPassesFilter = cipher.folderId === null; + cipherPassesFilter = cipher.folderId == null || cipher.folderId === ""; } // Folder if (this.folderId !== null && cipherPassesFilter) { diff --git a/libs/vault/src/services/routed-vault-filter-bridge.service.ts b/libs/vault/src/services/routed-vault-filter-bridge.service.ts index 1bff764964e..25c75f464f0 100644 --- a/libs/vault/src/services/routed-vault-filter-bridge.service.ts +++ b/libs/vault/src/services/routed-vault-filter-bridge.service.ts @@ -145,7 +145,7 @@ function createLegacyFilterForEndUser( ); } - if (filter.folderId !== undefined && filter.folderId === Unassigned) { + if (filter.folderId !== undefined && (filter.folderId === Unassigned || filter.folderId === "")) { legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, null); } else if (filter.folderId !== undefined && filter.folderId !== Unassigned) { legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, filter.folderId);