1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +00:00

[EC-267] Unassigned collection has disappeared in web vault (#3000)

* feat: add unassigned collection to decrypted collections

* feat: add support for unassigned in individual vault

* fix: dont activate collection when not selected

* fix: remove collection selection completely when pruning

* feat: prune collection selection if selecting my vault

* fix: filter and only show organization ciphers when unassigned collection is selected

* fix: only show unassigned for admins

* feat: add unassigned logic to organizational vault buildFilter

* refactor: move buildFilter to VaultFilterModel

* chore: add buildFilter tests

* fix: bugs in filtering logic

* refactor: use VaultFilter.buildFilter on desktop

* chore: group and reword tests for better readability

* feat: add additional test

* fix: connect unassigned collection to organization

* fix: test by adding missing

* chore: tweak test group naming

* fix: change undefined to null to better reflect real values
This commit is contained in:
Andreas Coroiu
2022-07-08 10:18:07 +02:00
committed by GitHub
parent 30200c2c3c
commit ce2606b406
11 changed files with 309 additions and 111 deletions

View File

@@ -41,6 +41,7 @@ export class CollectionFilterComponent {
applyFilter(collection: CollectionView) {
this.activeFilter.resetFilter();
this.activeFilter.selectedCollection = true;
this.activeFilter.selectedCollectionId = collection.id;
this.onFilterChange.emit(this.activeFilter);
}

View File

@@ -0,0 +1,237 @@
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { VaultFilter } from "./vault-filter.model";
describe("VaultFilter", () => {
describe("filterFunction", () => {
describe("generic cipher", () => {
it("should return true when not filtering for anything specific", () => {
const cipher = createCipher();
const filterFunction = createFilterFunction({ status: "all" });
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
describe("given a favorite cipher", () => {
const cipher = createCipher({ favorite: true });
it("should return true when filtering for favorites", () => {
const filterFunction = createFilterFunction({ status: "favorites" });
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filtering for trash", () => {
const filterFunction = createFilterFunction({ status: "trash" });
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a deleted cipher", () => {
const cipher = createCipher({ deletedDate: new Date() });
it("should return true when filtering for trash", () => {
const filterFunction = createFilterFunction({ status: "trash" });
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filtering for favorites", () => {
const filterFunction = createFilterFunction({ status: "favorites" });
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a cipher with type", () => {
it("should return true when filter matches cipher type", () => {
const cipher = createCipher({ type: CipherType.Identity });
const filterFunction = createFilterFunction({ cipherType: CipherType.Identity });
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filter does not match cipher type", () => {
const cipher = createCipher({ type: CipherType.Card });
const filterFunction = createFilterFunction({ cipherType: CipherType.Identity });
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a cipher with folder id", () => {
it("should return true when filter matches folder id", () => {
const cipher = createCipher({ folderId: "folderId" });
const filterFunction = createFilterFunction({
selectedFolder: true,
selectedFolderId: "folderId",
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filter does not match folder id", () => {
const cipher = createCipher({ folderId: "folderId" });
const filterFunction = createFilterFunction({
selectedFolder: true,
selectedFolderId: "anotherFolderId",
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a cipher without folder", () => {
const cipher = createCipher({ folderId: null });
it("should return true when filtering on unassigned folder", () => {
const filterFunction = createFilterFunction({
selectedFolder: true,
selectedFolderId: null,
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
describe("given an organizational cipher (with organization and collections)", () => {
const cipher = createCipher({
organizationId: "organizationId",
collectionIds: ["collectionId", "anotherId"],
});
it("should return true when filter matches collection id", () => {
const filterFunction = createFilterFunction({
selectedCollection: true,
selectedCollectionId: "collectionId",
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filter does not match collection id", () => {
const filterFunction = createFilterFunction({
selectedCollection: true,
selectedCollectionId: "nonMatchingId",
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return false when filter does not match organization id", () => {
const filterFunction = createFilterFunction({
selectedOrganizationId: "anotherOrganizationId",
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return false when filtering for my vault only", () => {
const filterFunction = createFilterFunction({
myVaultOnly: true,
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
it("should return true when filtering for unassigned collection", () => {
const filterFunction = createFilterFunction({
selectedCollection: true,
selectedCollectionId: null,
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return true when filter matches organization id", () => {
const filterFunction = createFilterFunction({
selectedOrganizationId: "organizationId",
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
describe("given an individual cipher (without organization or collection)", () => {
const cipher = createCipher({ organizationId: null, collectionIds: [] });
it("should return false when filtering for unassigned collection", () => {
const filterFunction = createFilterFunction({
selectedCollection: true,
selectedCollectionId: null,
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return true when filtering for my vault only", () => {
const cipher = createCipher({ organizationId: null });
const filterFunction = createFilterFunction({
myVaultOnly: true,
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
});
});
function createFilterFunction(options: Partial<VaultFilter> = {}) {
return new VaultFilter(options).buildFilter();
}
function createCipher(options: Partial<CipherView> = {}) {
const cipher = new CipherView();
cipher.favorite = options.favorite ?? false;
cipher.deletedDate = options.deletedDate;
cipher.type = options.type;
cipher.folderId = options.folderId;
cipher.collectionIds = options.collectionIds;
cipher.organizationId = options.organizationId;
return cipher;
}

View File

@@ -1,9 +1,13 @@
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { CipherStatus } from "./cipher-status.model";
export type VaultFilterFunction = (cipher: CipherView) => boolean;
export class VaultFilter {
cipherType?: CipherType;
selectedCollection = false; // This is needed because of how the "Unassigned" collection works. It has a null id.
selectedCollectionId?: string;
status?: CipherStatus;
selectedFolder = false; // This is needed because of how the "No Folder" folder works. It has a null id.
@@ -19,6 +23,7 @@ export class VaultFilter {
resetFilter() {
this.cipherType = null;
this.status = null;
this.selectedCollection = false;
this.selectedCollectionId = null;
this.selectedFolder = false;
this.selectedFolderId = null;
@@ -29,4 +34,41 @@ export class VaultFilter {
this.selectedOrganizationId = null;
this.resetFilter();
}
buildFilter(): VaultFilterFunction {
return (cipher) => {
let cipherPassesFilter = true;
if (this.status === "favorites" && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.status === "trash" && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.cipherType != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.cipherType;
}
if (this.selectedFolder && this.selectedFolderId == null && cipherPassesFilter) {
cipherPassesFilter = cipher.folderId == null;
}
if (this.selectedFolder && this.selectedFolderId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.folderId === this.selectedFolderId;
}
if (this.selectedCollection && this.selectedCollectionId == null && cipherPassesFilter) {
cipherPassesFilter =
cipher.organizationId != null &&
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
}
if (this.selectedCollection && this.selectedCollectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null && cipher.collectionIds.includes(this.selectedCollectionId);
}
if (this.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.selectedOrganizationId;
}
if (this.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
}

View File

@@ -111,9 +111,12 @@ export class VaultFilterComponent implements OnInit {
protected pruneInvalidCollectionSelection(filter: VaultFilter): VaultFilter {
if (
filter.selectedCollectionId != null &&
!this.collections?.hasId(filter.selectedCollectionId)
filter.myVaultOnly ||
(filter.selectedCollection &&
filter.selectedCollectionId != null &&
!this.collections?.hasId(filter.selectedCollectionId))
) {
filter.selectedCollection = false;
filter.selectedCollectionId = null;
}
return filter;