mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-24533] Initialize Archive Feature (#16226)
* [PM-19237] Add Archive Filter Type (#13852) * Browser can archive and unarchive items * Create Archive Cipher Service * Add flag and premium permissions to Archive --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: Shane <smelton@bitwarden.com> Co-authored-by: Patrick Pimentel <ppimentel@bitwarden.com>
This commit is contained in:
@@ -550,6 +550,33 @@
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
},
|
||||
"archive": {
|
||||
"message": "Archive"
|
||||
},
|
||||
"unarchive": {
|
||||
"message": "Unarchive"
|
||||
},
|
||||
"itemsInArchive": {
|
||||
"message": "Items in archive"
|
||||
},
|
||||
"noItemsInArchive": {
|
||||
"message": "No items in archive"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LoginViaAuthRequestComponent,
|
||||
NewDeviceVerificationComponent,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
RegistrationStartComponent,
|
||||
@@ -38,7 +39,6 @@ import {
|
||||
SsoComponent,
|
||||
TwoFactorAuthComponent,
|
||||
TwoFactorAuthGuard,
|
||||
NewDeviceVerificationComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
@@ -80,6 +80,7 @@ import { canAccessAtRiskPasswords } from "../vault/popup/guards/at-risk-password
|
||||
import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard";
|
||||
import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard";
|
||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||
import { ArchiveComponent } from "../vault/popup/settings/archive.component";
|
||||
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
|
||||
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
||||
import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
|
||||
@@ -675,6 +676,12 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "archive",
|
||||
component: ArchiveComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "security",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
|
||||
@@ -145,6 +145,8 @@ import {
|
||||
DefaultSshImportPromptService,
|
||||
PasswordRepromptService,
|
||||
SshImportPromptService,
|
||||
CipherArchiveService,
|
||||
DefaultCipherArchiveService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
|
||||
@@ -703,6 +705,18 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ExtensionDeviceManagementComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherArchiveService,
|
||||
useClass: DefaultCipherArchiveService,
|
||||
deps: [
|
||||
CipherService,
|
||||
ApiService,
|
||||
DialogService,
|
||||
PasswordRepromptService,
|
||||
BillingAccountProfileStateService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -38,5 +38,10 @@
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
@if (canArchive$ | async) {
|
||||
<button type="button" bitMenuItem (click)="archive()" *ngIf="canArchive$ | async">
|
||||
{{ "archive" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</bit-menu>
|
||||
</bit-item-action>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -11,6 +12,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
@@ -26,7 +28,7 @@ import {
|
||||
MenuModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { CipherArchiveService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
@@ -103,6 +105,20 @@ export class ItemMoreOptionsComponent {
|
||||
}),
|
||||
);
|
||||
|
||||
/** Observable Boolean checking if item can show Archive menu option */
|
||||
protected canArchive$ = combineLatest([
|
||||
this._cipher$,
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
),
|
||||
]).pipe(
|
||||
filter(([cipher, userId]) => cipher != null && userId != null),
|
||||
map(([cipher, canArchive]) => {
|
||||
return canArchive && !CipherViewLikeUtils.isArchived(cipher) && cipher.organizationId == null;
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
@@ -116,6 +132,7 @@ export class ItemMoreOptionsComponent {
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
private collectionService: CollectionService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
) {}
|
||||
|
||||
get canEdit() {
|
||||
@@ -233,4 +250,23 @@ export class ItemMoreOptionsComponent {
|
||||
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.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("itemSentToArchive"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { WritableSignal, signal } from "@angular/core";
|
||||
import { signal, WritableSignal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
|
||||
@@ -8,10 +8,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { CipherArchiveService } from "@bitwarden/vault";
|
||||
|
||||
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
@@ -43,7 +45,7 @@ describe("VaultPopupItemsService", () => {
|
||||
|
||||
let mockOrg: Organization;
|
||||
let mockCollections: CollectionView[];
|
||||
let activeUserLastSync$: BehaviorSubject<Date>;
|
||||
let activeUserLastSync$: BehaviorSubject<Date | null>;
|
||||
let viewCacheService: {
|
||||
signal: jest.Mock;
|
||||
mockSignal: WritableSignal<string | null>;
|
||||
@@ -64,6 +66,9 @@ describe("VaultPopupItemsService", () => {
|
||||
const inlineMenuFieldQualificationServiceMock = mock<InlineMenuFieldQualificationService>();
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const accountServiceMock = mockAccountServiceWith(userId);
|
||||
const configServiceMock = mock<ConfigService>();
|
||||
const cipherArchiveServiceMock = mock<CipherArchiveService>();
|
||||
cipherArchiveServiceMock.userCanArchive$.mockReturnValue(of(true));
|
||||
|
||||
const restrictedItemTypesService = {
|
||||
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||
@@ -101,7 +106,7 @@ describe("VaultPopupItemsService", () => {
|
||||
failedToDecryptCiphersSubject.asObservable(),
|
||||
);
|
||||
|
||||
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers);
|
||||
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers!);
|
||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
|
||||
ciphers.filter((c) => ["0", "1"].includes(uuidAsString(c.id))),
|
||||
);
|
||||
@@ -142,8 +147,9 @@ describe("VaultPopupItemsService", () => {
|
||||
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
|
||||
collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections));
|
||||
|
||||
activeUserLastSync$ = new BehaviorSubject(new Date());
|
||||
activeUserLastSync$ = new BehaviorSubject<Date | null>(new Date());
|
||||
syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$);
|
||||
configServiceMock.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const testSearchSignal = createMockSignal<string | null>("");
|
||||
viewCacheService = {
|
||||
@@ -168,10 +174,15 @@ describe("VaultPopupItemsService", () => {
|
||||
useValue: inlineMenuFieldQualificationServiceMock,
|
||||
},
|
||||
{ provide: PopupViewCacheService, useValue: viewCacheService },
|
||||
{ provide: ConfigService, useValue: configServiceMock },
|
||||
{
|
||||
provide: RestrictedItemTypesService,
|
||||
useValue: restrictedItemTypesService,
|
||||
},
|
||||
{
|
||||
provide: CipherArchiveService,
|
||||
useValue: cipherArchiveServiceMock,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -297,7 +308,7 @@ describe("VaultPopupItemsService", () => {
|
||||
const searchText = "Login";
|
||||
|
||||
searchService.searchCiphers.mockImplementation(async (userId, q, _, ciphers) => {
|
||||
return ciphers.filter((cipher) => {
|
||||
return ciphers!.filter((cipher) => {
|
||||
return cipher.name.includes(searchText);
|
||||
});
|
||||
});
|
||||
@@ -390,12 +401,12 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true when all ciphers are deleted", (done) => {
|
||||
it("should return true when all ciphers are deleted/archived", (done) => {
|
||||
cipherServiceMock.cipherListViews$.mockReturnValue(
|
||||
of([
|
||||
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
||||
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
||||
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
|
||||
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: false, isArchived: true },
|
||||
] as CipherView[]),
|
||||
);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { CipherArchiveService } from "@bitwarden/vault";
|
||||
|
||||
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
||||
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
|
||||
@@ -133,14 +135,24 @@ export class VaultPopupItemsService {
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
private userCanArchive$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => {
|
||||
return this.cipherArchiveService.userCanArchive$(userId);
|
||||
}),
|
||||
);
|
||||
|
||||
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
combineLatest([this.organizations$, this.decryptedCollections$, this.userCanArchive$]).pipe(
|
||||
map(([organizations, collections, canArchive]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
return ciphers
|
||||
.filter((c) => !CipherViewLikeUtils.isDeleted(c))
|
||||
.filter(
|
||||
(c) =>
|
||||
!CipherViewLikeUtils.isDeleted(c) &&
|
||||
(!canArchive || !CipherViewLikeUtils.isArchived(c)),
|
||||
)
|
||||
.map((cipher) => {
|
||||
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
|
||||
(colId) => collectionMap[colId as CollectionId],
|
||||
@@ -330,6 +342,8 @@ export class VaultPopupItemsService {
|
||||
private accountService: AccountService,
|
||||
private ngZone: NgZone,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private configService: ConfigService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
) {}
|
||||
|
||||
applyFilter(newSearchText: string) {
|
||||
|
||||
81
apps/browser/src/vault/popup/settings/archive.component.html
Normal file
81
apps/browser/src/vault/popup/settings/archive.component.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<popup-page [loading]="loading$ | async">
|
||||
<popup-header slot="header" [pageTitle]="'archive' | i18n" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
@if (archivedCiphers$ | async; as archivedItems) {
|
||||
@if (archivedItems.length) {
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ "itemsInArchive" | i18n }}
|
||||
</h2>
|
||||
<span bitTypography="body1" slot="end">{{ archivedItems.length }}</span>
|
||||
</bit-section-header>
|
||||
<bit-item-group>
|
||||
@for (cipher of archivedItems; track cipher.id) {
|
||||
<bit-item>
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||
(click)="view(cipher)"
|
||||
>
|
||||
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
@if (cipher.hasAttachments) {
|
||||
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
|
||||
}
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
</button>
|
||||
<bit-item-action slot="end">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
label="{{ 'options' | i18n }}"
|
||||
[appA11yTitle]="'moreOptionsTitle' | i18n: cipher.name"
|
||||
[bitMenuTriggerFor]="moreOptions"
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
<button type="button" bitMenuItem (click)="edit(cipher)">
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="clone(cipher)">
|
||||
{{ "clone" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="unarchive(cipher)">
|
||||
{{ "unarchive" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*appCanDeleteCipher="cipher"
|
||||
(click)="delete(cipher)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</bit-item-action>
|
||||
</bit-item>
|
||||
}
|
||||
</bit-item-group>
|
||||
</bit-section>
|
||||
} @else {
|
||||
<bit-no-items class="tw-flex tw-h-full tw-items-center tw-justify-center">
|
||||
<ng-container slot="title">
|
||||
{{ "noItemsInArchive" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container slot="description">
|
||||
{{ "noItemsInArchiveDesc" | i18n }}
|
||||
</ng-container>
|
||||
</bit-no-items>
|
||||
}
|
||||
}
|
||||
</popup-page>
|
||||
159
apps/browser/src/vault/popup/settings/archive.component.ts
Normal file
159
apps/browser/src/vault/popup/settings/archive.component.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map, Observable, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId, UserId } 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 {
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
MenuModule,
|
||||
NoItemsModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { CanDeleteCipherDirective, CipherArchiveService } from "@bitwarden/vault";
|
||||
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "archive.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
NoItemsModule,
|
||||
ItemModule,
|
||||
MenuModule,
|
||||
IconButtonModule,
|
||||
CanDeleteCipherDirective,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class ArchiveComponent {
|
||||
private dialogService = inject(DialogService);
|
||||
private router = inject(Router);
|
||||
private cipherService = inject(CipherService);
|
||||
private accountService = inject(AccountService);
|
||||
private logService = inject(LogService);
|
||||
private toastService = inject(ToastService);
|
||||
private i18nService = inject(I18nService);
|
||||
private cipherArchiveService = inject(CipherArchiveService);
|
||||
|
||||
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
protected archivedCiphers$ = this.userId$.pipe(
|
||||
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
|
||||
);
|
||||
|
||||
protected loading$ = this.archivedCiphers$.pipe(
|
||||
map(() => false),
|
||||
startWith(true),
|
||||
);
|
||||
|
||||
async view(cipher: CipherView) {
|
||||
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||
});
|
||||
}
|
||||
|
||||
async edit(cipher: CipherView) {
|
||||
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/edit-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(cipher: CipherView) {
|
||||
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: { key: "deleteItemConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.userId$);
|
||||
|
||||
try {
|
||||
await this.cipherService.softDeleteWithServer(cipher.id, activeUserId);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("deletedItem"),
|
||||
});
|
||||
}
|
||||
|
||||
async unarchive(cipher: CipherView) {
|
||||
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
const activeUserId = await firstValueFrom(this.userId$);
|
||||
|
||||
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("itemRemovedFromArchive"),
|
||||
});
|
||||
}
|
||||
|
||||
async clone(cipher: CipherView) {
|
||||
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipher.login?.hasFido2Credentials) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
type: "info",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.router.navigate(["/clone-cipher"], {
|
||||
queryParams: {
|
||||
clone: true.toString(),
|
||||
cipherId: cipher.id,
|
||||
type: cipher.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,14 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
@if (userCanArchive() || showArchiveFilter()) {
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/archive">
|
||||
{{ "archive" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
}
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/trash">
|
||||
{{ "trash" | i18n }}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
@@ -10,6 +11,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
import { CipherArchiveService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
@@ -32,6 +34,17 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
})
|
||||
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||
lastSync = "--";
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
// Check if user is premium user, they will be able to archive items
|
||||
protected userCanArchive = toSignal(
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||
);
|
||||
|
||||
// Check if user has archived items (does not check if user is premium)
|
||||
protected showArchiveFilter = toSignal(
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
|
||||
);
|
||||
|
||||
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
@@ -47,6 +60,7 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||
private i18nService: I18nService,
|
||||
private nudgeService: NudgesService,
|
||||
private accountService: AccountService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
Reference in New Issue
Block a user