mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 13:40:06 +00:00
Merge branch 'innovation/archive/clients' into InnovationMenuItemChanges
This commit is contained in:
@@ -544,6 +544,21 @@
|
||||
"searchVault": {
|
||||
"message": "Search vault"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"edit": {
|
||||
"message": "Edit"
|
||||
},
|
||||
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
DevicesIcon,
|
||||
DeviceVerificationIcon,
|
||||
LockIcon,
|
||||
LoginComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LoginViaAuthRequestComponent,
|
||||
NewDeviceVerificationComponent,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
RegistrationLockAltIcon,
|
||||
@@ -35,11 +37,9 @@ import {
|
||||
RegistrationUserAddIcon,
|
||||
SetPasswordJitComponent,
|
||||
SsoComponent,
|
||||
TwoFactorTimeoutIcon,
|
||||
TwoFactorAuthComponent,
|
||||
TwoFactorAuthGuard,
|
||||
NewDeviceVerificationComponent,
|
||||
DeviceVerificationIcon,
|
||||
TwoFactorTimeoutIcon,
|
||||
UserLockIcon,
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -90,6 +90,7 @@ import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/v
|
||||
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
|
||||
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
|
||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||
import { ArchiveComponent } from "../vault/popup/settings/archive.component";
|
||||
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
||||
import { TrashComponent } from "../vault/popup/settings/trash.component";
|
||||
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||
@@ -691,6 +692,12 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "archive",
|
||||
component: ArchiveComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WritableSignal, signal } from "@angular/core";
|
||||
import { TestBed, discardPeriodicTasks, fakeAsync, tick } from "@angular/core/testing";
|
||||
import { signal, WritableSignal } from "@angular/core";
|
||||
import { discardPeriodicTasks, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, timeout } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
@@ -9,9 +9,10 @@ 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 { 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 { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
@@ -36,7 +37,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>;
|
||||
@@ -57,6 +58,7 @@ describe("VaultPopupItemsService", () => {
|
||||
const inlineMenuFieldQualificationServiceMock = mock<InlineMenuFieldQualificationService>();
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const accountServiceMock = mockAccountServiceWith(userId);
|
||||
const configServiceMock = mock<ConfigService>();
|
||||
|
||||
beforeEach(() => {
|
||||
allCiphers = cipherFactory(10);
|
||||
@@ -87,7 +89,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(c.id)),
|
||||
);
|
||||
@@ -128,8 +130,9 @@ describe("VaultPopupItemsService", () => {
|
||||
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
|
||||
collectionService.decryptedCollections$ = 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 = {
|
||||
@@ -154,6 +157,7 @@ describe("VaultPopupItemsService", () => {
|
||||
useValue: inlineMenuFieldQualificationServiceMock,
|
||||
},
|
||||
{ provide: PopupViewCacheService, useValue: viewCacheService },
|
||||
{ provide: ConfigService, useValue: configServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -277,7 +281,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);
|
||||
});
|
||||
});
|
||||
@@ -366,11 +370,11 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true when all ciphers are deleted", (done) => {
|
||||
it("should return true when all ciphers are deleted/archived", (done) => {
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue([
|
||||
{ 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[]);
|
||||
|
||||
service.emptyVault$.subscribe((empty) => {
|
||||
|
||||
@@ -24,6 +24,8 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -108,20 +110,31 @@ export class VaultPopupItemsService {
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
]),
|
||||
),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [
|
||||
...(failedToDecryptCiphers ?? []),
|
||||
...(ciphers ?? []),
|
||||
]),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
private archiveFeatureFlag$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM19148_InnovationArchive,
|
||||
);
|
||||
|
||||
private _activeCipherList$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
combineLatest([
|
||||
this.organizations$,
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.archiveFeatureFlag$,
|
||||
]).pipe(
|
||||
map(([organizations, collections, archiveFeatureEnabled]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
return ciphers
|
||||
.filter((c) => !c.isDeleted)
|
||||
.filter((c) => !c.isDeleted && (!archiveFeatureEnabled || !c.isArchived))
|
||||
.map(
|
||||
(cipher) =>
|
||||
new PopupCipherView(
|
||||
@@ -295,6 +308,21 @@ export class VaultPopupItemsService {
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that contains the list of ciphers that have been archived.
|
||||
*/
|
||||
archivedCiphers$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||
map((ciphers) => {
|
||||
return (
|
||||
ciphers
|
||||
.filter((cipher) => cipher.isArchived && !cipher.isDeleted)
|
||||
// Archived ciphers are individual only and never belong to an organization/collection
|
||||
.map((cipher) => new PopupCipherView(cipher))
|
||||
);
|
||||
}),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private vaultSettingsService: VaultSettingsService,
|
||||
@@ -306,6 +334,7 @@ export class VaultPopupItemsService {
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
private ngZone: NgZone,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
applyFilter(newSearchText: string) {
|
||||
|
||||
82
apps/browser/src/vault/popup/settings/archive.component.html
Normal file
82
apps/browser/src/vault/popup/settings/archive.component.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<popup-page>
|
||||
<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-section>
|
||||
|
||||
<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"
|
||||
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
|
||||
[title]="'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>
|
||||
} @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>
|
||||
167
apps/browser/src/vault/popup/settings/archive.component.ts
Normal file
167
apps/browser/src/vault/popup/settings/archive.component.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } 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 } 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,
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} 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";
|
||||
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
|
||||
|
||||
@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 vaultPopupItemsService = inject(VaultPopupItemsService);
|
||||
private dialogService = inject(DialogService);
|
||||
private passwordRepromptService = inject(PasswordRepromptService);
|
||||
private router = inject(Router);
|
||||
private cipherService = inject(CipherService);
|
||||
private accountService = inject(AccountService);
|
||||
private logService = inject(LogService);
|
||||
private toastService = inject(ToastService);
|
||||
private i18nService = inject(I18nService);
|
||||
|
||||
protected archivedCiphers$ = this.vaultPopupItemsService.archivedCiphers$;
|
||||
|
||||
async view(cipher: CipherView) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||
});
|
||||
}
|
||||
|
||||
async edit(cipher: CipherView) {
|
||||
if (!(await this.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/edit-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(cipher: CipherView) {
|
||||
if (!(await this.canInteract(cipher, true))) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: { key: "deleteItemConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
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.canInteract(cipher))) {
|
||||
return;
|
||||
}
|
||||
// TODO: Implement once endpoint is available
|
||||
}
|
||||
|
||||
async clone(cipher: CipherView) {
|
||||
if (!(await this.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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is able to interact with the cipher
|
||||
* (password re-prompt / decryption failure checks).
|
||||
* @param cipher
|
||||
* @param ignoreDecryptionFailure - If true, the decryption failure check will be ignored.
|
||||
* @private
|
||||
*/
|
||||
private async canInteract(cipher: CipherView, ignoreDecryptionFailure = false) {
|
||||
if (cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item *appIfFeature="FeatureFlag.PM19148_InnovationArchive">
|
||||
<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 }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
@@ -10,7 +11,6 @@ import { ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
@@ -22,7 +22,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
JslibModule,
|
||||
RouterModule,
|
||||
PopupPageComponent,
|
||||
PopupFooterComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
ItemModule,
|
||||
@@ -73,4 +72,6 @@ export class VaultSettingsV2Component implements OnInit {
|
||||
this.lastSync = this.i18nService.t("never");
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly FeatureFlag = FeatureFlag;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,12 @@ export default {
|
||||
canDeleteCipher$() {
|
||||
return of(true);
|
||||
},
|
||||
canRestoreCipher$() {
|
||||
return of(true);
|
||||
},
|
||||
canCloneCipher$() {
|
||||
return of(true);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -39,7 +39,7 @@ export class CipherData {
|
||||
passwordHistory?: PasswordHistoryData[];
|
||||
collectionIds?: string[];
|
||||
creationDate: string;
|
||||
deletedDate: string;
|
||||
deletedDate: string | null;
|
||||
archivedDate: string | null;
|
||||
reprompt: CipherRepromptType;
|
||||
key: string;
|
||||
|
||||
@@ -53,8 +53,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
passwordHistory: Password[];
|
||||
collectionIds: string[];
|
||||
creationDate: Date;
|
||||
deletedDate: Date;
|
||||
archivedDate: Date;
|
||||
deletedDate: Date | null;
|
||||
archivedDate: Date | null;
|
||||
reprompt: CipherRepromptType;
|
||||
key: EncString;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export class CipherRequest {
|
||||
attachments: { [id: string]: string };
|
||||
attachments2: { [id: string]: AttachmentRequest };
|
||||
lastKnownRevisionDate: Date;
|
||||
archivedDate: Date | null;
|
||||
reprompt: CipherRepromptType;
|
||||
key: string;
|
||||
|
||||
@@ -44,6 +45,7 @@ export class CipherRequest {
|
||||
this.notes = cipher.notes ? cipher.notes.encryptedString : null;
|
||||
this.favorite = cipher.favorite;
|
||||
this.lastKnownRevisionDate = cipher.revisionDate;
|
||||
this.archivedDate = cipher.archivedDate;
|
||||
this.reprompt = cipher.reprompt;
|
||||
this.key = cipher.key?.encryptedString;
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ export class CipherView implements View, InitializerMetadata {
|
||||
collectionIds: string[] = null;
|
||||
revisionDate: Date = null;
|
||||
creationDate: Date = null;
|
||||
deletedDate: Date = null;
|
||||
archivedDate: Date = null;
|
||||
deletedDate: Date | null = null;
|
||||
archivedDate: Date | null = null;
|
||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||
|
||||
/**
|
||||
@@ -194,6 +194,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
const view = new CipherView();
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
||||
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
||||
@@ -201,6 +202,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
Object.assign(view, obj, {
|
||||
revisionDate: revisionDate,
|
||||
deletedDate: deletedDate,
|
||||
archivedDate: archivedDate,
|
||||
attachments: attachments,
|
||||
fields: fields,
|
||||
passwordHistory: passwordHistory,
|
||||
|
||||
@@ -538,6 +538,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
|
||||
|
||||
const archiveFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19148_InnovationArchive,
|
||||
);
|
||||
|
||||
return ciphers.filter((cipher) => {
|
||||
const cipherIsLogin = cipher.type === CipherType.Login && cipher.login !== null;
|
||||
|
||||
@@ -545,6 +549,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (archiveFeatureEnabled && cipher.isArchived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(includeOtherTypes) &&
|
||||
includeOtherTypes.includes(cipher.type) &&
|
||||
@@ -566,8 +574,16 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
userId: UserId,
|
||||
): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted(userId);
|
||||
const archiveFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19148_InnovationArchive,
|
||||
);
|
||||
return ciphers
|
||||
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
|
||||
.filter(
|
||||
(cipher) =>
|
||||
cipher.deletedDate == null &&
|
||||
(!archiveFeatureEnabled || !cipher.isArchived) &&
|
||||
type.includes(cipher.type),
|
||||
)
|
||||
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user