1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[PM-8125] Move Trash to the Vault settings page (#10736)

* created trash and trash container component

* added trash to vault settings

created observable to get deleted ciphers

* export icon

added locales

* remove edit and delete footver from trash view cipher

* Added helper text when viewing deleted ciphers

* prevent premature access of isDeleted from the cipher object

* simplified the condition to show the edit button

* return cipherView for deletedCiphers$ since that is what is used in the component

* changed section header to h6

* added routing animation

* Added restore to footer
This commit is contained in:
SmithThe4th 2024-08-30 15:46:26 -04:00 committed by GitHub
parent 963e339e4f
commit 5a73639946
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 356 additions and 7 deletions

View File

@ -4296,5 +4296,26 @@
},
"additionalContentAvailable": {
"message": "Additional content is available"
},
"itemsInTrash": {
"message": "Items in trash"
},
"noItemsInTrash": {
"message": "No items in trash"
},
"noItemsInTrashDesc": {
"message": "Items you delete will appear here and be permanently deleted after 30 days"
},
"trashWarning": {
"message": "Items that have been in trash more than 30 days will automatically be deleted"
},
"restore": {
"message": "Restore"
},
"deleteForever": {
"message": "Delete forever"
},
"noEditPermissions": {
"message": "You don't have permission to edit this item"
}
}

View File

@ -199,6 +199,9 @@ export const routerTransition = trigger("routerTransition", [
transition("vault-settings => sync", inSlideLeft),
transition("sync => vault-settings", outSlideRight),
transition("vault-settings => trash", inSlideLeft),
transition("trash => vault-settings", outSlideRight),
// Appearance settings
transition("tabs => appearance", inSlideLeft),
transition("appearance => tabs", outSlideRight),

View File

@ -91,6 +91,7 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { TrashComponent } from "../vault/popup/settings/trash.component";
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
@ -496,6 +497,12 @@ const routes: Routes = [
component: AccountSwitcherComponent,
data: { state: "account-switcher", doNotSaveUrl: true },
},
{
path: "trash",
component: TrashComponent,
canActivate: [authGuard],
data: { state: "trash" },
},
];
@Injectable()

View File

@ -5,13 +5,30 @@
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
<popup-footer slot="footer">
<button buttonType="primary" type="button" bitButton (click)="editCipher()">
<popup-footer slot="footer" *ngIf="showFooter()">
<button
*ngIf="!cipher.isDeleted"
buttonType="primary"
type="button"
bitButton
(click)="editCipher()"
>
{{ "edit" | i18n }}
</button>
<button
*ngIf="cipher.isDeleted && cipher.edit"
buttonType="primary"
type="button"
bitButton
[bitAction]="restore"
>
{{ "restore" | i18n }}
</button>
<button
slot="end"
*ngIf="cipher && cipher.edit"
*ngIf="cipher.edit"
[bitAction]="delete"
type="button"
buttonType="danger"

View File

@ -162,9 +162,28 @@ export class ViewV2Component {
return true;
};
restore = async (): Promise<void> => {
try {
await this.cipherService.restoreWithServer(this.cipher.id);
} catch (e) {
this.logService.error(e);
}
await this.router.navigate(["/vault"]);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItem"),
});
};
protected deleteCipher() {
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id)
: this.cipherService.softDeleteWithServer(this.cipher.id);
}
protected showFooter(): boolean {
return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit));
}
}

View File

@ -358,6 +358,24 @@ describe("VaultPopupItemsService", () => {
});
});
describe("deletedCiphers$", () => {
it("should return deleted ciphers", (done) => {
const ciphers = [
{ 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: "4", type: CipherType.Login, name: "Login 4", isDeleted: false },
] as CipherView[];
cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers);
service.deletedCiphers$.subscribe((deletedCiphers) => {
expect(deletedCiphers.length).toBe(3);
done();
});
});
});
describe("hasFilterApplied$", () => {
it("should return true if the search term provided is searchable", (done) => {
searchService.isSearchable.mockImplementation(async () => true);

View File

@ -76,7 +76,7 @@ export class VaultPopupItemsService {
* Observable that contains the list of all decrypted ciphers.
* @private
*/
private _cipherList$: Observable<PopupCipherView[]> = merge(
private _allDecryptedCiphers$: Observable<CipherView[]> = merge(
this.cipherService.ciphers$,
this.cipherService.localData$,
).pipe(
@ -84,6 +84,10 @@ export class VaultPopupItemsService {
tap(() => this._ciphersLoading$.next()),
waitUntilSync(this.syncService),
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private _activeCipherList$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
switchMap((ciphers) =>
combineLatest([
this.organizationService.organizations$,
@ -105,11 +109,10 @@ export class VaultPopupItemsService {
}),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
this._cipherList$,
this._activeCipherList$,
this._searchText$,
this.vaultPopupListFiltersService.filterFunction$,
]).pipe(
@ -208,7 +211,9 @@ export class VaultPopupItemsService {
/**
* Observable that indicates whether the user's vault is empty.
*/
emptyVault$: Observable<boolean> = this._cipherList$.pipe(map((ciphers) => !ciphers.length));
emptyVault$: Observable<boolean> = this._activeCipherList$.pipe(
map((ciphers) => !ciphers.length),
);
/**
* Observable that indicates whether there are no ciphers to show with the current filter.
@ -232,6 +237,14 @@ export class VaultPopupItemsService {
}),
);
/**
* Observable that contains the list of ciphers that have been deleted.
*/
deletedCiphers$: Observable<CipherView[]> = this._allDecryptedCiphers$.pipe(
map((ciphers) => ciphers.filter((c) => c.isDeleted)),
shareReplay({ refCount: false, bufferSize: 1 }),
);
constructor(
private cipherService: CipherService,
private vaultSettingsService: VaultSettingsService,

View File

@ -0,0 +1,40 @@
<bit-section *ngIf="ciphers?.length">
<bit-section-header>
<h2 bitTypography="h6">
{{ headerText }}
</h2>
<span bitTypography="body1" slot="end">{{ ciphers.length }}</span>
</bit-section-header>
<bit-item-group>
<bit-item *ngFor="let cipher of ciphers">
<a
bit-item-content
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
(click)="onViewCipher(cipher)"
>
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
<span data-testid="item-name">{{ cipher.name }}</span>
</a>
<ng-container slot="end" *ngIf="cipher.edit">
<bit-item-action>
<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)="restore(cipher)">
{{ "restore" | i18n }}
</button>
<button type="button" bitMenuItem (click)="delete(cipher)">
{{ "deleteForever" | i18n }}
</button>
</bit-menu>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
</bit-section>

View File

@ -0,0 +1,107 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
DialogService,
IconButtonModule,
ItemModule,
MenuModule,
SectionComponent,
SectionHeaderComponent,
ToastService,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@Component({
selector: "app-trash-list-items-container",
templateUrl: "trash-list-items-container.component.html",
standalone: true,
imports: [
CommonModule,
ItemModule,
JslibModule,
SectionComponent,
SectionHeaderComponent,
MenuModule,
IconButtonModule,
],
})
export class TrashListItemsContainerComponent {
/**
* The list of trashed items to display.
*/
@Input()
ciphers: CipherView[] = [];
@Input()
headerText: string;
constructor(
private cipherService: CipherService,
private logService: LogService,
private toastService: ToastService,
private i18nService: I18nService,
private dialogService: DialogService,
private passwordRepromptService: PasswordRepromptService,
private router: Router,
) {}
async restore(cipher: CipherView) {
try {
await this.cipherService.restoreWithServer(cipher.id);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredItem"),
});
} catch (e) {
this.logService.error(e);
}
}
async delete(cipher: CipherView) {
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
if (!repromptPassed) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },
content: { key: "permanentlyDeleteItemConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
try {
await this.cipherService.deleteWithServer(cipher.id);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedItem"),
});
} catch (e) {
this.logService.error(e);
}
}
async onViewCipher(cipher: CipherView) {
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
if (!repromptPassed) {
return;
}
await this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id, type: cipher.type },
});
}
}

View File

@ -0,0 +1,33 @@
<popup-page>
<popup-header slot="header" [pageTitle]="'trash' | i18n" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<ng-container *ngIf="deletedCiphers$ | async as deletedItems">
<bit-callout *ngIf="deletedItems.length" type="warning" title="{{ 'warning' | i18n }}">
{{ "trashWarning" | i18n }}
</bit-callout>
<ng-container *ngIf="deletedItems.length; else noDeletedItems">
<app-trash-list-items-container
[headerText]="'itemsInTrash' | i18n"
[ciphers]="deletedItems"
></app-trash-list-items-container>
</ng-container>
<ng-template #noDeletedItems>
<bit-no-items
[icon]="emptyTrashIcon"
class="tw-flex tw-h-full tw-items-center tw-justify-center"
>
<ng-container slot="title">
{{ "noItemsInTrash" | i18n }}
</ng-container>
<ng-container slot="description">
{{ "noItemsInTrashDesc" | i18n }}
</ng-container>
</bit-no-items>
</ng-template>
</ng-container>
</popup-page>

View File

@ -0,0 +1,37 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CalloutModule, NoItemsModule } from "@bitwarden/components";
import { VaultIcons } 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 { VaultListItemsContainerComponent } from "../components/vault-v2/vault-list-items-container/vault-list-items-container.component";
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
import { TrashListItemsContainerComponent } from "./trash-list-items-container/trash-list-items-container.component";
@Component({
templateUrl: "trash.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
VaultListItemsContainerComponent,
TrashListItemsContainerComponent,
CalloutModule,
NoItemsModule,
],
})
export class TrashComponent {
protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$;
protected emptyTrashIcon = VaultIcons.EmptyTrash;
constructor(private vaultPopupItemsService: VaultPopupItemsService) {}
}

View File

@ -24,6 +24,12 @@
<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 }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="sync()">
{{ "syncVaultNow" | i18n }}

View File

@ -3,6 +3,15 @@
{{ "cardExpiredMessage" | i18n }}
</bit-callout>
<!-- HELPER TEXT -->
<p
class="tw-text-sm tw-text-muted"
bitTypography="helper"
*ngIf="cipher?.isDeleted && !cipher?.edit"
>
{{ "noEditPermissions" | i18n }}
</p>
<!-- ITEM DETAILS -->
<app-item-details-v2
[cipher]="cipher"

View File

@ -0,0 +1,18 @@
import { svgIcon } from "@bitwarden/components";
export const EmptyTrash = svgIcon`
<svg width="174" height="100" viewBox="0 0 174 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M113.938 95.7919L121.802 25.2171C121.882 24.4997 121.32 23.8721 120.599 23.8721H52.8158C52.0939 23.8721 51.5324 24.4997 51.6123 25.2171L59.4759 95.7919C59.5442 96.405 60.0625 96.8687 60.6794 96.8687H112.735C113.352 96.8687 113.87 96.405 113.938 95.7919Z" fill="none"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M70.9462 38.4568C71.1965 38.44 71.4141 38.6291 71.4323 38.8793L74.2991 78.3031C74.3173 78.5532 74.1292 78.7696 73.879 78.7865C73.6288 78.8033 73.4112 78.6142 73.393 78.364L70.5261 38.9402C70.5079 38.6901 70.696 38.4737 70.9462 38.4568Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M87.4314 38.4082C87.6822 38.4082 87.8855 38.6115 87.8855 38.8623L87.8855 78.3824C87.8855 78.6332 87.6822 78.8365 87.4314 78.8365C87.1806 78.8365 86.9773 78.6332 86.9773 78.3824L86.9773 38.8623C86.9773 38.6115 87.1806 38.4082 87.4314 38.4082Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M103.917 38.4572C104.167 38.474 104.355 38.6905 104.337 38.9406L101.47 78.3644C101.452 78.6145 101.234 78.8037 100.984 78.7868C100.734 78.77 100.546 78.5536 100.564 78.3035L103.431 38.8797C103.449 38.6295 103.667 38.4404 103.917 38.4572Z"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" clip-rule="evenodd" d="M52.8159 24.7803C52.6354 24.7803 52.4951 24.9372 52.515 25.1165L59.3506 86.4648H76.54C76.7908 86.4648 76.9941 86.6682 76.9941 86.9189C76.9941 87.1697 76.7908 87.373 76.54 87.373H59.4518L60.3786 95.6913C60.3957 95.8446 60.5252 95.9605 60.6795 95.9605H112.735C112.889 95.9605 113.019 95.8446 113.036 95.6913L120.353 30.0186L58.2399 30.0186C57.9891 30.0186 57.7858 29.8152 57.7858 29.5645C57.7858 29.3137 57.9891 29.1104 58.2399 29.1104L120.455 29.1104L120.9 25.1165C120.919 24.9372 120.779 24.7803 120.599 24.7803H52.8159ZM50.7098 25.3177C50.5699 24.0621 51.5526 22.9639 52.8159 22.9639H120.599C121.862 22.9639 122.845 24.0622 122.705 25.3177L114.841 95.8924C114.722 96.9654 113.815 97.7769 112.735 97.7769H60.6795C59.5999 97.7769 58.6929 96.9654 58.5734 95.8924L50.7098 25.3177Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M88.4499 0.527344C88.9515 0.527344 89.3581 0.933958 89.3581 1.43554V11.2051C89.3581 11.7067 88.9515 12.1133 88.4499 12.1133C87.9484 12.1133 87.5417 11.7067 87.5417 11.2051V1.43554C87.5417 0.933958 87.9484 0.527344 88.4499 0.527344Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M56.8137 6.2397C57.2694 6.03014 57.8774 6.18948 58.1718 6.59559L64.3048 15.0563C64.5992 15.4624 64.4684 15.9615 64.0127 16.1711C63.557 16.3806 62.9489 16.2213 62.6545 15.8152L56.5215 7.35447C56.2272 6.94836 56.358 6.44926 56.8137 6.2397Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M73.1704 2.01822C73.6671 1.94846 74.1576 2.28892 74.266 2.77864L76.396 12.3998C76.5044 12.8895 76.1896 13.3431 75.6929 13.4129C75.1962 13.4826 74.7057 13.1422 74.5973 12.6524L72.4673 3.03126C72.3589 2.54153 72.6737 2.08798 73.1704 2.01822Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M104.344 2.13682C104.835 2.24151 105.103 2.71177 104.943 3.18717L101.768 12.6239C101.609 13.0993 101.081 13.3998 100.591 13.2951C100.1 13.1904 99.8321 12.7202 99.9921 12.2448L103.167 2.80806C103.327 2.33266 103.854 2.03213 104.344 2.13682Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M120.085 6.23979C120.541 6.44935 120.672 6.94845 120.378 7.35456L114.245 15.8153C113.95 16.2214 113.342 16.3807 112.886 16.1712C112.431 15.9616 112.3 15.4625 112.594 15.0564L118.727 6.59568C119.022 6.18957 119.63 6.03023 120.085 6.23979Z"/>
<path d="M129.384 27.2001L124.272 27.9646C123.505 28.0793 123.059 28.8635 123.353 29.579L150.626 95.9087C150.908 96.5946 151.738 96.888 152.38 96.5285L156.79 94.0573C157.31 93.766 157.526 93.1391 157.297 92.5833L130.726 27.9604C130.509 27.4321 129.95 27.1155 129.384 27.2001Z" fill="none"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" clip-rule="evenodd" d="M144.4 49.2028C145.911 49.8345 146.573 50.261 147.061 50.8557C147.593 51.504 147.976 52.4111 148.726 54.2353L151.93 62.0272C152.68 63.8513 153.045 64.7652 153.118 65.587C153.185 66.3407 153.004 67.0854 152.349 68.5355C152.26 68.732 152.174 68.9115 152.09 69.0865C151.969 69.3389 151.852 69.5821 151.738 69.8536C151.527 70.3581 151.273 71.0631 150.824 72.4643C150.693 72.8741 150.581 73.2651 150.49 73.6452L139.404 46.6825C139.741 46.9012 140.101 47.1138 140.489 47.3276C141.814 48.0582 142.501 48.408 143.015 48.6385C143.292 48.7625 143.55 48.864 143.818 48.9693C144.004 49.0424 144.195 49.1173 144.4 49.2028ZM134.933 40.574C134.938 40.5882 134.943 40.6024 134.949 40.6166C134.99 40.7164 135.031 40.8147 135.072 40.9115L151.431 80.6977C151.47 80.7949 151.51 80.8934 151.551 80.9931C151.557 81.0072 151.563 81.0211 151.569 81.0349L156.449 92.9041C156.507 93.043 156.453 93.1998 156.323 93.2726L151.912 95.7438C151.752 95.8337 151.544 95.7603 151.474 95.5888L124.201 29.2592C124.127 29.0803 124.239 28.8843 124.431 28.8556L129.543 28.0911C129.685 28.0699 129.824 28.1491 129.879 28.2812L134.933 40.574ZM136.764 40.2619C137.429 41.8455 137.981 42.8653 138.622 43.6471C139.287 44.4581 140.092 45.0652 141.355 45.7612C142.672 46.4872 143.303 46.8056 143.742 47.0027C144.006 47.1212 144.177 47.1875 144.389 47.2695C144.566 47.338 144.771 47.4175 145.082 47.5476C146.656 48.2055 147.682 48.778 148.476 49.7453C149.205 50.6349 149.689 51.8128 150.366 53.4594L150.422 53.5946L153.626 61.3866L153.681 61.5218C154.359 63.1683 154.843 64.3461 154.943 65.4735C155.051 66.6995 154.708 67.7893 154.026 69.2998C153.891 69.5983 153.797 69.7904 153.717 69.9561L153.717 69.9563C153.621 70.1545 153.543 70.3148 153.434 70.5741C153.253 71.0054 153.019 71.6508 152.572 73.0431C152.144 74.3778 151.988 75.3479 152.079 76.3759C152.166 77.3668 152.489 78.4733 153.13 80.066L158.145 92.2635C158.545 93.2361 158.168 94.3331 157.258 94.8429L152.847 97.3142C151.724 97.9433 150.271 97.4298 149.778 96.2295L122.505 29.8998C121.99 28.6476 122.771 27.2752 124.113 27.0746L129.225 26.3101C130.216 26.162 131.194 26.716 131.574 27.6406L136.764 40.2619Z"/>
</svg>
`;

View File

@ -1,3 +1,4 @@
export * from "./deactivated-org";
export * from "./no-folders";
export * from "./vault";
export * from "./empty-trash";