1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-24227] Enable TS-strict for Collection Domain models (#15765)

* wip ts-strict

* wip ts-strict

* wip

* cleanup

* cleanup

* fix story

* fix story

* fix story

* wip

* clean up CollectionAdminView construction

* fix deprecated function call

* fix cli

* clean up

* fix story

* wip

* fix cli

* requested changes

* clean up, fixing minor bugs, more type saftey

* assign props in static ctor, clean up
This commit is contained in:
Brandon Treston
2025-08-14 13:08:24 -04:00
committed by GitHub
parent ac7e873813
commit 27089fbb57
41 changed files with 537 additions and 379 deletions

View File

@@ -1,5 +1,7 @@
import { CollectionView } from "@bitwarden/admin-console/common";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { newGuid } from "@bitwarden/guid";
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
@@ -9,11 +11,17 @@ describe("CollectionUtils Service", () => {
// Arrange
const collections: CollectionView[] = [];
const parentCollection = new CollectionView();
parentCollection.name = "Parent";
const parentCollection = new CollectionView({
name: "Parent",
organizationId: "orgId" as OrganizationId,
id: newGuid() as CollectionId,
});
const childCollection = new CollectionView();
childCollection.name = "Parent/Child";
const childCollection = new CollectionView({
name: "Parent/Child",
organizationId: "orgId" as OrganizationId,
id: newGuid() as CollectionId,
});
collections.push(childCollection);
collections.push(parentCollection);
@@ -41,12 +49,14 @@ describe("CollectionUtils Service", () => {
describe("getFlatCollectionTree", () => {
it("should flatten a tree node with no children", () => {
// Arrange
const collection = new CollectionView();
collection.name = "Test Collection";
collection.id = "test-id";
const collection = new CollectionView({
name: "Test Collection",
id: "test-id" as CollectionId,
organizationId: "orgId" as OrganizationId,
});
const treeNodes: TreeNode<CollectionView>[] = [
new TreeNode<CollectionView>(collection, null),
new TreeNode<CollectionView>(collection, {} as TreeNode<CollectionView>),
];
// Act
@@ -59,23 +69,34 @@ describe("CollectionUtils Service", () => {
it("should flatten a tree node with children", () => {
// Arrange
const parentCollection = new CollectionView();
parentCollection.name = "Parent";
parentCollection.id = "parent-id";
const parentCollection = new CollectionView({
name: "Parent",
id: "parent-id" as CollectionId,
organizationId: "orgId" as OrganizationId,
});
const child1Collection = new CollectionView();
child1Collection.name = "Child 1";
child1Collection.id = "child1-id";
const child1Collection = new CollectionView({
name: "Child 1",
id: "child1-id" as CollectionId,
organizationId: "orgId" as OrganizationId,
});
const child2Collection = new CollectionView();
child2Collection.name = "Child 2";
child2Collection.id = "child2-id";
const child2Collection = new CollectionView({
name: "Child 2",
id: "child2-id" as CollectionId,
organizationId: "orgId" as OrganizationId,
});
const grandchildCollection = new CollectionView();
grandchildCollection.name = "Grandchild";
grandchildCollection.id = "grandchild-id";
const grandchildCollection = new CollectionView({
name: "Grandchild",
id: "grandchild-id" as CollectionId,
organizationId: "orgId" as OrganizationId,
});
const parentNode = new TreeNode<CollectionView>(parentCollection, null);
const parentNode = new TreeNode<CollectionView>(
parentCollection,
{} as TreeNode<CollectionView>,
);
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);

View File

@@ -22,7 +22,7 @@ export function getNestedCollectionTree(
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
// modifies the names of collections.
// These changes risk affecting collections store in StateService.
const clonedCollections = collections
const clonedCollections: CollectionView[] | CollectionAdminView[] = collections
.sort((a, b) => a.name.localeCompare(b.name))
.map(cloneCollection);
@@ -37,6 +37,21 @@ export function getNestedCollectionTree(
return nodes;
}
export function cloneCollection(collection: CollectionView): CollectionView;
export function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
export function cloneCollection(
collection: CollectionView | CollectionAdminView,
): CollectionView | CollectionAdminView {
let cloned;
if (collection instanceof CollectionAdminView) {
cloned = Object.assign(new CollectionAdminView({ ...collection }), collection);
} else {
cloned = Object.assign(new CollectionView({ ...collection }), collection);
}
return cloned;
}
export function getFlatCollectionTree(
nodes: TreeNode<CollectionAdminView>[],
): CollectionAdminView[];
@@ -57,32 +72,3 @@ export function getFlatCollectionTree(
return [node.node, ...children];
});
}
function cloneCollection(collection: CollectionView): CollectionView;
function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
function cloneCollection(
collection: CollectionView | CollectionAdminView,
): CollectionView | CollectionAdminView {
let cloned;
if (collection instanceof CollectionAdminView) {
cloned = new CollectionAdminView();
cloned.groups = [...collection.groups];
cloned.users = [...collection.users];
cloned.assigned = collection.assigned;
cloned.unmanaged = collection.unmanaged;
} else {
cloned = new CollectionView();
}
cloned.id = collection.id;
cloned.externalId = collection.externalId;
cloned.hidePasswords = collection.hidePasswords;
cloned.name = collection.name;
cloned.organizationId = collection.organizationId;
cloned.readOnly = collection.readOnly;
cloned.manage = collection.manage;
cloned.type = collection.type;
return cloned;
}

View File

@@ -391,11 +391,13 @@ export class VaultComponent implements OnInit, OnDestroy {
// FIXME: We should not assert that the Unassigned type is a CollectionId.
// Instead we should consider representing the Unassigned collection as a different object, given that
// it is not actually a collection.
const noneCollection = new CollectionAdminView();
noneCollection.name = this.i18nService.t("unassigned");
noneCollection.id = Unassigned as CollectionId;
noneCollection.organizationId = organizationId;
return allCollections.concat(noneCollection);
return allCollections.concat(
new CollectionAdminView({
name: this.i18nService.t("unassigned"),
id: Unassigned as CollectionId,
organizationId,
}),
);
}),
);

View File

@@ -253,8 +253,8 @@ export class GroupsComponent {
private toCollectionMap(
response: ListResponse<CollectionResponse>,
): Observable<Record<string, CollectionView>> {
const collections = response.data.map(
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
const collections = response.data.map((r) =>
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
);
return this.accountService.activeAccount$.pipe(

View File

@@ -312,7 +312,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
async getCollectionNameMap() {
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
map((res) =>
res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))),
res.data.map((r) =>
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
),
),
);

View File

@@ -399,9 +399,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
return;
}
const collectionView = new CollectionAdminView();
collectionView.id = this.params.collectionId;
collectionView.organizationId = this.formGroup.controls.selectedOrg.value;
const parent = this.formGroup.controls.parent?.value;
const collectionView = new CollectionAdminView({
id: this.params.collectionId as CollectionId,
organizationId: this.formGroup.controls.selectedOrg.value,
name: parent
? `${parent}/${this.formGroup.controls.name.value}`
: this.formGroup.controls.name.value,
});
collectionView.externalId = this.formGroup.controls.externalId.value;
collectionView.groups = this.formGroup.controls.access.value
.filter((v) => v.type === AccessItemType.Group)
@@ -410,13 +415,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
.filter((v) => v.type === AccessItemType.Member)
.map(convertToSelectionView);
const parent = this.formGroup.controls.parent.value;
if (parent) {
collectionView.name = `${parent}/${this.formGroup.controls.name.value}`;
} else {
collectionView.name = this.formGroup.controls.name.value;
}
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const savedCollection = await this.collectionAdminService.save(collectionView, userId);

View File

@@ -224,7 +224,7 @@ export class VaultItemsComponent<C extends CipherViewLike> {
}
protected canEditCollection(collection: CollectionView): boolean {
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
// Only allow deletion if collection editing is enabled and not deleting "Unassigned"
if (collection.id === Unassigned) {
return false;
}
@@ -235,7 +235,7 @@ export class VaultItemsComponent<C extends CipherViewLike> {
}
protected canDeleteCollection(collection: CollectionView): boolean {
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
// Only allow deletion if collection editing is enabled and not deleting "Unassigned"
if (collection.id === Unassigned) {
return false;
}

View File

@@ -262,9 +262,11 @@ export const OrganizationTrash: Story = {
},
};
const unassignedCollection = new CollectionAdminView();
unassignedCollection.id = Unassigned as CollectionId;
unassignedCollection.name = "Unassigned";
const unassignedCollection = new CollectionAdminView({
id: Unassigned as CollectionId,
name: "Unassigned",
organizationId: "org id" as OrganizationId,
});
export const OrganizationTopLevelCollection: Story = {
args: {
ciphers: [],
@@ -327,11 +329,11 @@ function createCipherView(i: number, deleted = false): CipherView {
function createCollectionView(i: number): CollectionAdminView {
const organization = organizations[i % (organizations.length + 1)];
const group = groups[i % (groups.length + 1)];
const view = new CollectionAdminView();
view.id = `collection-${i}` as CollectionId;
view.name = `Collection ${i}`;
view.organizationId = organization?.id;
view.manage = true;
const view = new CollectionAdminView({
id: `collection-${i}` as CollectionId,
name: `Collection ${i}`,
organizationId: organization?.id ?? ("orgId" as OrganizationId),
});
if (group !== undefined) {
view.groups = [
@@ -344,6 +346,7 @@ function createCollectionView(i: number): CollectionAdminView {
];
}
view.manage = true;
return view;
}

View File

@@ -21,7 +21,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -368,11 +368,16 @@ describe("vault filter service", () => {
orgId: string,
type?: CollectionType,
): CollectionView {
const collection = new CollectionView();
collection.id = id;
collection.name = name;
collection.organizationId = orgId;
collection.type = type || CollectionTypes.SharedCollection;
const collection = new CollectionView({
id: id as CollectionId,
name,
organizationId: orgId as OrganizationId,
});
if (type) {
collection.type = type;
}
return collection;
}
});

View File

@@ -12,11 +12,7 @@ import {
switchMap,
} from "rxjs";
import {
CollectionAdminView,
CollectionService,
CollectionView,
} from "@bitwarden/admin-console/common";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -38,6 +34,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
import { CipherListView } from "@bitwarden/sdk-internal";
import { cloneCollection } from "@bitwarden/web-vault/app/admin-console/organizations/collections";
import {
CipherTypeFilter,
@@ -253,14 +250,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
}
collections.forEach((c) => {
const collectionCopy = new CollectionView() as CollectionFilter;
collectionCopy.id = c.id;
collectionCopy.organizationId = c.organizationId;
const collectionCopy = cloneCollection(new CollectionView({ ...c })) as CollectionFilter;
collectionCopy.icon = "bwi-collection-shared";
if (c instanceof CollectionAdminView) {
collectionCopy.groups = c.groups;
collectionCopy.assigned = c.assigned;
}
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
});
@@ -274,7 +265,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
}
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
const head = new CollectionView() as CollectionFilter;
const head = CollectionView.vaultFilterHead() as CollectionFilter;
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
}

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CollectionId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -224,7 +225,9 @@ describe("VaultFilter", () => {
it("should return false when filtering by All Collections", () => {
const filterFunction = createFilterFunction({
selectedCollectionNode: createCollectionFilterNode({ id: "AllCollections" }),
selectedCollectionNode: createCollectionFilterNode({
id: "AllCollections" as CollectionId,
}),
});
const result = filterFunction(cipher);
@@ -309,15 +312,12 @@ function createFolderFilterNode(options: Partial<FolderFilter>): TreeNode<Folder
function createCollectionFilterNode(
options: Partial<CollectionFilter>,
): TreeNode<CollectionFilter> {
const collection = new CollectionView() as CollectionFilter;
collection.id = options.id;
collection.name = options.name ?? "";
collection.icon = options.icon ?? "";
collection.organizationId = options.organizationId;
collection.externalId = options.externalId ?? "";
collection.readOnly = options.readOnly ?? false;
collection.hidePasswords = options.hidePasswords ?? false;
return new TreeNode<CollectionFilter>(collection, null);
const collection = new CollectionView({
name: options.name ?? "Test Name",
id: options.id ?? null,
organizationId: options.organizationId ?? "Org Id",
}) as CollectionFilter;
return new TreeNode<CollectionFilter>(collection, {} as TreeNode<CollectionFilter>);
}
function createCipher(options: Partial<CipherView> = {}) {