1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

[PM-19406] Archive item actions Browser (#13933)

* [PM-19406] Cipher service changes

* [PM-19406] Wire up archive/unarchive actions
This commit is contained in:
Shane Melton
2025-03-20 16:46:18 -07:00
committed by GitHub
parent a146172168
commit 0e90e91f67
7 changed files with 128 additions and 2 deletions

View File

@@ -559,6 +559,18 @@
"noItemsInArchiveDesc": {
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
},
"itemSentToArchive": {
"message": "Item sent to archive"
},
"itemRemovedFromArchive": {
"message": "Item removed from archive"
},
"archiveItem": {
"message": "Archive item"
},
"archiveItemConfirmDesc": {
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
},
"edit": {
"message": "Edit"
},

View File

@@ -35,5 +35,8 @@
{{ "assignToCollections" | i18n }}
</a>
</ng-container>
<button type="button" bitMenuItem (click)="archive()" *ngIf="canArchive$ | async">
{{ "archive" | i18n }}
</button>
</bit-menu>
</bit-item-action>

View File

@@ -3,14 +3,17 @@
import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, Input, OnInit } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
import { BehaviorSubject, firstValueFrom, map, of, switchMap } from "rxjs";
import { filter } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -72,6 +75,24 @@ export class ItemMoreOptionsComponent implements OnInit {
switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)),
);
/**
* Observable that emits a boolean value indicating if the user is authorized to archive the cipher.
* @protected
*/
protected canArchive$ = this.configService
.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive)
.pipe(
switchMap((enabled) => {
if (!enabled) {
return of(false);
}
return this._cipher$.pipe(
filter((c) => c != null),
map((c) => !c.isArchived && c.organizationId == null),
);
}),
);
/** Boolean dependent on the current user having access to an organization */
protected hasOrganizations = false;
@@ -86,6 +107,7 @@ export class ItemMoreOptionsComponent implements OnInit {
private accountService: AccountService,
private organizationService: OrganizationService,
private cipherAuthorizationService: CipherAuthorizationService,
private configService: ConfigService,
) {}
async ngOnInit(): Promise<void> {
@@ -196,4 +218,23 @@ export class ItemMoreOptionsComponent implements OnInit {
queryParams: { cipherId: this.cipher.id },
});
}
async archive() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "archiveItem" },
content: { key: "archiveItemConfirmDesc" },
type: "info",
});
if (!confirmed) {
return;
}
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("itemSentToArchive"),
});
}
}

View File

@@ -118,7 +118,14 @@ export class ArchiveComponent {
if (!(await this.canInteract(cipher))) {
return;
}
// TODO: Implement once endpoint is available
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("itemRemovedFromArchive"),
});
}
async clone(cipher: CipherView) {

View File

@@ -197,6 +197,8 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
): Promise<any>;
abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
abstract restoreManyWithServer(ids: string[], orgId?: string): Promise<void>;
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
abstract getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<any>;
abstract setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId): Promise<void>;
/**

View File

@@ -0,0 +1,17 @@
import { CipherId } from "@bitwarden/common/types/guid";
export class CipherBulkArchiveRequest {
ids: CipherId[];
constructor(ids: CipherId[]) {
this.ids = ids == null ? [] : ids;
}
}
export class CipherBulkUnarchiveRequest {
ids: CipherId[];
constructor(ids: CipherId[]) {
this.ids = ids == null ? [] : ids;
}
}

View File

@@ -14,6 +14,10 @@ import {
} from "rxjs";
import { SemVer } from "semver";
import {
CipherBulkArchiveRequest,
CipherBulkUnarchiveRequest,
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
import { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
@@ -1313,6 +1317,46 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore({ id: id, revisionDate: response.revisionDate }, userId);
}
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
const request = new CipherBulkArchiveRequest(Array.isArray(ids) ? ids : [ids]);
const r = await this.apiService.send("PUT", "/ciphers/archive", request, true, true);
const response = new ListResponse(r, CipherResponse);
await this.updateEncryptedCipherState((ciphers) => {
for (const cipher of response.data) {
const localCipher = ciphers[cipher.id as CipherId];
if (localCipher == null) {
continue;
}
localCipher.archivedDate = cipher.archivedDate;
localCipher.revisionDate = cipher.revisionDate;
}
return ciphers;
}, userId);
}
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
const request = new CipherBulkUnarchiveRequest(Array.isArray(ids) ? ids : [ids]);
const r = await this.apiService.send("PUT", "/ciphers/unarchive", request, true, true);
const response = new ListResponse(r, CipherResponse);
await this.updateEncryptedCipherState((ciphers) => {
for (const cipher of response.data) {
const localCipher = ciphers[cipher.id as CipherId];
if (localCipher == null) {
continue;
}
localCipher.archivedDate = cipher.archivedDate;
localCipher.revisionDate = cipher.revisionDate;
}
return ciphers;
}, userId);
}
/**
* No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
* The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore