1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-02 08:33:43 +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);