1
0
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:
Robyn MacCallum 2023-10-04 17:15:20 -04:00 committed by GitHub
parent f43c3220dc
commit d40f996e71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 302 additions and 107 deletions

View File

@ -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";

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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[] }

View File

@ -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()"

View File

@ -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() {

View File

@ -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,
};

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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"

View File

@ -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();
}

View File

@ -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)"
>

View File

@ -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);

View File

@ -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() {

View File

@ -51,7 +51,6 @@
[showBulkMove]="false"
[showBulkTrashOptions]="filter.type === 'trash'"
[useEvents]="organization?.useEvents"
[editableCollections]="true"
[cloneableOrganizationCiphers]="true"
(onEvent)="onVaultItemsEvent($event)"
>

View File

@ -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);

View File

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