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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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> = {}) {
|
||||
|
||||
Reference in New Issue
Block a user