From 09ef9dd4110a0d818f8071bcab52a508c49ba339 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 27 Jun 2023 09:21:07 -0400 Subject: [PATCH 01/11] Fix code owners format issue (#5689) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cc8eb66289..9548168f85 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,7 +4,7 @@ # The following owners will be the default owners for everything in the repo. # Unless a later match takes precedence -@bitwarden/team-leads +* @bitwarden/team-leads-eng ## Secrets Manager team files ## bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev From 683b7fea77c0a1d199c34b9c2e518124b7a8534a Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 27 Jun 2023 11:36:48 -0400 Subject: [PATCH 02/11] [AC-1120] Implement 'New' button dropdown in Individual Vault (#5235) * Change 'New' button to dropdown with folders and collections * Individual vault changes to support adding collections * Add org selector to CollectionDialogComponent * Implement CollectionService.upsert() in CollectionAdminService.save() * Filter collections to ones that users can create collections in * Filter organizations by ones the user can create a collection in * CollectionDialog observable updates * Remove CollectionService.upsert from CollectionAdminService and return collection on save from CollectionDialog. * Filter out collections that the user does not have access to in collection dialog for Individual Vault. * Remove add folder action from vault filter * Remove add button from filters as it is no longer used * Update comment to reference future ticket * Change CollectionDialogResult from a class to an interface * Remove extra call to loadOrg() in the case of opening the modal from the individual vault * Use async pipe instead of subscribe for organizations --- .../collection-dialog.component.html | 13 +++ .../collection-dialog.component.ts | 81 +++++++++++++++---- .../vault/core/collection-admin.service.ts | 6 +- .../components/vault-filter.component.ts | 9 --- .../vault-filter-section.component.html | 10 --- .../vault-filter-section.component.ts | 4 - .../vault-header/vault-header.component.html | 30 ++++++- .../vault-header/vault-header.component.ts | 23 ++++++ .../individual-vault/vault.component.html | 4 +- .../vault/individual-vault/vault.component.ts | 39 +++++++-- .../app/vault/org-vault/vault.component.ts | 12 ++- 11 files changed, 174 insertions(+), 57 deletions(-) diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index 785afc0e95..e39dcb12ef 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -22,6 +22,19 @@ + + {{ "organization" | i18n }} + + + + + + {{ "externalId" | i18n }} diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index ac3c62a5b8..74b7d0fdd3 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -1,7 +1,16 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + map, + Observable, + of, + shareReplay, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog"; import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; @@ -10,6 +19,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { BitValidators } from "@bitwarden/components"; @@ -35,9 +46,16 @@ export interface CollectionDialogParams { organizationId: string; initialTab?: CollectionDialogTabType; parentCollectionId?: string; + showOrgSelector?: boolean; + collectionIds?: string[]; } -export enum CollectionDialogResult { +export interface CollectionDialogResult { + action: CollectionDialogAction; + collection: CollectionResponse; +} + +export enum CollectionDialogAction { Saved = "saved", Canceled = "canceled", Deleted = "deleted", @@ -48,6 +66,7 @@ export enum CollectionDialogResult { }) export class CollectionDialogComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + protected organizations$: Observable; protected tabIndex: CollectionDialogTabType; protected loading = true; @@ -56,11 +75,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { protected nestOptions: CollectionView[] = []; protected accessItems: AccessItemView[] = []; protected deletedParentName: string | undefined; + protected showOrgSelector = false; protected formGroup = this.formBuilder.group({ name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]], externalId: "", parent: undefined as string | undefined, access: [[] as AccessItemValue[]], + selectedOrg: "", }); protected PermissionMode = PermissionMode; @@ -79,8 +100,31 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; } - ngOnInit() { - const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe( + async ngOnInit() { + // Opened from the individual vault + if (this.params.showOrgSelector) { + this.showOrgSelector = true; + this.formGroup.controls.selectedOrg.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((id) => this.loadOrg(id, this.params.collectionIds)); + this.organizations$ = this.organizationService.organizations$.pipe( + map((orgs) => + orgs + .filter((o) => o.canCreateNewCollections) + .sort(Utils.getSortFunction(this.i18nService, "name")) + ) + ); + // patchValue will trigger a call to loadOrg() in this case, so no need to call it again here + this.formGroup.patchValue({ selectedOrg: this.params.organizationId }); + } else { + // Opened from the org vault + this.formGroup.patchValue({ selectedOrg: this.params.organizationId }); + this.loadOrg(this.params.organizationId, this.params.collectionIds); + } + } + + async loadOrg(orgId: string, collectionIds: string[]) { + const organization$ = of(this.organizationService.get(orgId)).pipe( shareReplay({ refCount: true, bufferSize: 1 }) ); const groups$ = organization$.pipe( @@ -89,20 +133,19 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { return of([] as GroupView[]); } - return this.groupService.getAll(this.params.organizationId); + return this.groupService.getAll(orgId); }) ); - combineLatest({ organization: organization$, - collections: this.collectionService.getAll(this.params.organizationId), + collections: this.collectionService.getAll(orgId), collectionDetails: this.params.collectionId - ? this.collectionService.get(this.params.organizationId, this.params.collectionId) + ? this.collectionService.get(orgId, this.params.collectionId) : of(null), groups: groups$, - users: this.organizationUserService.getAllUsers(this.params.organizationId), + users: this.organizationUserService.getAllUsers(orgId), }) - .pipe(takeUntil(this.destroy$)) + .pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$)) .subscribe(({ organization, collections, collectionDetails, groups, users }) => { this.organization = organization; this.accessItems = [].concat( @@ -110,6 +153,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { users.data.map(mapUserToAccessItemView) ); + if (collectionIds) { + collections = collections.filter((c) => collectionIds.includes(c.id)); + } + if (this.params.collectionId) { this.collection = collections.find((c) => c.id === this.collectionId); this.nestOptions = collections.filter((c) => c.id !== this.collectionId); @@ -149,7 +196,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } protected async cancel() { - this.close(CollectionDialogResult.Canceled); + this.close(CollectionDialogAction.Canceled); } protected submit = async () => { @@ -168,7 +215,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const collectionView = new CollectionAdminView(); collectionView.id = this.params.collectionId; - collectionView.organizationId = this.params.organizationId; + collectionView.organizationId = this.formGroup.controls.selectedOrg.value; collectionView.externalId = this.formGroup.controls.externalId.value; collectionView.groups = this.formGroup.controls.access.value .filter((v) => v.type === AccessItemType.Group) @@ -184,7 +231,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { collectionView.name = this.formGroup.controls.name.value; } - await this.collectionService.save(collectionView); + const savedCollection = await this.collectionService.save(collectionView); this.platformUtilsService.showToast( "success", @@ -195,7 +242,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { ) ); - this.close(CollectionDialogResult.Saved); + this.close(CollectionDialogAction.Saved, savedCollection); }; protected delete = async () => { @@ -217,7 +264,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.i18nService.t("deletedCollectionId", this.collection?.name) ); - this.close(CollectionDialogResult.Deleted); + this.close(CollectionDialogAction.Deleted); }; ngOnDestroy(): void { @@ -225,8 +272,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private close(result: CollectionDialogResult) { - this.dialogRef.close(result); + private close(action: CollectionDialogAction, collection?: CollectionResponse) { + this.dialogRef.close({ action, collection } as CollectionDialogResult); } } diff --git a/apps/web/src/app/vault/core/collection-admin.service.ts b/apps/web/src/app/vault/core/collection-admin.service.ts index efeb3817ea..08e94e58bd 100644 --- a/apps/web/src/app/vault/core/collection-admin.service.ts +++ b/apps/web/src/app/vault/core/collection-admin.service.ts @@ -46,7 +46,7 @@ export class CollectionAdminService { return view; } - async save(collection: CollectionAdminView): Promise { + async save(collection: CollectionAdminView): Promise { const request = await this.encrypt(collection); let response: CollectionResponse; @@ -61,9 +61,7 @@ export class CollectionAdminService { ); } - // TODO: Implement upsert when in PS-1083: Collection Service refactors - // await this.collectionService.upsert(data); - return; + return response; } async delete(organizationId: string, collectionId: string): Promise { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 0cce8df38b..084573446f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -32,7 +32,6 @@ import { OrganizationOptionsComponent } from "./organization-options.component"; export class VaultFilterComponent implements OnInit, OnDestroy { filters?: VaultFilterList; @Input() activeFilter: VaultFilter = new VaultFilter(); - @Output() onAddFolder = new EventEmitter(); @Output() onEditFolder = new EventEmitter(); @Input() searchText = ""; @@ -142,10 +141,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { filter.selectedCollectionNode = collectionNode; }; - addFolder = async (): Promise => { - this.onAddFolder.emit(); - }; - editFolder = async (folder: FolderFilter): Promise => { this.onEditFolder.emit(folder); }; @@ -249,10 +244,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { text: "editFolder", action: this.editFolder, }, - add: { - text: "Add Folder", - action: this.addFolder, - }, }; return folderFilterSection; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html index b26b827274..bcd151193a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html @@ -34,16 +34,6 @@

 {{ headerNode.node.name | i18n }}

- -