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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>;
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user