mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[AC-1347] Allow editing of collections in individual vault (#6081)
* Rename Collection events to be more explicit * Implement edit collection for individual vault row * Implement edit and delete collection from individual vault header * Implement bulk delete for collections in individual vault * Clean up CollectionDialogResult properties * Centralize canEdit and canDelete logic to Collection models * Check orgId in canEdit and canDelete and add clarifying comments --------- Co-authored-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
parent
f43c3220dc
commit
d40f996e71
@ -33,10 +33,10 @@ import { DialogService } from "@bitwarden/components";
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { CollectionsComponent } from "../../../vault/app/vault/collections.component";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
import { CollectionsComponent } from "./collections.component";
|
||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||
import { PasswordHistoryComponent } from "./password-history.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
|
@ -51,7 +51,7 @@ export interface CollectionDialogParams {
|
||||
|
||||
export interface CollectionDialogResult {
|
||||
action: CollectionDialogAction;
|
||||
collection: CollectionResponse;
|
||||
collection: CollectionResponse | CollectionView;
|
||||
}
|
||||
|
||||
export enum CollectionDialogAction {
|
||||
@ -263,7 +263,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.i18nService.t("deletedCollectionId", this.collection?.name)
|
||||
);
|
||||
|
||||
this.close(CollectionDialogAction.Deleted);
|
||||
this.close(CollectionDialogAction.Deleted, this.collection);
|
||||
};
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -271,7 +271,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private close(action: CollectionDialogAction, collection?: CollectionResponse) {
|
||||
private close(action: CollectionDialogAction, collection?: CollectionResponse | CollectionView) {
|
||||
this.dialogRef.close({ action, collection } as CollectionDialogResult);
|
||||
}
|
||||
}
|
||||
|
@ -60,11 +60,11 @@ export class VaultCollectionRowComponent {
|
||||
}
|
||||
|
||||
protected edit() {
|
||||
this.onEvent.next({ type: "edit", item: this.collection });
|
||||
this.onEvent.next({ type: "editCollection", item: this.collection });
|
||||
}
|
||||
|
||||
protected access() {
|
||||
this.onEvent.next({ type: "viewAccess", item: this.collection });
|
||||
this.onEvent.next({ type: "viewCollectionAccess", item: this.collection });
|
||||
}
|
||||
|
||||
protected deleteCollection() {
|
||||
|
@ -6,9 +6,9 @@ import { VaultItem } from "./vault-item";
|
||||
export type VaultItemEvent =
|
||||
| { type: "viewAttachments"; item: CipherView }
|
||||
| { type: "viewCollections"; item: CipherView }
|
||||
| { type: "viewAccess"; item: CollectionView }
|
||||
| { type: "viewCollectionAccess"; item: CollectionView }
|
||||
| { type: "viewEvents"; item: CipherView }
|
||||
| { type: "edit"; item: CollectionView }
|
||||
| { type: "editCollection"; item: CollectionView }
|
||||
| { type: "clone"; item: CipherView }
|
||||
| { type: "restore"; items: CipherView[] }
|
||||
| { type: "delete"; items: VaultItem[] }
|
||||
|
@ -30,12 +30,12 @@
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #headerMenu>
|
||||
<button *ngIf="showBulkMove" type="button" bitMenuItem (click)="bulkMoveToFolder()">
|
||||
<button *ngIf="bulkMoveAllowed" type="button" bitMenuItem (click)="bulkMoveToFolder()">
|
||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||
{{ "moveSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showBulkMove"
|
||||
*ngIf="bulkMoveAllowed"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkMoveToOrganization()"
|
||||
|
@ -7,7 +7,6 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import { VaultItem } from "./vault-item";
|
||||
@ -33,7 +32,6 @@ export class VaultItemsComponent {
|
||||
@Input() showCollections: boolean;
|
||||
@Input() showGroups: boolean;
|
||||
@Input() useEvents: boolean;
|
||||
@Input() editableCollections: boolean;
|
||||
@Input() cloneableOrganizationCiphers: boolean;
|
||||
@Input() showPremiumFeatures: boolean;
|
||||
@Input() showBulkMove: boolean;
|
||||
@ -80,44 +78,30 @@ export class VaultItemsComponent {
|
||||
return this.dataSource.data.length === 0;
|
||||
}
|
||||
|
||||
protected canEditCollection(collection: CollectionView): boolean {
|
||||
// We currently don't support editing collections from individual vault
|
||||
if (!(collection instanceof CollectionAdminView)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (!this.editableCollections || collection.id === Unassigned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||
|
||||
// Otherwise, check if we can edit the specified collection
|
||||
get bulkMoveAllowed() {
|
||||
return (
|
||||
organization?.canEditAnyCollection ||
|
||||
(organization?.canEditAssignedCollections && collection.assigned)
|
||||
this.showBulkMove && this.selection.selected.filter((item) => item.collection).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
protected canDeleteCollection(collection: CollectionView): boolean {
|
||||
// We currently don't support editing collections from individual vault
|
||||
if (!(collection instanceof CollectionAdminView)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected canEditCollection(collection: CollectionView): boolean {
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (!this.editableCollections || collection.id === Unassigned) {
|
||||
if (collection.id === Unassigned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||
return collection.canEdit(organization);
|
||||
}
|
||||
|
||||
// Otherwise, check if we can delete the specified collection
|
||||
return (
|
||||
organization?.canDeleteAnyCollection ||
|
||||
(organization?.canDeleteAssignedCollections && collection.assigned)
|
||||
);
|
||||
protected canDeleteCollection(collection: CollectionView): boolean {
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (collection.id === Unassigned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||
return collection.canDelete(organization);
|
||||
}
|
||||
|
||||
protected toggleAll() {
|
||||
|
@ -125,7 +125,6 @@ Individual.args = {
|
||||
showBulkMove: true,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: false,
|
||||
editableCollections: false,
|
||||
cloneableOrganizationCiphers: false,
|
||||
};
|
||||
|
||||
@ -141,7 +140,6 @@ IndividualDisabled.args = {
|
||||
showBulkMove: true,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: false,
|
||||
editableCollections: false,
|
||||
cloneableOrganizationCiphers: false,
|
||||
};
|
||||
|
||||
@ -156,7 +154,6 @@ IndividualTrash.args = {
|
||||
showBulkMove: false,
|
||||
showBulkTrashOptions: true,
|
||||
useEvents: false,
|
||||
editableCollections: false,
|
||||
cloneableOrganizationCiphers: false,
|
||||
};
|
||||
|
||||
@ -171,7 +168,6 @@ IndividualTopLevelCollection.args = {
|
||||
showBulkMove: false,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: false,
|
||||
editableCollections: false,
|
||||
cloneableOrganizationCiphers: false,
|
||||
};
|
||||
|
||||
@ -186,7 +182,6 @@ IndividualSecondLevelCollection.args = {
|
||||
showBulkMove: true,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: false,
|
||||
editableCollections: false,
|
||||
cloneableOrganizationCiphers: false,
|
||||
};
|
||||
|
||||
@ -201,7 +196,6 @@ OrganizationVault.args = {
|
||||
showBulkMove: false,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: true,
|
||||
editableCollections: true,
|
||||
cloneableOrganizationCiphers: true,
|
||||
};
|
||||
|
||||
@ -216,7 +210,6 @@ OrganizationTrash.args = {
|
||||
showBulkMove: false,
|
||||
showBulkTrashOptions: true,
|
||||
useEvents: true,
|
||||
editableCollections: true,
|
||||
cloneableOrganizationCiphers: true,
|
||||
};
|
||||
|
||||
@ -234,7 +227,6 @@ OrganizationTopLevelCollection.args = {
|
||||
showBulkMove: false,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: true,
|
||||
editableCollections: true,
|
||||
cloneableOrganizationCiphers: true,
|
||||
};
|
||||
|
||||
@ -249,7 +241,6 @@ OrganizationSecondLevelCollection.args = {
|
||||
showBulkMove: false,
|
||||
showBulkTrashOptions: false,
|
||||
useEvents: true,
|
||||
editableCollections: true,
|
||||
cloneableOrganizationCiphers: true,
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
@ -29,4 +30,12 @@ export class CollectionAdminView extends CollectionView {
|
||||
|
||||
this.assigned = response.assigned;
|
||||
}
|
||||
|
||||
override canEdit(org: Organization): boolean {
|
||||
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
|
||||
}
|
||||
|
||||
override canDelete(org: Organization): boolean {
|
||||
return org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,9 @@ import { CollectionBulkDeleteRequest } from "@bitwarden/common/models/request/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CipherBulkDeleteRequest } from "@bitwarden/common/vault/models/request/cipher-bulk-delete.request";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface BulkDeleteDialogParams {
|
||||
@ -15,6 +17,8 @@ export interface BulkDeleteDialogParams {
|
||||
collectionIds?: string[];
|
||||
permanent?: boolean;
|
||||
organization?: Organization;
|
||||
organizations?: Organization[];
|
||||
collections?: CollectionView[];
|
||||
}
|
||||
|
||||
export enum BulkDeleteDialogResult {
|
||||
@ -45,6 +49,8 @@ export class BulkDeleteDialogComponent {
|
||||
collectionIds: string[];
|
||||
permanent = false;
|
||||
organization: Organization;
|
||||
organizations: Organization[];
|
||||
collections: CollectionView[];
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||
@ -52,12 +58,15 @@ export class BulkDeleteDialogComponent {
|
||||
private cipherService: CipherService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService
|
||||
private apiService: ApiService,
|
||||
private collectionService: CollectionService
|
||||
) {
|
||||
this.cipherIds = params.cipherIds ?? [];
|
||||
this.collectionIds = params.collectionIds ?? [];
|
||||
this.permanent = params.permanent;
|
||||
this.organization = params.organization;
|
||||
this.organizations = params.organizations;
|
||||
this.collections = params.collections;
|
||||
}
|
||||
|
||||
protected async cancel() {
|
||||
@ -74,7 +83,7 @@ export class BulkDeleteDialogComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.collectionIds.length && this.organization) {
|
||||
if (this.collectionIds.length) {
|
||||
deletePromises.push(this.deleteCollections());
|
||||
}
|
||||
|
||||
@ -88,6 +97,7 @@ export class BulkDeleteDialogComponent {
|
||||
);
|
||||
}
|
||||
if (this.collectionIds.length) {
|
||||
await this.collectionService.delete(this.collectionIds);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
@ -116,19 +126,44 @@ export class BulkDeleteDialogComponent {
|
||||
}
|
||||
|
||||
private async deleteCollections(): Promise<any> {
|
||||
if (
|
||||
!this.organization.canDeleteAssignedCollections &&
|
||||
!this.organization.canDeleteAnyCollection
|
||||
) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("missingPermissions")
|
||||
// From org vault
|
||||
if (this.organization) {
|
||||
if (
|
||||
!this.organization.canDeleteAssignedCollections &&
|
||||
!this.organization.canDeleteAnyCollection
|
||||
) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("missingPermissions")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const deleteRequest = new CollectionBulkDeleteRequest(
|
||||
this.collectionIds,
|
||||
this.organization.id
|
||||
);
|
||||
return;
|
||||
return await this.apiService.deleteManyCollections(deleteRequest);
|
||||
// From individual vault, so there can be multiple organizations
|
||||
} else if (this.organizations && this.collections) {
|
||||
const deletePromises: Promise<any>[] = [];
|
||||
for (const organization of this.organizations) {
|
||||
if (!organization.canDeleteAssignedCollections && !organization.canDeleteAnyCollection) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("missingPermissions")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const orgCollections = this.collections
|
||||
.filter((o) => o.organizationId === organization.id)
|
||||
.map((c) => c.id);
|
||||
const deleteRequest = new CollectionBulkDeleteRequest(orgCollections, organization.id);
|
||||
deletePromises.push(this.apiService.deleteManyCollections(deleteRequest));
|
||||
}
|
||||
return await Promise.all(deletePromises);
|
||||
}
|
||||
const deleteRequest = new CollectionBulkDeleteRequest(this.collectionIds, this.organization.id);
|
||||
return await this.apiService.deleteManyCollections(deleteRequest);
|
||||
}
|
||||
|
||||
private close(result: BulkDeleteDialogResult) {
|
||||
|
@ -28,6 +28,46 @@
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ title }}</span>
|
||||
<ng-container *ngIf="collection !== undefined && (canEditCollection || canDeleteCollection)">
|
||||
<button
|
||||
bitIconButton="bwi-angle-down"
|
||||
[bitMenuTriggerFor]="editCollectionMenu"
|
||||
size="small"
|
||||
type="button"
|
||||
aria-haspopup
|
||||
></button>
|
||||
<bit-menu #editCollectionMenu>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="canEditCollection"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "editInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="canEditCollection"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "access" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="canDeleteCollection"
|
||||
bitMenuItem
|
||||
(click)="deleteCollection()"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
|
@ -5,6 +5,7 @@ import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { CollectionDialogTabType } from "../../components/collection-dialog";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
export class VaultHeaderComponent {
|
||||
protected Unassigned = Unassigned;
|
||||
protected All = All;
|
||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||
|
||||
/**
|
||||
* Boolean to determine the loading state of the header.
|
||||
@ -29,36 +31,30 @@ export class VaultHeaderComponent {
|
||||
/** Current active filter */
|
||||
@Input() filter: RoutedVaultFilterModel;
|
||||
|
||||
/**
|
||||
* All organizations that can be shown
|
||||
*/
|
||||
/** All organizations that can be shown */
|
||||
@Input() organizations: Organization[] = [];
|
||||
|
||||
/**
|
||||
* Currently selected collection
|
||||
*/
|
||||
/** Currently selected collection */
|
||||
@Input() collection?: TreeNode<CollectionView>;
|
||||
|
||||
/**
|
||||
* Whether 'Collection' option is shown in the 'New' dropdown
|
||||
*/
|
||||
/** 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>();
|
||||
|
||||
/**
|
||||
* Emits an event when the new collection button is clicked in the 'New' dropdown menu
|
||||
*/
|
||||
/** 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
|
||||
*/
|
||||
/** Emits an event when the new folder button is clicked in the 'New' dropdown menu */
|
||||
@Output() onAddFolder = new EventEmitter<null>();
|
||||
|
||||
/** Emits an event when the edit collection button is clicked in the header */
|
||||
@Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType }>();
|
||||
|
||||
/** Emits an event when the delete collection button is clicked in the header */
|
||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
/**
|
||||
@ -127,6 +123,40 @@ export class VaultHeaderComponent {
|
||||
.map((treeNode) => treeNode.node);
|
||||
}
|
||||
|
||||
get canEditCollection(): boolean {
|
||||
// Only edit collections if not editing "Unassigned"
|
||||
if (this.collection === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, check if we can edit the specified collection
|
||||
const organization = this.organizations.find(
|
||||
(o) => o.id === this.collection?.node.organizationId
|
||||
);
|
||||
return this.collection.node.canEdit(organization);
|
||||
}
|
||||
|
||||
async editCollection(tab: CollectionDialogTabType): Promise<void> {
|
||||
this.onEditCollection.emit({ tab });
|
||||
}
|
||||
|
||||
get canDeleteCollection(): boolean {
|
||||
// Only delete collections if not deleting "Unassigned"
|
||||
if (this.collection === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, check if we can edit the specified collection
|
||||
const organization = this.organizations.find(
|
||||
(o) => o.id === this.collection?.node.organizationId
|
||||
);
|
||||
return this.collection.node.canDelete(organization);
|
||||
}
|
||||
|
||||
deleteCollection() {
|
||||
this.onDeleteCollection.emit();
|
||||
}
|
||||
|
||||
protected addCipher() {
|
||||
this.onAddCipher.emit();
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
(onAddCipher)="addCipher()"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||
></app-vault-header>
|
||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||
{{ trashCleanupWarning }}
|
||||
@ -42,7 +44,6 @@
|
||||
[showBulkMove]="showBulkMove"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="false"
|
||||
[editableCollections]="false"
|
||||
[cloneableOrganizationCiphers]="false"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
>
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
@ -60,7 +61,12 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { DialogService, Icons } from "@bitwarden/components";
|
||||
|
||||
import { CollectionDialogAction, openCollectionDialog } from "../components/collection-dialog";
|
||||
import {
|
||||
CollectionDialogAction,
|
||||
CollectionDialogTabType,
|
||||
openCollectionDialog,
|
||||
} from "../components/collection-dialog";
|
||||
import { VaultItem } from "../components/vault-items/vault-item";
|
||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||
|
||||
@ -130,7 +136,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected showBulkMove: boolean;
|
||||
protected canAccessPremium: boolean;
|
||||
protected allCollections: CollectionView[];
|
||||
protected allOrganizations: Organization[];
|
||||
protected allOrganizations: Organization[] = [];
|
||||
protected ciphers: CipherView[];
|
||||
protected collections: CollectionView[];
|
||||
protected isEmpty: boolean;
|
||||
@ -170,6 +176,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private searchService: SearchService,
|
||||
private searchPipe: SearchPipe,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private apiService: ApiService,
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
@ -430,12 +437,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.bulkRestore(event.items);
|
||||
}
|
||||
} else if (event.type === "delete") {
|
||||
const ciphers = event.items.filter((i) => i.collection === undefined).map((i) => i.cipher);
|
||||
if (ciphers.length === 1) {
|
||||
await this.deleteCipher(ciphers[0]);
|
||||
} else {
|
||||
await this.bulkDelete(ciphers);
|
||||
}
|
||||
await this.handleDeleteEvent(event.items);
|
||||
} else if (event.type === "moveToFolder") {
|
||||
await this.bulkMove(event.items);
|
||||
} else if (event.type === "moveToOrganization") {
|
||||
@ -446,6 +448,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (event.type === "copyField") {
|
||||
await this.copy(event.item, event.field);
|
||||
} else if (event.type === "editCollection") {
|
||||
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
||||
} else if (event.type === "viewCollectionAccess") {
|
||||
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
||||
}
|
||||
} finally {
|
||||
this.processingEvent = false;
|
||||
@ -652,10 +658,65 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> {
|
||||
const dialog = openCollectionDialog(this.dialogService, {
|
||||
data: { collectionId: c?.id, organizationId: c.organizationId, initialTab: tab },
|
||||
});
|
||||
|
||||
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) {
|
||||
await this.collectionService.delete(result.collection?.id);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCollection(collection: CollectionView): Promise<void> {
|
||||
const organization = this.organizationService.get(collection.organizationId);
|
||||
if (!organization.canDeleteAssignedCollections && !organization.canDeleteAnyCollection) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("missingPermissions")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: collection.name,
|
||||
content: { key: "deleteCollectionConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.apiService.deleteCollection(collection.organizationId, collection.id);
|
||||
await this.collectionService.delete(collection.id);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("deletedCollectionId", collection.name)
|
||||
);
|
||||
// Navigate away if we deleted the collection we were viewing
|
||||
if (this.selectedCollection?.node.id === collection.id) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
this.refresh();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -702,6 +763,26 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async handleDeleteEvent(items: VaultItem[]) {
|
||||
const ciphers = items.filter((i) => i.collection === undefined).map((i) => i.cipher);
|
||||
const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection);
|
||||
if (ciphers.length === 1 && collections.length === 0) {
|
||||
await this.deleteCipher(ciphers[0]);
|
||||
} else if (ciphers.length === 0 && collections.length === 1) {
|
||||
await this.deleteCollection(collections[0]);
|
||||
} else {
|
||||
const orgIds = items
|
||||
.filter((i) => i.cipher === undefined)
|
||||
.map((i) => i.collection.organizationId);
|
||||
const orgs = await firstValueFrom(
|
||||
this.organizationService.organizations$.pipe(
|
||||
map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))
|
||||
)
|
||||
);
|
||||
await this.bulkDelete(ciphers, collections, orgs);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCipher(c: CipherView): Promise<boolean> {
|
||||
if (!(await this.repromptCipher([c]))) {
|
||||
return;
|
||||
@ -732,13 +813,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async bulkDelete(ciphers: CipherView[]) {
|
||||
async bulkDelete(
|
||||
ciphers: CipherView[],
|
||||
collections: CollectionView[],
|
||||
organizations: Organization[]
|
||||
) {
|
||||
if (!(await this.repromptCipher(ciphers))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = ciphers.map((cipher) => cipher.id);
|
||||
if (selectedIds.length === 0) {
|
||||
if (ciphers.length === 0 && collections.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
@ -747,7 +831,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
const dialog = openBulkDeleteDialog(this.dialogService, {
|
||||
data: { permanent: this.filter.type === "trash", cipherIds: selectedIds },
|
||||
data: {
|
||||
permanent: this.filter.type === "trash",
|
||||
cipherIds: ciphers.map((c) => c.id),
|
||||
collectionIds: collections.map((c) => c.id),
|
||||
organizations: organizations,
|
||||
collections: collections,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
@ -141,10 +141,7 @@ export class VaultHeaderComponent {
|
||||
}
|
||||
|
||||
// Otherwise, check if we can edit the specified collection
|
||||
return (
|
||||
this.organization.canEditAnyCollection ||
|
||||
(this.organization.canEditAssignedCollections && this.collection?.node.assigned)
|
||||
);
|
||||
return this.collection.node.canEdit(this.organization);
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
@ -174,10 +171,7 @@ export class VaultHeaderComponent {
|
||||
}
|
||||
|
||||
// Otherwise, check if we can delete the specified collection
|
||||
return (
|
||||
this.organization?.canDeleteAnyCollection ||
|
||||
(this.organization?.canDeleteAssignedCollections && this.collection.node.assigned)
|
||||
);
|
||||
return this.collection.node.canDelete(this.organization);
|
||||
}
|
||||
|
||||
deleteCollection() {
|
||||
|
@ -51,7 +51,6 @@
|
||||
[showBulkMove]="false"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="organization?.useEvents"
|
||||
[editableCollections]="true"
|
||||
[cloneableOrganizationCiphers]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
>
|
||||
|
@ -495,9 +495,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (event.type === "copyField") {
|
||||
await this.copy(event.item, event.field);
|
||||
} else if (event.type === "edit") {
|
||||
} else if (event.type === "editCollection") {
|
||||
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
||||
} else if (event.type === "viewAccess") {
|
||||
} else if (event.type === "viewCollectionAccess") {
|
||||
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
||||
} else if (event.type === "viewEvents") {
|
||||
await this.viewEvents(event.item);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { ITreeNodeObject } from "../../../models/domain/tree-node";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { Collection } from "../domain/collection";
|
||||
@ -10,6 +11,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
organizationId: string = null;
|
||||
name: string = null;
|
||||
externalId: string = null;
|
||||
// readOnly applies to the items within a collection
|
||||
readOnly: boolean = null;
|
||||
hidePasswords: boolean = null;
|
||||
|
||||
@ -26,4 +28,24 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
this.hidePasswords = c.hidePasswords;
|
||||
}
|
||||
}
|
||||
|
||||
// For editing collection details, not the items within it.
|
||||
canEdit(org: Organization): boolean {
|
||||
if (org.id !== this.organizationId) {
|
||||
throw new Error(
|
||||
"Id of the organization provided does not match the org id of the collection."
|
||||
);
|
||||
}
|
||||
return org?.canEditAnyCollection || org?.canEditAssignedCollections;
|
||||
}
|
||||
|
||||
// For deleting a collection, not the items within it.
|
||||
canDelete(org: Organization): boolean {
|
||||
if (org.id !== this.organizationId) {
|
||||
throw new Error(
|
||||
"Id of the organization provided does not match the org id of the collection."
|
||||
);
|
||||
}
|
||||
return org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user