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 }}
-
-