mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 23:03:32 +00: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:
@@ -33,10 +33,10 @@ import { DialogService } from "@bitwarden/components";
|
|||||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||||
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
||||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||||
import { CollectionsComponent } from "../../../vault/app/vault/collections.component";
|
|
||||||
|
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AddEditComponent } from "./add-edit.component";
|
||||||
import { AttachmentsComponent } from "./attachments.component";
|
import { AttachmentsComponent } from "./attachments.component";
|
||||||
|
import { CollectionsComponent } from "./collections.component";
|
||||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||||
import { PasswordHistoryComponent } from "./password-history.component";
|
import { PasswordHistoryComponent } from "./password-history.component";
|
||||||
import { ShareComponent } from "./share.component";
|
import { ShareComponent } from "./share.component";
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export interface CollectionDialogParams {
|
|||||||
|
|
||||||
export interface CollectionDialogResult {
|
export interface CollectionDialogResult {
|
||||||
action: CollectionDialogAction;
|
action: CollectionDialogAction;
|
||||||
collection: CollectionResponse;
|
collection: CollectionResponse | CollectionView;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CollectionDialogAction {
|
export enum CollectionDialogAction {
|
||||||
@@ -263,7 +263,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.i18nService.t("deletedCollectionId", this.collection?.name)
|
this.i18nService.t("deletedCollectionId", this.collection?.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.close(CollectionDialogAction.Deleted);
|
this.close(CollectionDialogAction.Deleted, this.collection);
|
||||||
};
|
};
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -271,7 +271,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private close(action: CollectionDialogAction, collection?: CollectionResponse) {
|
private close(action: CollectionDialogAction, collection?: CollectionResponse | CollectionView) {
|
||||||
this.dialogRef.close({ action, collection } as CollectionDialogResult);
|
this.dialogRef.close({ action, collection } as CollectionDialogResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ export class VaultCollectionRowComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected edit() {
|
protected edit() {
|
||||||
this.onEvent.next({ type: "edit", item: this.collection });
|
this.onEvent.next({ type: "editCollection", item: this.collection });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected access() {
|
protected access() {
|
||||||
this.onEvent.next({ type: "viewAccess", item: this.collection });
|
this.onEvent.next({ type: "viewCollectionAccess", item: this.collection });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deleteCollection() {
|
protected deleteCollection() {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { VaultItem } from "./vault-item";
|
|||||||
export type VaultItemEvent =
|
export type VaultItemEvent =
|
||||||
| { type: "viewAttachments"; item: CipherView }
|
| { type: "viewAttachments"; item: CipherView }
|
||||||
| { type: "viewCollections"; item: CipherView }
|
| { type: "viewCollections"; item: CipherView }
|
||||||
| { type: "viewAccess"; item: CollectionView }
|
| { type: "viewCollectionAccess"; item: CollectionView }
|
||||||
| { type: "viewEvents"; item: CipherView }
|
| { type: "viewEvents"; item: CipherView }
|
||||||
| { type: "edit"; item: CollectionView }
|
| { type: "editCollection"; item: CollectionView }
|
||||||
| { type: "clone"; item: CipherView }
|
| { type: "clone"; item: CipherView }
|
||||||
| { type: "restore"; items: CipherView[] }
|
| { type: "restore"; items: CipherView[] }
|
||||||
| { type: "delete"; items: VaultItem[] }
|
| { type: "delete"; items: VaultItem[] }
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
appA11yTitle="{{ 'options' | i18n }}"
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #headerMenu>
|
<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>
|
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||||
{{ "moveSelected" | i18n }}
|
{{ "moveSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="showBulkMove"
|
*ngIf="bulkMoveAllowed"
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="bulkMoveToOrganization()"
|
(click)="bulkMoveToOrganization()"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
|||||||
import { TableDataSource } from "@bitwarden/components";
|
import { TableDataSource } from "@bitwarden/components";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
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 { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||||
|
|
||||||
import { VaultItem } from "./vault-item";
|
import { VaultItem } from "./vault-item";
|
||||||
@@ -33,7 +32,6 @@ export class VaultItemsComponent {
|
|||||||
@Input() showCollections: boolean;
|
@Input() showCollections: boolean;
|
||||||
@Input() showGroups: boolean;
|
@Input() showGroups: boolean;
|
||||||
@Input() useEvents: boolean;
|
@Input() useEvents: boolean;
|
||||||
@Input() editableCollections: boolean;
|
|
||||||
@Input() cloneableOrganizationCiphers: boolean;
|
@Input() cloneableOrganizationCiphers: boolean;
|
||||||
@Input() showPremiumFeatures: boolean;
|
@Input() showPremiumFeatures: boolean;
|
||||||
@Input() showBulkMove: boolean;
|
@Input() showBulkMove: boolean;
|
||||||
@@ -80,44 +78,30 @@ export class VaultItemsComponent {
|
|||||||
return this.dataSource.data.length === 0;
|
return this.dataSource.data.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canEditCollection(collection: CollectionView): boolean {
|
get bulkMoveAllowed() {
|
||||||
// We currently don't support editing collections from individual vault
|
return (
|
||||||
if (!(collection instanceof CollectionAdminView)) {
|
this.showBulkMove && this.selection.selected.filter((item) => item.collection).length === 0
|
||||||
return false;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected canEditCollection(collection: CollectionView): boolean {
|
||||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||||
|
return collection.canEdit(organization);
|
||||||
// Otherwise, check if we can edit the specified collection
|
|
||||||
return (
|
|
||||||
organization?.canEditAnyCollection ||
|
|
||||||
(organization?.canEditAssignedCollections && collection.assigned)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canDeleteCollection(collection: CollectionView): boolean {
|
protected canDeleteCollection(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"
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||||
|
return collection.canDelete(organization);
|
||||||
// Otherwise, check if we can delete the specified collection
|
|
||||||
return (
|
|
||||||
organization?.canDeleteAnyCollection ||
|
|
||||||
(organization?.canDeleteAssignedCollections && collection.assigned)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toggleAll() {
|
protected toggleAll() {
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ Individual.args = {
|
|||||||
showBulkMove: true,
|
showBulkMove: true,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: false,
|
useEvents: false,
|
||||||
editableCollections: false,
|
|
||||||
cloneableOrganizationCiphers: false,
|
cloneableOrganizationCiphers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,7 +140,6 @@ IndividualDisabled.args = {
|
|||||||
showBulkMove: true,
|
showBulkMove: true,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: false,
|
useEvents: false,
|
||||||
editableCollections: false,
|
|
||||||
cloneableOrganizationCiphers: false,
|
cloneableOrganizationCiphers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +154,6 @@ IndividualTrash.args = {
|
|||||||
showBulkMove: false,
|
showBulkMove: false,
|
||||||
showBulkTrashOptions: true,
|
showBulkTrashOptions: true,
|
||||||
useEvents: false,
|
useEvents: false,
|
||||||
editableCollections: false,
|
|
||||||
cloneableOrganizationCiphers: false,
|
cloneableOrganizationCiphers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,7 +168,6 @@ IndividualTopLevelCollection.args = {
|
|||||||
showBulkMove: false,
|
showBulkMove: false,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: false,
|
useEvents: false,
|
||||||
editableCollections: false,
|
|
||||||
cloneableOrganizationCiphers: false,
|
cloneableOrganizationCiphers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,7 +182,6 @@ IndividualSecondLevelCollection.args = {
|
|||||||
showBulkMove: true,
|
showBulkMove: true,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: false,
|
useEvents: false,
|
||||||
editableCollections: false,
|
|
||||||
cloneableOrganizationCiphers: false,
|
cloneableOrganizationCiphers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -201,7 +196,6 @@ OrganizationVault.args = {
|
|||||||
showBulkMove: false,
|
showBulkMove: false,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: true,
|
useEvents: true,
|
||||||
editableCollections: true,
|
|
||||||
cloneableOrganizationCiphers: true,
|
cloneableOrganizationCiphers: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,7 +210,6 @@ OrganizationTrash.args = {
|
|||||||
showBulkMove: false,
|
showBulkMove: false,
|
||||||
showBulkTrashOptions: true,
|
showBulkTrashOptions: true,
|
||||||
useEvents: true,
|
useEvents: true,
|
||||||
editableCollections: true,
|
|
||||||
cloneableOrganizationCiphers: true,
|
cloneableOrganizationCiphers: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,7 +227,6 @@ OrganizationTopLevelCollection.args = {
|
|||||||
showBulkMove: false,
|
showBulkMove: false,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: true,
|
useEvents: true,
|
||||||
editableCollections: true,
|
|
||||||
cloneableOrganizationCiphers: true,
|
cloneableOrganizationCiphers: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,7 +241,6 @@ OrganizationSecondLevelCollection.args = {
|
|||||||
showBulkMove: false,
|
showBulkMove: false,
|
||||||
showBulkTrashOptions: false,
|
showBulkTrashOptions: false,
|
||||||
useEvents: true,
|
useEvents: true,
|
||||||
editableCollections: true,
|
|
||||||
cloneableOrganizationCiphers: 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 { CollectionAccessDetailsResponse } from "@bitwarden/common/src/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";
|
||||||
|
|
||||||
@@ -29,4 +30,12 @@ export class CollectionAdminView extends CollectionView {
|
|||||||
|
|
||||||
this.assigned = response.assigned;
|
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 { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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 { 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";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
export interface BulkDeleteDialogParams {
|
export interface BulkDeleteDialogParams {
|
||||||
@@ -15,6 +17,8 @@ export interface BulkDeleteDialogParams {
|
|||||||
collectionIds?: string[];
|
collectionIds?: string[];
|
||||||
permanent?: boolean;
|
permanent?: boolean;
|
||||||
organization?: Organization;
|
organization?: Organization;
|
||||||
|
organizations?: Organization[];
|
||||||
|
collections?: CollectionView[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BulkDeleteDialogResult {
|
export enum BulkDeleteDialogResult {
|
||||||
@@ -45,6 +49,8 @@ export class BulkDeleteDialogComponent {
|
|||||||
collectionIds: string[];
|
collectionIds: string[];
|
||||||
permanent = false;
|
permanent = false;
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
|
organizations: Organization[];
|
||||||
|
collections: CollectionView[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||||
@@ -52,12 +58,15 @@ export class BulkDeleteDialogComponent {
|
|||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private apiService: ApiService
|
private apiService: ApiService,
|
||||||
|
private collectionService: CollectionService
|
||||||
) {
|
) {
|
||||||
this.cipherIds = params.cipherIds ?? [];
|
this.cipherIds = params.cipherIds ?? [];
|
||||||
this.collectionIds = params.collectionIds ?? [];
|
this.collectionIds = params.collectionIds ?? [];
|
||||||
this.permanent = params.permanent;
|
this.permanent = params.permanent;
|
||||||
this.organization = params.organization;
|
this.organization = params.organization;
|
||||||
|
this.organizations = params.organizations;
|
||||||
|
this.collections = params.collections;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async cancel() {
|
protected async cancel() {
|
||||||
@@ -74,7 +83,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.collectionIds.length && this.organization) {
|
if (this.collectionIds.length) {
|
||||||
deletePromises.push(this.deleteCollections());
|
deletePromises.push(this.deleteCollections());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +97,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.collectionIds.length) {
|
if (this.collectionIds.length) {
|
||||||
|
await this.collectionService.delete(this.collectionIds);
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
null,
|
||||||
@@ -116,6 +126,8 @@ export class BulkDeleteDialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async deleteCollections(): Promise<any> {
|
private async deleteCollections(): Promise<any> {
|
||||||
|
// From org vault
|
||||||
|
if (this.organization) {
|
||||||
if (
|
if (
|
||||||
!this.organization.canDeleteAssignedCollections &&
|
!this.organization.canDeleteAssignedCollections &&
|
||||||
!this.organization.canDeleteAnyCollection
|
!this.organization.canDeleteAnyCollection
|
||||||
@@ -127,8 +139,31 @@ export class BulkDeleteDialogComponent {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deleteRequest = new CollectionBulkDeleteRequest(this.collectionIds, this.organization.id);
|
const deleteRequest = new CollectionBulkDeleteRequest(
|
||||||
|
this.collectionIds,
|
||||||
|
this.organization.id
|
||||||
|
);
|
||||||
return await this.apiService.deleteManyCollections(deleteRequest);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private close(result: BulkDeleteDialogResult) {
|
private close(result: BulkDeleteDialogResult) {
|
||||||
|
|||||||
@@ -28,6 +28,46 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span>{{ title }}</span>
|
<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">
|
<small *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
|
||||||
|
import { CollectionDialogTabType } from "../../components/collection-dialog";
|
||||||
import {
|
import {
|
||||||
All,
|
All,
|
||||||
RoutedVaultFilterModel,
|
RoutedVaultFilterModel,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
export class VaultHeaderComponent {
|
export class VaultHeaderComponent {
|
||||||
protected Unassigned = Unassigned;
|
protected Unassigned = Unassigned;
|
||||||
protected All = All;
|
protected All = All;
|
||||||
|
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean to determine the loading state of the header.
|
* Boolean to determine the loading state of the header.
|
||||||
@@ -29,36 +31,30 @@ export class VaultHeaderComponent {
|
|||||||
/** Current active filter */
|
/** Current active filter */
|
||||||
@Input() filter: RoutedVaultFilterModel;
|
@Input() filter: RoutedVaultFilterModel;
|
||||||
|
|
||||||
/**
|
/** All organizations that can be shown */
|
||||||
* All organizations that can be shown
|
|
||||||
*/
|
|
||||||
@Input() organizations: Organization[] = [];
|
@Input() organizations: Organization[] = [];
|
||||||
|
|
||||||
/**
|
/** Currently selected collection */
|
||||||
* Currently selected collection
|
|
||||||
*/
|
|
||||||
@Input() collection?: TreeNode<CollectionView>;
|
@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;
|
@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 */
|
||||||
* Emits an event when the new collection button is clicked in the 'New' dropdown menu
|
|
||||||
*/
|
|
||||||
@Output() onAddCollection = new EventEmitter<null>();
|
@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>();
|
@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) {}
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,6 +123,40 @@ export class VaultHeaderComponent {
|
|||||||
.map((treeNode) => treeNode.node);
|
.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() {
|
protected addCipher() {
|
||||||
this.onAddCipher.emit();
|
this.onAddCipher.emit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
(onAddCipher)="addCipher()"
|
(onAddCipher)="addCipher()"
|
||||||
(onAddCollection)="addCollection()"
|
(onAddCollection)="addCollection()"
|
||||||
(onAddFolder)="addFolder()"
|
(onAddFolder)="addFolder()"
|
||||||
|
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
|
||||||
|
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||||
></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 }}
|
||||||
@@ -42,7 +44,6 @@
|
|||||||
[showBulkMove]="showBulkMove"
|
[showBulkMove]="showBulkMove"
|
||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="false"
|
[useEvents]="false"
|
||||||
[editableCollections]="false"
|
|
||||||
[cloneableOrganizationCiphers]="false"
|
[cloneableOrganizationCiphers]="false"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
|
|
||||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
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 { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { TotpService } from "@bitwarden/common/abstractions/totp.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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { DialogService, Icons } from "@bitwarden/components";
|
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 { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||||
|
|
||||||
@@ -130,7 +136,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected showBulkMove: boolean;
|
protected showBulkMove: boolean;
|
||||||
protected canAccessPremium: boolean;
|
protected canAccessPremium: boolean;
|
||||||
protected allCollections: CollectionView[];
|
protected allCollections: CollectionView[];
|
||||||
protected allOrganizations: Organization[];
|
protected allOrganizations: Organization[] = [];
|
||||||
protected ciphers: CipherView[];
|
protected ciphers: CipherView[];
|
||||||
protected collections: CollectionView[];
|
protected collections: CollectionView[];
|
||||||
protected isEmpty: boolean;
|
protected isEmpty: boolean;
|
||||||
@@ -170,6 +176,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private searchPipe: SearchPipe,
|
private searchPipe: SearchPipe,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
|
private apiService: ApiService,
|
||||||
private userVerificationService: UserVerificationService
|
private userVerificationService: UserVerificationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -430,12 +437,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
await this.bulkRestore(event.items);
|
await this.bulkRestore(event.items);
|
||||||
}
|
}
|
||||||
} else if (event.type === "delete") {
|
} else if (event.type === "delete") {
|
||||||
const ciphers = event.items.filter((i) => i.collection === undefined).map((i) => i.cipher);
|
await this.handleDeleteEvent(event.items);
|
||||||
if (ciphers.length === 1) {
|
|
||||||
await this.deleteCipher(ciphers[0]);
|
|
||||||
} else {
|
|
||||||
await this.bulkDelete(ciphers);
|
|
||||||
}
|
|
||||||
} else if (event.type === "moveToFolder") {
|
} else if (event.type === "moveToFolder") {
|
||||||
await this.bulkMove(event.items);
|
await this.bulkMove(event.items);
|
||||||
} else if (event.type === "moveToOrganization") {
|
} else if (event.type === "moveToOrganization") {
|
||||||
@@ -446,6 +448,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else if (event.type === "copyField") {
|
} else if (event.type === "copyField") {
|
||||||
await this.copy(event.item, event.field);
|
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 {
|
} finally {
|
||||||
this.processingEvent = false;
|
this.processingEvent = false;
|
||||||
@@ -652,10 +658,65 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
await this.collectionService.upsert(c);
|
await this.collectionService.upsert(c);
|
||||||
}
|
}
|
||||||
this.refresh();
|
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();
|
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();
|
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> {
|
async deleteCipher(c: CipherView): Promise<boolean> {
|
||||||
if (!(await this.repromptCipher([c]))) {
|
if (!(await this.repromptCipher([c]))) {
|
||||||
return;
|
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))) {
|
if (!(await this.repromptCipher(ciphers))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedIds = ciphers.map((cipher) => cipher.id);
|
if (ciphers.length === 0 && collections.length === 0) {
|
||||||
if (selectedIds.length === 0) {
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
@@ -747,7 +831,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dialog = openBulkDeleteDialog(this.dialogService, {
|
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);
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
|||||||
@@ -141,10 +141,7 @@ export class VaultHeaderComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check if we can edit the specified collection
|
// Otherwise, check if we can edit the specified collection
|
||||||
return (
|
return this.collection.node.canEdit(this.organization);
|
||||||
this.organization.canEditAnyCollection ||
|
|
||||||
(this.organization.canEditAssignedCollections && this.collection?.node.assigned)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addCipher() {
|
addCipher() {
|
||||||
@@ -174,10 +171,7 @@ export class VaultHeaderComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check if we can delete the specified collection
|
// Otherwise, check if we can delete the specified collection
|
||||||
return (
|
return this.collection.node.canDelete(this.organization);
|
||||||
this.organization?.canDeleteAnyCollection ||
|
|
||||||
(this.organization?.canDeleteAssignedCollections && this.collection.node.assigned)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCollection() {
|
deleteCollection() {
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
[showBulkMove]="false"
|
[showBulkMove]="false"
|
||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="organization?.useEvents"
|
[useEvents]="organization?.useEvents"
|
||||||
[editableCollections]="true"
|
|
||||||
[cloneableOrganizationCiphers]="true"
|
[cloneableOrganizationCiphers]="true"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -495,9 +495,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else if (event.type === "copyField") {
|
} else if (event.type === "copyField") {
|
||||||
await this.copy(event.item, event.field);
|
await this.copy(event.item, event.field);
|
||||||
} else if (event.type === "edit") {
|
} else if (event.type === "editCollection") {
|
||||||
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
||||||
} else if (event.type === "viewAccess") {
|
} else if (event.type === "viewCollectionAccess") {
|
||||||
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
||||||
} else if (event.type === "viewEvents") {
|
} else if (event.type === "viewEvents") {
|
||||||
await this.viewEvents(event.item);
|
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 { ITreeNodeObject } from "../../../models/domain/tree-node";
|
||||||
import { View } from "../../../models/view/view";
|
import { View } from "../../../models/view/view";
|
||||||
import { Collection } from "../domain/collection";
|
import { Collection } from "../domain/collection";
|
||||||
@@ -10,6 +11,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
organizationId: string = null;
|
organizationId: string = null;
|
||||||
name: string = null;
|
name: string = null;
|
||||||
externalId: string = null;
|
externalId: string = null;
|
||||||
|
// readOnly applies to the items within a collection
|
||||||
readOnly: boolean = null;
|
readOnly: boolean = null;
|
||||||
hidePasswords: boolean = null;
|
hidePasswords: boolean = null;
|
||||||
|
|
||||||
@@ -26,4 +28,24 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
this.hidePasswords = c.hidePasswords;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user