[PM-2383] Bulk collection assignment (#8429)
* [PM-2383] Add bulkUpdateCollectionsWithServer method to CipherService * [PM-2383] Introduce bulk-collection-assignment-dialog.component * [PM-2383] Add bulk assign collections option to org vault
This commit is contained in:
parent
905d177873
commit
bac0874dc0
|
@ -15,4 +15,5 @@ export type VaultItemEvent =
|
|||
| { type: "delete"; items: VaultItem[] }
|
||||
| { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" }
|
||||
| { type: "moveToFolder"; items: CipherView[] }
|
||||
| { type: "moveToOrganization"; items: CipherView[] };
|
||||
| { type: "moveToOrganization"; items: CipherView[] }
|
||||
| { type: "assignToCollections"; items: CipherView[] };
|
||||
|
|
|
@ -46,6 +46,15 @@
|
|||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "access" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showAdminActions && bulkAssignToCollectionsAllowed"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="assignToCollections()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="bulkMoveAllowed"
|
||||
type="button"
|
||||
|
|
|
@ -42,6 +42,7 @@ export class VaultItemsComponent {
|
|||
@Input() allCollections: CollectionView[] = [];
|
||||
@Input() allGroups: GroupView[] = [];
|
||||
@Input() showBulkEditCollectionAccess = false;
|
||||
@Input() showBulkAddToCollections = false;
|
||||
@Input() showPermissionsColumn = false;
|
||||
@Input() viewingOrgVault: boolean;
|
||||
|
||||
|
@ -89,6 +90,10 @@ export class VaultItemsComponent {
|
|||
);
|
||||
}
|
||||
|
||||
get bulkAssignToCollectionsAllowed() {
|
||||
return this.ciphers.length > 0;
|
||||
}
|
||||
|
||||
protected canEditCollection(collection: CollectionView): boolean {
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (collection.id === Unassigned) {
|
||||
|
@ -182,4 +187,13 @@ export class VaultItemsComponent {
|
|||
.map((item) => item.collection),
|
||||
});
|
||||
}
|
||||
|
||||
protected assignToCollections() {
|
||||
this.event({
|
||||
type: "assignToCollections",
|
||||
items: this.selection.selected
|
||||
.filter((item) => item.cipher !== undefined)
|
||||
.map((item) => item.cipher),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
<span class="tw-text-sm tw-normal-case tw-text-muted">
|
||||
{{ pluralize(editableItemCount, "item", "items") }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<p>{{ "bulkCollectionAssignmentDialogDescription" | i18n }}</p>
|
||||
|
||||
<p *ngIf="readonlyItemCount > 0">
|
||||
{{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }}
|
||||
</p>
|
||||
|
||||
<div class="tw-flex">
|
||||
<bit-form-field class="tw-grow">
|
||||
<bit-label>{{ "selectCollectionsToAssign" | i18n }}</bit-label>
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
[baseItems]="availableCollections"
|
||||
[removeSelectedItems]="true"
|
||||
(onItemsConfirmed)="selectCollections($event)"
|
||||
></bit-multi-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<td bitCell>{{ "assignToTheseCollections" | i18n }}</td>
|
||||
<td bitCell class="tw-w-20"></td>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let item of selectedCollections; let i = index">
|
||||
<td bitCell>
|
||||
<i class="bwi bwi-collection" aria-hidden="true"></i>
|
||||
{{ item.labelName }}
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="muted"
|
||||
appA11yTitle="{{ 'remove' | i18n }} {{ item.labelName }}"
|
||||
(click)="unselectCollection(i)"
|
||||
></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="selectedCollections.length == 0">
|
||||
<td bitCell>
|
||||
{{ "noCollectionsAssigned" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton buttonType="primary" [bitAction]="submit" [disabled]="!isValid">
|
||||
{{ "assign" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
|
@ -0,0 +1,191 @@
|
|||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { DialogService, SelectItemView } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
export interface BulkCollectionAssignmentDialogParams {
|
||||
organizationId: OrganizationId;
|
||||
|
||||
/**
|
||||
* The ciphers to be assigned to the collections selected in the dialog.
|
||||
*/
|
||||
ciphers: CipherView[];
|
||||
|
||||
/**
|
||||
* The collections available to assign the ciphers to.
|
||||
*/
|
||||
availableCollections: CollectionView[];
|
||||
|
||||
/**
|
||||
* The currently filtered collection. Selected by default. If the user deselects it in the dialog then it will be
|
||||
* removed from the ciphers upon submission.
|
||||
*/
|
||||
activeCollection?: CollectionView;
|
||||
}
|
||||
|
||||
export enum BulkCollectionAssignmentDialogResult {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [SharedModule],
|
||||
selector: "app-bulk-collection-assignment-dialog",
|
||||
templateUrl: "./bulk-collection-assignment-dialog.component.html",
|
||||
standalone: true,
|
||||
})
|
||||
export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnInit {
|
||||
protected totalItemCount: number;
|
||||
protected editableItemCount: number;
|
||||
protected readonlyItemCount: number;
|
||||
protected availableCollections: SelectItemView[] = [];
|
||||
protected selectedCollections: SelectItemView[] = [];
|
||||
|
||||
private editableItems: CipherView[] = [];
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected pluralize = (count: number, singular: string, plural: string) =>
|
||||
`${count} ${this.i18nService.t(count === 1 ? singular : plural)}`;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: BulkCollectionAssignmentDialogParams,
|
||||
private dialogRef: DialogRef<BulkCollectionAssignmentDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const v1FCEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
const org = await this.organizationService.get(this.params.organizationId);
|
||||
|
||||
if (org.canEditAllCiphers(v1FCEnabled)) {
|
||||
this.editableItems = this.params.ciphers;
|
||||
} else {
|
||||
this.editableItems = this.params.ciphers.filter((c) => c.edit);
|
||||
}
|
||||
|
||||
this.editableItemCount = this.editableItems.length;
|
||||
|
||||
// If no ciphers are editable, close the dialog
|
||||
if (this.editableItemCount == 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("nothingSelected"),
|
||||
);
|
||||
this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled);
|
||||
}
|
||||
|
||||
this.totalItemCount = this.params.ciphers.length;
|
||||
this.readonlyItemCount = this.totalItemCount - this.editableItemCount;
|
||||
|
||||
this.availableCollections = this.params.availableCollections.map((c) => ({
|
||||
icon: "bwi-collection",
|
||||
id: c.id,
|
||||
labelName: c.name,
|
||||
listName: c.name,
|
||||
}));
|
||||
|
||||
// If the active collection is set, select it by default
|
||||
if (this.params.activeCollection) {
|
||||
this.selectCollections([
|
||||
{
|
||||
icon: "bwi-collection",
|
||||
id: this.params.activeCollection.id,
|
||||
labelName: this.params.activeCollection.name,
|
||||
listName: this.params.activeCollection.name,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private sortItems = (a: SelectItemView, b: SelectItemView) =>
|
||||
this.i18nService.collator.compare(a.labelName, b.labelName);
|
||||
|
||||
selectCollections(items: SelectItemView[]) {
|
||||
this.selectedCollections = [...this.selectedCollections, ...items].sort(this.sortItems);
|
||||
|
||||
this.availableCollections = this.availableCollections.filter(
|
||||
(item) => !items.find((i) => i.id === item.id),
|
||||
);
|
||||
}
|
||||
|
||||
unselectCollection(i: number) {
|
||||
const removed = this.selectedCollections.splice(i, 1);
|
||||
this.availableCollections = [...this.availableCollections, ...removed].sort(this.sortItems);
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return this.params.activeCollection != null || this.selectedCollections.length > 0;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (!this.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cipherIds = this.editableItems.map((i) => i.id as CipherId);
|
||||
|
||||
if (this.selectedCollections.length > 0) {
|
||||
await this.cipherService.bulkUpdateCollectionsWithServer(
|
||||
this.params.organizationId,
|
||||
cipherIds,
|
||||
this.selectedCollections.map((i) => i.id as CollectionId),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.params.activeCollection != null &&
|
||||
this.selectedCollections.find((c) => c.id === this.params.activeCollection.id) == null
|
||||
) {
|
||||
await this.cipherService.bulkUpdateCollectionsWithServer(
|
||||
this.params.organizationId,
|
||||
cipherIds,
|
||||
[this.params.activeCollection.id as CollectionId],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("successfullyAssignedCollections"),
|
||||
);
|
||||
|
||||
this.dialogRef.close(BulkCollectionAssignmentDialogResult.Saved);
|
||||
};
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<BulkCollectionAssignmentDialogParams>,
|
||||
) {
|
||||
return dialogService.open<
|
||||
BulkCollectionAssignmentDialogResult,
|
||||
BulkCollectionAssignmentDialogParams
|
||||
>(BulkCollectionAssignmentDialogComponent, config);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./bulk-collection-assignment-dialog.component";
|
|
@ -54,6 +54,7 @@
|
|||
[showBulkEditCollectionAccess]="
|
||||
(showBulkEditCollectionAccess$ | async) && organization?.flexibleCollections
|
||||
"
|
||||
[showBulkAddToCollections]="organization?.flexibleCollections"
|
||||
[viewingOrgVault]="true"
|
||||
>
|
||||
</app-vault-items>
|
||||
|
|
|
@ -46,6 +46,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
@ -86,6 +87,10 @@ import { getNestedCollectionTree } from "../utils/collection-utils";
|
|||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
import {
|
||||
BulkCollectionAssignmentDialogComponent,
|
||||
BulkCollectionAssignmentDialogResult,
|
||||
} from "./bulk-collection-assignment-dialog";
|
||||
import {
|
||||
BulkCollectionsDialogComponent,
|
||||
BulkCollectionsDialogResult,
|
||||
|
@ -631,6 +636,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
||||
} else if (event.type === "bulkEditCollectionAccess") {
|
||||
await this.bulkEditCollectionAccess(event.items);
|
||||
} else if (event.type === "assignToCollections") {
|
||||
await this.bulkAssignToCollections(event.items);
|
||||
} else if (event.type === "viewEvents") {
|
||||
await this.viewEvents(event.item);
|
||||
}
|
||||
|
@ -1092,6 +1099,41 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
async bulkAssignToCollections(items: CipherView[]) {
|
||||
if (items.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("nothingSelected"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let availableCollections: CollectionView[];
|
||||
|
||||
if (this.flexibleCollectionsV1Enabled) {
|
||||
availableCollections = await firstValueFrom(this.editableCollections$);
|
||||
} else {
|
||||
availableCollections = (
|
||||
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||
).filter((c) => c.id != Unassigned);
|
||||
}
|
||||
|
||||
const dialog = BulkCollectionAssignmentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
ciphers: items,
|
||||
organizationId: this.organization?.id as OrganizationId,
|
||||
availableCollections,
|
||||
activeCollection: this.activeFilter?.selectedCollectionNode?.node,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === BulkCollectionAssignmentDialogResult.Saved) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async viewEvents(cipher: CipherView) {
|
||||
await openEntityEventsDialog(this.dialogService, {
|
||||
data: {
|
||||
|
|
|
@ -7605,5 +7605,42 @@
|
|||
},
|
||||
"restrictedCollectionAccess": {
|
||||
"message": "You cannot add yourself to collections."
|
||||
},
|
||||
"assign": {
|
||||
"message": "Assign"
|
||||
},
|
||||
"assignToCollections": {
|
||||
"message": "Assign to collections"
|
||||
},
|
||||
"assignToTheseCollections": {
|
||||
"message": "Assign to these collections"
|
||||
},
|
||||
"bulkCollectionAssignmentDialogDescription": {
|
||||
"message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items."
|
||||
},
|
||||
"selectCollectionsToAssign": {
|
||||
"message": "Select collections to assign"
|
||||
},
|
||||
"noCollectionsAssigned": {
|
||||
"message": "No collections have been assigned"
|
||||
},
|
||||
"successfullyAssignedCollections": {
|
||||
"message": "Successfully assigned collections"
|
||||
},
|
||||
"bulkCollectionAssignmentWarning": {
|
||||
"message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.",
|
||||
"placeholders": {
|
||||
"total_count": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
},
|
||||
"readonly_count": {
|
||||
"content": "$2",
|
||||
"example": "3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"message": "Items"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,3 +7,4 @@ export type OrganizationId = Opaque<string, "OrganizationId">;
|
|||
export type CollectionId = Opaque<string, "CollectionId">;
|
||||
export type ProviderId = Opaque<string, "ProviderId">;
|
||||
export type PolicyId = Opaque<string, "PolicyId">;
|
||||
export type CipherId = Opaque<string, "CipherId">;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
|
@ -63,6 +64,19 @@ export abstract class CipherService {
|
|||
admin?: boolean,
|
||||
) => Promise<Cipher>;
|
||||
saveCollectionsWithServer: (cipher: Cipher) => Promise<any>;
|
||||
/**
|
||||
* Bulk update collections for many ciphers with the server
|
||||
* @param orgId
|
||||
* @param cipherIds
|
||||
* @param collectionIds
|
||||
* @param removeCollections - If true, the collections will be removed from the ciphers, otherwise they will be added
|
||||
*/
|
||||
bulkUpdateCollectionsWithServer: (
|
||||
orgId: OrganizationId,
|
||||
cipherIds: CipherId[],
|
||||
collectionIds: CollectionId[],
|
||||
removeCollections: boolean,
|
||||
) => Promise<void>;
|
||||
upsert: (cipher: CipherData | CipherData[]) => Promise<any>;
|
||||
replace: (ciphers: { [id: string]: CipherData }) => Promise<any>;
|
||||
clear: (userId: string) => Promise<any>;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { CipherId, CollectionId, OrganizationId } from "../../../types/guid";
|
||||
|
||||
export class CipherBulkUpdateCollectionsRequest {
|
||||
organizationId: OrganizationId;
|
||||
cipherIds: CipherId[];
|
||||
collectionIds: CollectionId[];
|
||||
removeCollections: boolean;
|
||||
constructor(
|
||||
organizationId: OrganizationId,
|
||||
cipherIds: CipherId[],
|
||||
collectionIds: CollectionId[],
|
||||
removeCollections: boolean = false,
|
||||
) {
|
||||
this.organizationId = organizationId;
|
||||
this.cipherIds = cipherIds;
|
||||
this.collectionIds = collectionIds;
|
||||
this.removeCollections = removeCollections;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,8 @@ import Domain from "../../platform/models/domain/domain-base";
|
|||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { UserKey, OrgKey } from "../../types/key";
|
||||
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType } from "../enums";
|
||||
|
@ -42,6 +43,7 @@ import { CipherBulkDeleteRequest } from "../models/request/cipher-bulk-delete.re
|
|||
import { CipherBulkMoveRequest } from "../models/request/cipher-bulk-move.request";
|
||||
import { CipherBulkRestoreRequest } from "../models/request/cipher-bulk-restore.request";
|
||||
import { CipherBulkShareRequest } from "../models/request/cipher-bulk-share.request";
|
||||
import { CipherBulkUpdateCollectionsRequest } from "../models/request/cipher-bulk-update-collections.request";
|
||||
import { CipherCollectionsRequest } from "../models/request/cipher-collections.request";
|
||||
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
||||
|
@ -685,6 +687,49 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
await this.upsert(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update collections for many ciphers with the server
|
||||
* @param orgId
|
||||
* @param cipherIds
|
||||
* @param collectionIds
|
||||
* @param removeCollections - If true, the collectionIds will be removed from the ciphers, otherwise they will be added
|
||||
*/
|
||||
async bulkUpdateCollectionsWithServer(
|
||||
orgId: OrganizationId,
|
||||
cipherIds: CipherId[],
|
||||
collectionIds: CollectionId[],
|
||||
removeCollections: boolean = false,
|
||||
): Promise<void> {
|
||||
const request = new CipherBulkUpdateCollectionsRequest(
|
||||
orgId,
|
||||
cipherIds,
|
||||
collectionIds,
|
||||
removeCollections,
|
||||
);
|
||||
|
||||
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
|
||||
|
||||
// Update the local state
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
|
||||
for (const id of cipherIds) {
|
||||
const cipher = ciphers[id];
|
||||
if (cipher) {
|
||||
if (removeCollections) {
|
||||
cipher.collectionIds = cipher.collectionIds?.filter(
|
||||
(cid) => !collectionIds.includes(cid as CollectionId),
|
||||
);
|
||||
} else {
|
||||
// Append to the collectionIds if it's not already there
|
||||
cipher.collectionIds = [...new Set([...(cipher.collectionIds ?? []), ...collectionIds])];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
}
|
||||
|
||||
async upsert(cipher: CipherData | CipherData[]): Promise<any> {
|
||||
let ciphers = await this.stateService.getEncryptedCiphers();
|
||||
if (ciphers == null) {
|
||||
|
|
Loading…
Reference in New Issue