mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 09:13:33 +00:00
[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
This commit is contained in:
@@ -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<void>();
|
||||
protected organizations$: Observable<Organization[]>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user