mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01: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:
parent
09ef9dd411
commit
683b7fea77
@ -22,6 +22,19 @@
|
|||||||
<input bitInput appAutofocus formControlName="name" />
|
<input bitInput appAutofocus formControlName="name" />
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
|
<bit-form-field *ngIf="showOrgSelector">
|
||||||
|
<bit-label>{{ "organization" | i18n }}</bit-label>
|
||||||
|
<bit-select bitInput formControlName="selectedOrg">
|
||||||
|
<bit-option
|
||||||
|
*ngFor="let org of organizations$ | async"
|
||||||
|
icon="bwi-business"
|
||||||
|
[value]="org.id"
|
||||||
|
[label]="org.name"
|
||||||
|
>
|
||||||
|
</bit-option>
|
||||||
|
</bit-select>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="externalId" />
|
<input bitInput formControlName="externalId" />
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
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 { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { BitValidators } from "@bitwarden/components";
|
import { BitValidators } from "@bitwarden/components";
|
||||||
|
|
||||||
@ -35,9 +46,16 @@ export interface CollectionDialogParams {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
initialTab?: CollectionDialogTabType;
|
initialTab?: CollectionDialogTabType;
|
||||||
parentCollectionId?: string;
|
parentCollectionId?: string;
|
||||||
|
showOrgSelector?: boolean;
|
||||||
|
collectionIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CollectionDialogResult {
|
export interface CollectionDialogResult {
|
||||||
|
action: CollectionDialogAction;
|
||||||
|
collection: CollectionResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CollectionDialogAction {
|
||||||
Saved = "saved",
|
Saved = "saved",
|
||||||
Canceled = "canceled",
|
Canceled = "canceled",
|
||||||
Deleted = "deleted",
|
Deleted = "deleted",
|
||||||
@ -48,6 +66,7 @@ export enum CollectionDialogResult {
|
|||||||
})
|
})
|
||||||
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
protected organizations$: Observable<Organization[]>;
|
||||||
|
|
||||||
protected tabIndex: CollectionDialogTabType;
|
protected tabIndex: CollectionDialogTabType;
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
@ -56,11 +75,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
protected nestOptions: CollectionView[] = [];
|
protected nestOptions: CollectionView[] = [];
|
||||||
protected accessItems: AccessItemView[] = [];
|
protected accessItems: AccessItemView[] = [];
|
||||||
protected deletedParentName: string | undefined;
|
protected deletedParentName: string | undefined;
|
||||||
|
protected showOrgSelector = false;
|
||||||
protected formGroup = this.formBuilder.group({
|
protected formGroup = this.formBuilder.group({
|
||||||
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
|
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
|
||||||
externalId: "",
|
externalId: "",
|
||||||
parent: undefined as string | undefined,
|
parent: undefined as string | undefined,
|
||||||
access: [[] as AccessItemValue[]],
|
access: [[] as AccessItemValue[]],
|
||||||
|
selectedOrg: "",
|
||||||
});
|
});
|
||||||
protected PermissionMode = PermissionMode;
|
protected PermissionMode = PermissionMode;
|
||||||
|
|
||||||
@ -79,8 +100,31 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe(
|
// 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 })
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
);
|
);
|
||||||
const groups$ = organization$.pipe(
|
const groups$ = organization$.pipe(
|
||||||
@ -89,20 +133,19 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
return of([] as GroupView[]);
|
return of([] as GroupView[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.groupService.getAll(this.params.organizationId);
|
return this.groupService.getAll(orgId);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
combineLatest({
|
combineLatest({
|
||||||
organization: organization$,
|
organization: organization$,
|
||||||
collections: this.collectionService.getAll(this.params.organizationId),
|
collections: this.collectionService.getAll(orgId),
|
||||||
collectionDetails: this.params.collectionId
|
collectionDetails: this.params.collectionId
|
||||||
? this.collectionService.get(this.params.organizationId, this.params.collectionId)
|
? this.collectionService.get(orgId, this.params.collectionId)
|
||||||
: of(null),
|
: of(null),
|
||||||
groups: groups$,
|
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 }) => {
|
.subscribe(({ organization, collections, collectionDetails, groups, users }) => {
|
||||||
this.organization = organization;
|
this.organization = organization;
|
||||||
this.accessItems = [].concat(
|
this.accessItems = [].concat(
|
||||||
@ -110,6 +153,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
users.data.map(mapUserToAccessItemView)
|
users.data.map(mapUserToAccessItemView)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (collectionIds) {
|
||||||
|
collections = collections.filter((c) => collectionIds.includes(c.id));
|
||||||
|
}
|
||||||
|
|
||||||
if (this.params.collectionId) {
|
if (this.params.collectionId) {
|
||||||
this.collection = collections.find((c) => c.id === this.collectionId);
|
this.collection = collections.find((c) => c.id === this.collectionId);
|
||||||
this.nestOptions = collections.filter((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() {
|
protected async cancel() {
|
||||||
this.close(CollectionDialogResult.Canceled);
|
this.close(CollectionDialogAction.Canceled);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected submit = async () => {
|
protected submit = async () => {
|
||||||
@ -168,7 +215,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const collectionView = new CollectionAdminView();
|
const collectionView = new CollectionAdminView();
|
||||||
collectionView.id = this.params.collectionId;
|
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.externalId = this.formGroup.controls.externalId.value;
|
||||||
collectionView.groups = this.formGroup.controls.access.value
|
collectionView.groups = this.formGroup.controls.access.value
|
||||||
.filter((v) => v.type === AccessItemType.Group)
|
.filter((v) => v.type === AccessItemType.Group)
|
||||||
@ -184,7 +231,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
collectionView.name = this.formGroup.controls.name.value;
|
collectionView.name = this.formGroup.controls.name.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.collectionService.save(collectionView);
|
const savedCollection = await this.collectionService.save(collectionView);
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
@ -195,7 +242,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.close(CollectionDialogResult.Saved);
|
this.close(CollectionDialogAction.Saved, savedCollection);
|
||||||
};
|
};
|
||||||
|
|
||||||
protected delete = async () => {
|
protected delete = async () => {
|
||||||
@ -217,7 +264,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.i18nService.t("deletedCollectionId", this.collection?.name)
|
this.i18nService.t("deletedCollectionId", this.collection?.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.close(CollectionDialogResult.Deleted);
|
this.close(CollectionDialogAction.Deleted);
|
||||||
};
|
};
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -225,8 +272,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private close(result: CollectionDialogResult) {
|
private close(action: CollectionDialogAction, collection?: CollectionResponse) {
|
||||||
this.dialogRef.close(result);
|
this.dialogRef.close({ action, collection } as CollectionDialogResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export class CollectionAdminService {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(collection: CollectionAdminView): Promise<unknown> {
|
async save(collection: CollectionAdminView): Promise<CollectionResponse> {
|
||||||
const request = await this.encrypt(collection);
|
const request = await this.encrypt(collection);
|
||||||
|
|
||||||
let response: CollectionResponse;
|
let response: CollectionResponse;
|
||||||
@ -61,9 +61,7 @@ export class CollectionAdminService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement upsert when in PS-1083: Collection Service refactors
|
return response;
|
||||||
// await this.collectionService.upsert(data);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(organizationId: string, collectionId: string): Promise<void> {
|
async delete(organizationId: string, collectionId: string): Promise<void> {
|
||||||
|
@ -32,7 +32,6 @@ import { OrganizationOptionsComponent } from "./organization-options.component";
|
|||||||
export class VaultFilterComponent implements OnInit, OnDestroy {
|
export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||||
filters?: VaultFilterList;
|
filters?: VaultFilterList;
|
||||||
@Input() activeFilter: VaultFilter = new VaultFilter();
|
@Input() activeFilter: VaultFilter = new VaultFilter();
|
||||||
@Output() onAddFolder = new EventEmitter<never>();
|
|
||||||
@Output() onEditFolder = new EventEmitter<FolderFilter>();
|
@Output() onEditFolder = new EventEmitter<FolderFilter>();
|
||||||
|
|
||||||
@Input() searchText = "";
|
@Input() searchText = "";
|
||||||
@ -142,10 +141,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
filter.selectedCollectionNode = collectionNode;
|
filter.selectedCollectionNode = collectionNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
addFolder = async (): Promise<void> => {
|
|
||||||
this.onAddFolder.emit();
|
|
||||||
};
|
|
||||||
|
|
||||||
editFolder = async (folder: FolderFilter): Promise<void> => {
|
editFolder = async (folder: FolderFilter): Promise<void> => {
|
||||||
this.onEditFolder.emit(folder);
|
this.onEditFolder.emit(folder);
|
||||||
};
|
};
|
||||||
@ -249,10 +244,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
text: "editFolder",
|
text: "editFolder",
|
||||||
action: this.editFolder,
|
action: this.editFolder,
|
||||||
},
|
},
|
||||||
add: {
|
|
||||||
text: "Add Folder",
|
|
||||||
action: this.addFolder,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return folderFilterSection;
|
return folderFilterSection;
|
||||||
}
|
}
|
||||||
|
@ -34,16 +34,6 @@
|
|||||||
<h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
|
<h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
|
||||||
{{ headerNode.node.name | i18n }}
|
{{ headerNode.node.name | i18n }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
*ngIf="showAddButton"
|
|
||||||
(click)="onAdd()"
|
|
||||||
class="text-muted ml-auto add-button"
|
|
||||||
appA11yTitle="{{ addInfo.text | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
id="{{ headerNode.node.name }}-filters"
|
id="{{ headerNode.node.name }}-filters"
|
||||||
|
@ -87,10 +87,6 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
|||||||
return this.section.add;
|
return this.section.add;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showAddButton() {
|
|
||||||
return this.section.add && !this.section.add.route;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showAddLink() {
|
get showAddLink() {
|
||||||
return this.section.add && this.section.add.route;
|
return this.section.add && this.section.add.route;
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
|
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
|
||||||
<button type="button" bitButton buttonType="primary" (click)="addCipher()">
|
<div appListDropdown>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
<button
|
||||||
{{ "newItem" | i18n }}
|
bitButton
|
||||||
</button>
|
buttonType="primary"
|
||||||
|
type="button"
|
||||||
|
[bitMenuTriggerFor]="addOptions"
|
||||||
|
id="newItemDropdown"
|
||||||
|
appA11yTitle="{{ 'new' | i18n }}"
|
||||||
|
>
|
||||||
|
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||||
|
<button type="button" bitMenuItem (click)="addCipher()">
|
||||||
|
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
|
||||||
|
{{ "item" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="addFolder()">
|
||||||
|
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||||
|
{{ "folder" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
|
||||||
|
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||||
|
{{ "collection" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,11 +39,26 @@ export class VaultHeaderComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() collection?: TreeNode<CollectionView>;
|
@Input() collection?: TreeNode<CollectionView>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether 'Collection' option is shown in the 'New' dropdown
|
||||||
|
*/
|
||||||
|
@Input() canCreateCollections: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits an event when the new item button is clicked in the header
|
* Emits an event when the new item button is clicked in the header
|
||||||
*/
|
*/
|
||||||
@Output() onAddCipher = new EventEmitter<void>();
|
@Output() onAddCipher = new EventEmitter<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event when the new collection button is clicked in the 'New' dropdown menu
|
||||||
|
*/
|
||||||
|
@Output() onAddCollection = new EventEmitter<null>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event when the new folder button is clicked in the 'New' dropdown menu
|
||||||
|
*/
|
||||||
|
@Output() onAddFolder = new EventEmitter<null>();
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,4 +130,12 @@ export class VaultHeaderComponent {
|
|||||||
protected addCipher() {
|
protected addCipher() {
|
||||||
this.onAddCipher.emit();
|
this.onAddCipher.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addFolder(): Promise<void> {
|
||||||
|
this.onAddFolder.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCollection(): Promise<void> {
|
||||||
|
this.onAddCollection.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
[activeFilter]="activeFilter"
|
[activeFilter]="activeFilter"
|
||||||
[searchText]="currentSearchText$ | async"
|
[searchText]="currentSearchText$ | async"
|
||||||
(searchTextChanged)="filterSearchText($event)"
|
(searchTextChanged)="filterSearchText($event)"
|
||||||
(onAddFolder)="addFolder()"
|
|
||||||
(onEditFolder)="editFolder($event)"
|
(onEditFolder)="editFolder($event)"
|
||||||
></app-vault-filter>
|
></app-vault-filter>
|
||||||
</div>
|
</div>
|
||||||
@ -21,8 +20,11 @@
|
|||||||
[filter]="filter"
|
[filter]="filter"
|
||||||
[loading]="refreshing && !performingInitialLoad"
|
[loading]="refreshing && !performingInitialLoad"
|
||||||
[organizations]="allOrganizations"
|
[organizations]="allOrganizations"
|
||||||
|
[canCreateCollections]="canCreateCollections"
|
||||||
[collection]="selectedCollection"
|
[collection]="selectedCollection"
|
||||||
(onAddCipher)="addCipher()"
|
(onAddCipher)="addCipher()"
|
||||||
|
(onAddCollection)="addCollection()"
|
||||||
|
(onAddFolder)="addFolder()"
|
||||||
></app-vault-header>
|
></app-vault-header>
|
||||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
|
@ -54,11 +54,14 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
|
|||||||
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||||
|
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { Icons } from "@bitwarden/components";
|
import { Icons } from "@bitwarden/components";
|
||||||
|
|
||||||
import { UpdateKeyComponent } from "../../settings/update-key.component";
|
import { UpdateKeyComponent } from "../../settings/update-key.component";
|
||||||
|
import { CollectionDialogAction, openCollectionDialog } from "../components/collection-dialog";
|
||||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||||
|
|
||||||
@ -140,6 +143,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected collections: CollectionView[];
|
protected collections: CollectionView[];
|
||||||
protected isEmpty: boolean;
|
protected isEmpty: boolean;
|
||||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||||
|
protected canCreateCollections = false;
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
|
|
||||||
private searchText$ = new Subject<string>();
|
private searchText$ = new Subject<string>();
|
||||||
@ -234,12 +238,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const canAccessPremium$ = Utils.asyncToObservable(() =>
|
const canAccessPremium$ = Utils.asyncToObservable(() =>
|
||||||
this.stateService.getCanAccessPremium()
|
this.stateService.getCanAccessPremium()
|
||||||
).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
const allCollections$ = Utils.asyncToObservable(() =>
|
const allCollections$ = Utils.asyncToObservable(() => this.collectionService.getAllDecrypted());
|
||||||
this.collectionService.getAllDecrypted()
|
|
||||||
).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
|
||||||
const nestedCollections$ = allCollections$.pipe(
|
const nestedCollections$ = allCollections$.pipe(
|
||||||
map((collections) => getNestedCollectionTree(collections)),
|
map((collections) => getNestedCollectionTree(collections))
|
||||||
shareReplay({ refCount: true, bufferSize: 1 })
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.searchText$
|
this.searchText$
|
||||||
@ -384,6 +385,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.collections = collections;
|
this.collections = collections;
|
||||||
this.selectedCollection = selectedCollection;
|
this.selectedCollection = selectedCollection;
|
||||||
|
|
||||||
|
this.canCreateCollections = allOrganizations?.some((o) => o.canCreateNewCollections);
|
||||||
|
|
||||||
this.showBulkMove =
|
this.showBulkMove =
|
||||||
filter.type !== "trash" &&
|
filter.type !== "trash" &&
|
||||||
(filter.organizationId === undefined || filter.organizationId === Unassigned);
|
(filter.organizationId === undefined || filter.organizationId === Unassigned);
|
||||||
@ -639,6 +642,32 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return childComponent;
|
return childComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addCollection() {
|
||||||
|
const dialog = openCollectionDialog(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
organizationId: this.allOrganizations
|
||||||
|
.filter((o) => o.canCreateNewCollections)
|
||||||
|
.sort(Utils.getSortFunction(this.i18nService, "name"))[0].id,
|
||||||
|
parentCollectionId: this.filter.collectionId,
|
||||||
|
showOrgSelector: true,
|
||||||
|
collectionIds: this.allCollections.map((c) => c.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
if (result.action === CollectionDialogAction.Saved) {
|
||||||
|
if (result.collection) {
|
||||||
|
// Update CollectionService with the new collection
|
||||||
|
const c = new CollectionData(result.collection as CollectionDetailsResponse);
|
||||||
|
await this.collectionService.upsert(c);
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
} else if (result.action === CollectionDialogAction.Deleted) {
|
||||||
|
// TODO: Remove collection from collectionService when collection
|
||||||
|
// deletion is implemented in the individual vault in AC-1347
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async cloneCipher(cipher: CipherView) {
|
async cloneCipher(cipher: CipherView) {
|
||||||
const component = await this.editCipher(cipher);
|
const component = await this.editCipher(cipher);
|
||||||
component.cloneMode = true;
|
component.cloneMode = true;
|
||||||
|
@ -60,7 +60,7 @@ import { openEntityEventsDialog } from "../../admin-console/organizations/manage
|
|||||||
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||||
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||||
import {
|
import {
|
||||||
CollectionDialogResult,
|
CollectionDialogAction,
|
||||||
CollectionDialogTabType,
|
CollectionDialogTabType,
|
||||||
openCollectionDialog,
|
openCollectionDialog,
|
||||||
} from "../components/collection-dialog";
|
} from "../components/collection-dialog";
|
||||||
@ -866,7 +866,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(dialog.closed);
|
const result = await lastValueFrom(dialog.closed);
|
||||||
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
|
if (
|
||||||
|
result.action === CollectionDialogAction.Saved ||
|
||||||
|
result.action === CollectionDialogAction.Deleted
|
||||||
|
) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -877,7 +880,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(dialog.closed);
|
const result = await lastValueFrom(dialog.closed);
|
||||||
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
|
if (
|
||||||
|
result.action === CollectionDialogAction.Saved ||
|
||||||
|
result.action === CollectionDialogAction.Deleted
|
||||||
|
) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user