mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-19152] Archive in Web (#16686)
* archive and unarchive an individual item * bulk archive and unachive * updates to text strings for archive empty state and tooltips * update translation keys to have an archive verb and noun differentiation * if premium member loses premium and has archive items. apply filter changes, and item more option changes * updating unArchive text * unarchive an archived item on edit if user loses premium * updates for unarchive btn, refactor archive flag for less churn * add services to cipher form stories * add refresh to archive calls in vault, update bulk archive copy * Do not show archive ability for deleted items * add archive check for login menu actions * remove assign to collections for archive filter * update bulk success message * add error handling for archive methods * fix null reference check * add unarchive icon --------- Co-authored-by: Nick Krantz <nick@livefront.com>
This commit is contained in:
@@ -558,7 +558,7 @@
|
|||||||
"message": "Archive",
|
"message": "Archive",
|
||||||
"description": "Verb"
|
"description": "Verb"
|
||||||
},
|
},
|
||||||
"unarchive": {
|
"unArchive": {
|
||||||
"message": "Unarchive"
|
"message": "Unarchive"
|
||||||
},
|
},
|
||||||
"itemsInArchive": {
|
"itemsInArchive": {
|
||||||
@@ -570,11 +570,11 @@
|
|||||||
"noItemsInArchiveDesc": {
|
"noItemsInArchiveDesc": {
|
||||||
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||||
},
|
},
|
||||||
"itemSentToArchive": {
|
"itemWasSentToArchive": {
|
||||||
"message": "Item sent to archive"
|
"message": "Item was sent to archive"
|
||||||
},
|
},
|
||||||
"itemRemovedFromArchive": {
|
"itemUnarchived": {
|
||||||
"message": "Item removed from archive"
|
"message": "Item was unarchived"
|
||||||
},
|
},
|
||||||
"archiveItem": {
|
"archiveItem": {
|
||||||
"message": "Archive item"
|
"message": "Archive item"
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export class ItemMoreOptionsComponent {
|
|||||||
await this.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
|
await this.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
message: this.i18nService.t("itemSentToArchive"),
|
message: this.i18nService.t("itemWasSentToArchive"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
{{ "clone" | i18n }}
|
{{ "clone" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem (click)="unarchive(cipher)">
|
<button type="button" bitMenuItem (click)="unarchive(cipher)">
|
||||||
{{ "unarchive" | i18n }}
|
{{ "unArchive" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export class ArchiveComponent {
|
|||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
message: this.i18nService.t("itemRemovedFromArchive"),
|
message: this.i18nService.t("itemUnarchived"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4149,7 +4149,7 @@
|
|||||||
"message": "Archive",
|
"message": "Archive",
|
||||||
"description": "Verb"
|
"description": "Verb"
|
||||||
},
|
},
|
||||||
"unarchive": {
|
"unArchive": {
|
||||||
"message": "Unarchive"
|
"message": "Unarchive"
|
||||||
},
|
},
|
||||||
"itemsInArchive": {
|
"itemsInArchive": {
|
||||||
@@ -4161,11 +4161,11 @@
|
|||||||
"noItemsInArchiveDesc": {
|
"noItemsInArchiveDesc": {
|
||||||
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||||
},
|
},
|
||||||
"itemSentToArchive": {
|
"itemWasSentToArchive": {
|
||||||
"message": "Item sent to archive"
|
"message": "Item was sent to archive"
|
||||||
},
|
},
|
||||||
"itemRemovedFromArchive": {
|
"itemUnarchived": {
|
||||||
"message": "Item removed from archive"
|
"message": "Item was unarchived"
|
||||||
},
|
},
|
||||||
"archiveItem": {
|
"archiveItem": {
|
||||||
"message": "Archive item"
|
"message": "Archive item"
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
label="{{ 'options' | i18n }}"
|
label="{{ 'options' | i18n }}"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #cipherOptions>
|
<bit-menu #cipherOptions>
|
||||||
<ng-container *ngIf="isNotDeletedLoginCipher">
|
<ng-container *ngIf="isActiveLoginCipher">
|
||||||
<button bitMenuItem type="button" (click)="copy('username')" *ngIf="hasUsernameToCopy">
|
<button bitMenuItem type="button" (click)="copy('username')" *ngIf="hasUsernameToCopy">
|
||||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||||
{{ "copyUsername" | i18n }}
|
{{ "copyUsername" | i18n }}
|
||||||
@@ -151,6 +151,20 @@
|
|||||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||||
{{ "eventLogs" | i18n }}
|
{{ "eventLogs" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
@if (showArchiveButton) {
|
||||||
|
<button bitMenuItem (click)="archive()" type="button">
|
||||||
|
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||||
|
{{ "archiveVerb" | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (showUnArchiveButton) {
|
||||||
|
<button bitMenuItem (click)="unarchive()" type="button">
|
||||||
|
<i class="bwi bwi-fw bwi-unarchive" aria-hidden="true"></i>
|
||||||
|
{{ "unArchive" | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<button bitMenuItem (click)="restore()" type="button" *ngIf="isDeleted && canRestoreCipher">
|
<button bitMenuItem (click)="restore()" type="button" *ngIf="isDeleted && canRestoreCipher">
|
||||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||||
{{ "restore" | i18n }}
|
{{ "restore" | i18n }}
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
* uses new permission restore logic from PM-15493
|
* uses new permission restore logic from PM-15493
|
||||||
*/
|
*/
|
||||||
@Input() canRestoreCipher: boolean;
|
@Input() canRestoreCipher: boolean;
|
||||||
|
/**
|
||||||
|
* user has archive permissions
|
||||||
|
*/
|
||||||
|
@Input() userCanArchive: boolean;
|
||||||
|
/**
|
||||||
|
* Enforge Org Data Ownership Policy Status
|
||||||
|
*/
|
||||||
|
@Input() enforceOrgDataOwnershipPolicy: boolean;
|
||||||
|
|
||||||
@Output() onEvent = new EventEmitter<VaultItemEvent<C>>();
|
@Output() onEvent = new EventEmitter<VaultItemEvent<C>>();
|
||||||
|
|
||||||
@@ -76,6 +84,20 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get showArchiveButton() {
|
||||||
|
return (
|
||||||
|
this.userCanArchive &&
|
||||||
|
!CipherViewLikeUtils.isArchived(this.cipher) &&
|
||||||
|
!CipherViewLikeUtils.isDeleted(this.cipher) &&
|
||||||
|
!this.cipher.organizationId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If item is archived always show unarchive button, even if user is not premium
|
||||||
|
protected get showUnArchiveButton() {
|
||||||
|
return CipherViewLikeUtils.isArchived(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
protected get clickAction() {
|
protected get clickAction() {
|
||||||
if (this.decryptionFailure) {
|
if (this.decryptionFailure) {
|
||||||
return "showFailedToDecrypt";
|
return "showFailedToDecrypt";
|
||||||
@@ -100,7 +122,12 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
return CipherViewLikeUtils.hasAttachments(this.cipher);
|
return CipherViewLikeUtils.hasAttachments(this.cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not show attachments button if:
|
||||||
|
// item is archived AND user is not premium user
|
||||||
protected get showAttachments() {
|
protected get showAttachments() {
|
||||||
|
if (CipherViewLikeUtils.isArchived(this.cipher) && !this.userCanArchive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return this.canEditCipher || this.hasAttachments;
|
return this.canEditCipher || this.hasAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +151,11 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
return CipherViewLikeUtils.decryptionFailure(this.cipher);
|
return CipherViewLikeUtils.decryptionFailure(this.cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do Not show Assign to Collections option if item is archived
|
||||||
protected get showAssignToCollections() {
|
protected get showAssignToCollections() {
|
||||||
|
if (CipherViewLikeUtils.isArchived(this.cipher)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
this.organizations?.length &&
|
this.organizations?.length &&
|
||||||
this.canAssignCollections &&
|
this.canAssignCollections &&
|
||||||
@@ -132,7 +163,16 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do NOT show clone option if:
|
||||||
|
// item is archived AND user is not premium user
|
||||||
|
// item is archived AND enforce org data ownership policy is on
|
||||||
protected get showClone() {
|
protected get showClone() {
|
||||||
|
if (
|
||||||
|
CipherViewLikeUtils.isArchived(this.cipher) &&
|
||||||
|
(!this.userCanArchive || this.enforceOrgDataOwnershipPolicy)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return this.cloneable && !CipherViewLikeUtils.isDeleted(this.cipher);
|
return this.cloneable && !CipherViewLikeUtils.isDeleted(this.cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +180,11 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
return this.useEvents && this.cipher.organizationId;
|
return this.useEvents && this.cipher.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get isNotDeletedLoginCipher() {
|
protected get isActiveLoginCipher() {
|
||||||
return (
|
return (
|
||||||
CipherViewLikeUtils.getType(this.cipher) === this.CipherType.Login &&
|
CipherViewLikeUtils.getType(this.cipher) === this.CipherType.Login &&
|
||||||
!CipherViewLikeUtils.isDeleted(this.cipher)
|
!CipherViewLikeUtils.isDeleted(this.cipher) &&
|
||||||
|
!CipherViewLikeUtils.isArchived(this.cipher)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,20 +232,20 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
|
|
||||||
protected get showCopyUsername(): boolean {
|
protected get showCopyUsername(): boolean {
|
||||||
const usernameCopy = CipherViewLikeUtils.hasCopyableValue(this.cipher, "username");
|
const usernameCopy = CipherViewLikeUtils.hasCopyableValue(this.cipher, "username");
|
||||||
return this.isNotDeletedLoginCipher && usernameCopy;
|
return this.isActiveLoginCipher && usernameCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get showCopyPassword(): boolean {
|
protected get showCopyPassword(): boolean {
|
||||||
const passwordCopy = CipherViewLikeUtils.hasCopyableValue(this.cipher, "password");
|
const passwordCopy = CipherViewLikeUtils.hasCopyableValue(this.cipher, "password");
|
||||||
return this.isNotDeletedLoginCipher && this.cipher.viewPassword && passwordCopy;
|
return this.isActiveLoginCipher && this.cipher.viewPassword && passwordCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get showCopyTotp(): boolean {
|
protected get showCopyTotp(): boolean {
|
||||||
return this.isNotDeletedLoginCipher && this.showTotpCopyButton;
|
return this.isActiveLoginCipher && this.showTotpCopyButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get showLaunchUri(): boolean {
|
protected get showLaunchUri(): boolean {
|
||||||
return this.isNotDeletedLoginCipher && this.canLaunch;
|
return this.isActiveLoginCipher && this.canLaunch;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get isDeletedCanRestore(): boolean {
|
protected get isDeletedCanRestore(): boolean {
|
||||||
@@ -236,6 +277,14 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
this.onEvent.emit({ type: "viewEvents", item: this.cipher });
|
this.onEvent.emit({ type: "viewEvents", item: this.cipher });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected archive() {
|
||||||
|
this.onEvent.emit({ type: "archive", items: [this.cipher] });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected unarchive() {
|
||||||
|
this.onEvent.emit({ type: "unarchive", items: [this.cipher] });
|
||||||
|
}
|
||||||
|
|
||||||
protected restore() {
|
protected restore() {
|
||||||
this.onEvent.emit({ type: "restore", items: [this.cipher] });
|
this.onEvent.emit({ type: "restore", items: [this.cipher] });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,6 @@ export type VaultItemEvent<C extends CipherViewLike> =
|
|||||||
| { type: "delete"; items: VaultItem<C>[] }
|
| { type: "delete"; items: VaultItem<C>[] }
|
||||||
| { type: "copyField"; item: C; field: "username" | "password" | "totp" }
|
| { type: "copyField"; item: C; field: "username" | "password" | "totp" }
|
||||||
| { type: "moveToFolder"; items: C[] }
|
| { type: "moveToFolder"; items: C[] }
|
||||||
| { type: "assignToCollections"; items: C[] };
|
| { type: "assignToCollections"; items: C[] }
|
||||||
|
| { type: "archive"; items: C[] }
|
||||||
|
| { type: "unarchive"; items: C[] };
|
||||||
|
|||||||
@@ -83,6 +83,22 @@
|
|||||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||||
{{ "assignToCollections" | i18n }}
|
{{ "assignToCollections" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button *ngIf="bulkArchiveAllowed" type="button" bitMenuItem (click)="bulkArchive()">
|
||||||
|
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||||
|
{{ "archiveVerb" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="bulkUnarchiveAllowed"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="bulkUnarchive()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-unarchive" aria-hidden="true"></i>
|
||||||
|
{{ "unArchive" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
*ngIf="canRestoreSelected$ | async"
|
*ngIf="canRestoreSelected$ | async"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -161,6 +177,8 @@
|
|||||||
"
|
"
|
||||||
(checkedToggled)="selection.toggle(item)"
|
(checkedToggled)="selection.toggle(item)"
|
||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
|
[userCanArchive]="userCanArchive"
|
||||||
|
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy"
|
||||||
></tr>
|
></tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
|||||||
@Input() addAccessStatus: number;
|
@Input() addAccessStatus: number;
|
||||||
@Input() addAccessToggle: boolean;
|
@Input() addAccessToggle: boolean;
|
||||||
@Input() activeCollection: CollectionView | undefined;
|
@Input() activeCollection: CollectionView | undefined;
|
||||||
|
@Input() userCanArchive: boolean;
|
||||||
|
@Input() enforceOrgDataOwnershipPolicy: boolean;
|
||||||
|
|
||||||
private restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$);
|
private restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$);
|
||||||
|
|
||||||
@@ -191,6 +193,30 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get bulkArchiveAllowed() {
|
||||||
|
if (this.selection.selected.length === 0 || !this.userCanArchive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.userCanArchive &&
|
||||||
|
!this.selection.selected.find(
|
||||||
|
(item) => item.cipher && (item.cipher.organizationId || item.cipher.archivedDate),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk Unarchive button should appear for Archive vault even if user does not have archive permissions
|
||||||
|
get bulkUnarchiveAllowed() {
|
||||||
|
if (this.selection.selected.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !this.selection.selected.find(
|
||||||
|
(item) => !item.cipher.archivedDate || item.cipher.organizationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//@TODO: remove this function when removing the limitItemDeletion$ feature flag.
|
//@TODO: remove this function when removing the limitItemDeletion$ feature flag.
|
||||||
get showDelete(): boolean {
|
get showDelete(): boolean {
|
||||||
if (this.selection.selected.length === 0) {
|
if (this.selection.selected.length === 0) {
|
||||||
@@ -221,7 +247,17 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get bulkAssignToCollectionsAllowed() {
|
get bulkAssignToCollectionsAllowed() {
|
||||||
return this.showBulkAddToCollections && this.ciphers.length > 0;
|
return (
|
||||||
|
this.showBulkAddToCollections &&
|
||||||
|
this.ciphers.length > 0 &&
|
||||||
|
!this.anySelectedCiphersAreArchived
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get anySelectedCiphersAreArchived() {
|
||||||
|
return this.selection.selected.some(
|
||||||
|
(item) => item.cipher && CipherViewLikeUtils.isArchived(item.cipher),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canEditCollection(collection: CollectionView): boolean {
|
protected canEditCollection(collection: CollectionView): boolean {
|
||||||
@@ -270,6 +306,24 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected bulkArchive() {
|
||||||
|
this.event({
|
||||||
|
type: "archive",
|
||||||
|
items: this.selection.selected
|
||||||
|
.filter((item) => item.cipher !== undefined)
|
||||||
|
.map((item) => item.cipher),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bulkUnarchive() {
|
||||||
|
this.event({
|
||||||
|
type: "unarchive",
|
||||||
|
items: this.selection.selected
|
||||||
|
.filter((item) => item.cipher !== undefined)
|
||||||
|
.map((item) => item.cipher),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected bulkRestore() {
|
protected bulkRestore() {
|
||||||
this.event({
|
this.event({
|
||||||
type: "restore",
|
type: "restore",
|
||||||
|
|||||||
@@ -242,16 +242,13 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async buildAllFilters(): Promise<VaultFilterList> {
|
async buildAllFilters(): Promise<VaultFilterList> {
|
||||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const hasArchiveFlag = await firstValueFrom(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
||||||
const builderFilter = {} as VaultFilterList;
|
const builderFilter = {} as VaultFilterList;
|
||||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||||
builderFilter.typeFilter = await this.addTypeFilter();
|
builderFilter.typeFilter = await this.addTypeFilter();
|
||||||
builderFilter.folderFilter = await this.addFolderFilter();
|
builderFilter.folderFilter = await this.addFolderFilter();
|
||||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||||
if (
|
if (hasArchiveFlag) {
|
||||||
(await firstValueFrom(this.cipherArchiveService.userCanArchive$(userId))) ||
|
|
||||||
(await firstValueFrom(this.cipherArchiveService.showArchiveVault$(userId)))
|
|
||||||
) {
|
|
||||||
builderFilter.archiveFilter = await this.addArchiveFilter();
|
builderFilter.archiveFilter = await this.addArchiveFilter();
|
||||||
}
|
}
|
||||||
builderFilter.trashFilter = await this.addTrashFilter();
|
builderFilter.trashFilter = await this.addTrashFilter();
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
[useEvents]="false"
|
[useEvents]="false"
|
||||||
[showAdminActions]="false"
|
[showAdminActions]="false"
|
||||||
[showBulkAddToCollections]="true"
|
[showBulkAddToCollections]="true"
|
||||||
|
[userCanArchive]="userCanArchive$ | async"
|
||||||
|
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy$ | async"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
>
|
>
|
||||||
</app-vault-items>
|
</app-vault-items>
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import {
|
|||||||
getOrganizationById,
|
getOrganizationById,
|
||||||
OrganizationService,
|
OrganizationService,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
@@ -210,7 +212,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
.pipe(map((a) => a?.id))
|
.pipe(map((a) => a?.id))
|
||||||
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
||||||
|
|
||||||
private userCanArchive$ = this.accountService.activeAccount$.pipe(
|
protected userCanArchive$ = this.accountService.activeAccount$.pipe(
|
||||||
getUserId,
|
getUserId,
|
||||||
switchMap((userId) => {
|
switchMap((userId) => {
|
||||||
return this.cipherArchiveService.userCanArchive$(userId);
|
return this.cipherArchiveService.userCanArchive$(userId);
|
||||||
@@ -256,7 +258,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
},
|
},
|
||||||
archive: {
|
archive: {
|
||||||
title: "noItemsInArchive",
|
title: "noItemsInArchive",
|
||||||
description: "archivedItemsDescription",
|
description: "noItemsInArchiveDesc",
|
||||||
icon: this.itemTypesIcon,
|
icon: this.itemTypesIcon,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -275,6 +277,15 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
protected enforceOrgDataOwnershipPolicy$ = this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) =>
|
||||||
|
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -307,6 +318,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
private cipherArchiveService: CipherArchiveService,
|
private cipherArchiveService: CipherArchiveService,
|
||||||
private organizationWarningsService: OrganizationWarningsService,
|
private organizationWarningsService: OrganizationWarningsService,
|
||||||
|
private policyService: PolicyService,
|
||||||
private unifiedUpgradePromptService: UnifiedUpgradePromptService,
|
private unifiedUpgradePromptService: UnifiedUpgradePromptService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -405,7 +417,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
allowedCiphers$,
|
allowedCiphers$,
|
||||||
filter$,
|
filter$,
|
||||||
this.currentSearchText$,
|
this.currentSearchText$,
|
||||||
this.userCanArchive$,
|
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||||
concatMap(async ([ciphers, filter, searchText, archiveEnabled]) => {
|
concatMap(async ([ciphers, filter, searchText, archiveEnabled]) => {
|
||||||
@@ -653,12 +665,140 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
case "assignToCollections":
|
case "assignToCollections":
|
||||||
await this.bulkAssignToCollections(event.items);
|
await this.bulkAssignToCollections(event.items);
|
||||||
break;
|
break;
|
||||||
|
case "archive":
|
||||||
|
if (event.items.length === 1) {
|
||||||
|
await this.archive(event.items[0]);
|
||||||
|
} else {
|
||||||
|
await this.bulkArchive(event.items);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "unarchive":
|
||||||
|
if (event.items.length === 1) {
|
||||||
|
await this.unarchive(event.items[0]);
|
||||||
|
} else {
|
||||||
|
await this.bulkUnarchive(event.items);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.processingEvent = false;
|
this.processingEvent = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async archive(cipher: C) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "archiveItem" },
|
||||||
|
content: { key: "archiveItemConfirmDesc" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||||
|
if (!repromptPassed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
try {
|
||||||
|
await this.cipherArchiveService.archiveWithServer(cipher.id as CipherId, activeUserId);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("itemWasSentToArchive"),
|
||||||
|
});
|
||||||
|
this.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error("Error archiving cipher", e);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
message: this.i18nService.t("errorOccurred"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkArchive(ciphers: C[]) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "archiveBulkItems" },
|
||||||
|
content: { key: "archiveBulkItemsConfirmDesc" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.repromptCipher(ciphers))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
const cipherIds = ciphers.map((c) => c.id as CipherId);
|
||||||
|
try {
|
||||||
|
await this.cipherArchiveService.archiveWithServer(cipherIds as CipherId[], activeUserId);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("itemsWereSentToArchive"),
|
||||||
|
});
|
||||||
|
this.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error("Error archiving ciphers", e);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
message: this.i18nService.t("errorOccurred"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unarchive(cipher: C) {
|
||||||
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||||
|
if (!repromptPassed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("itemUnarchived"),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error("Error unarchiving cipher", e);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
message: this.i18nService.t("errorOccurred"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkUnarchive(ciphers: C[]) {
|
||||||
|
if (!(await this.repromptCipher(ciphers))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
const cipherIds = ciphers.map((c) => c.id as CipherId);
|
||||||
|
try {
|
||||||
|
await this.cipherArchiveService.unarchiveWithServer(cipherIds as CipherId[], activeUserId);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("bulkUnarchiveItems"),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error("Error unarchiving ciphers", e);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
message: this.i18nService.t("errorOccurred"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async applyOrganizationFilter(orgId: string) {
|
async applyOrganizationFilter(orgId: string) {
|
||||||
if (orgId == null) {
|
if (orgId == null) {
|
||||||
orgId = "MyVault";
|
orgId = "MyVault";
|
||||||
|
|||||||
@@ -11298,12 +11298,47 @@
|
|||||||
"message": "Archive",
|
"message": "Archive",
|
||||||
"description": "Verb"
|
"description": "Verb"
|
||||||
},
|
},
|
||||||
|
"unArchive": {
|
||||||
|
"message": "Unarchive"
|
||||||
|
},
|
||||||
|
"itemsInArchive": {
|
||||||
|
"message": "Items in archive"
|
||||||
|
},
|
||||||
"noItemsInArchive": {
|
"noItemsInArchive": {
|
||||||
"message": "No items in archive"
|
"message": "No items in archive"
|
||||||
},
|
},
|
||||||
"archivedItemsDescription": {
|
"noItemsInArchiveDesc": {
|
||||||
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||||
},
|
},
|
||||||
|
"itemWasSentToArchive": {
|
||||||
|
"message": "Item was sent to archive"
|
||||||
|
},
|
||||||
|
"itemsWereSentToArchive": {
|
||||||
|
"message": "Items were sent to archive"
|
||||||
|
},
|
||||||
|
"itemUnarchived": {
|
||||||
|
"message": "Item was unarchived"
|
||||||
|
},
|
||||||
|
"bulkArchiveItems": {
|
||||||
|
"message": "Items archived"
|
||||||
|
},
|
||||||
|
"bulkUnarchiveItems": {
|
||||||
|
"message": "Items unarchived"
|
||||||
|
},
|
||||||
|
"archiveItem": {
|
||||||
|
"message": "Archive item",
|
||||||
|
"description": "Verb"
|
||||||
|
},
|
||||||
|
"archiveItemConfirmDesc": {
|
||||||
|
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
||||||
|
},
|
||||||
|
"archiveBulkItems": {
|
||||||
|
"message": "Archive items",
|
||||||
|
"description": "Verb"
|
||||||
|
},
|
||||||
|
"archiveBulkItemsConfirmDesc": {
|
||||||
|
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive these items?"
|
||||||
|
},
|
||||||
"businessUnit": {
|
"businessUnit": {
|
||||||
"message": "Business Unit"
|
"message": "Business Unit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
|
||||||
export abstract class CipherArchiveService {
|
export abstract class CipherArchiveService {
|
||||||
|
abstract hasArchiveFlagEnabled$(): Observable<boolean>;
|
||||||
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
||||||
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
||||||
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
hasArchiveFlagEnabled$(): Observable<boolean> {
|
||||||
|
return this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that contains the list of ciphers that have been archived.
|
* Observable that contains the list of ciphers that have been archived.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
moduleMetadata,
|
moduleMetadata,
|
||||||
StoryObj,
|
StoryObj,
|
||||||
} from "@storybook/angular";
|
} from "@storybook/angular";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
@@ -155,6 +155,20 @@ export default {
|
|||||||
} as NudgeStatus),
|
} as NudgeStatus),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useValue: {
|
||||||
|
userCanArchive$: of(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AccountService,
|
||||||
|
useValue: {
|
||||||
|
activeAccount$: of({
|
||||||
|
name: "User 1",
|
||||||
|
}),
|
||||||
|
} as Partial<AccountService>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: CipherFormService,
|
provide: CipherFormService,
|
||||||
useClass: TestAddEditFormService,
|
useClass: TestAddEditFormService,
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ import { ChangeDetectorRef } from "@angular/core";
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { ReactiveFormsModule } from "@angular/forms";
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||||
|
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
|
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
|
||||||
import { ToastService } from "@bitwarden/components";
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
import { CipherFormConfig } from "../abstractions/cipher-form-config.service";
|
import { CipherFormConfig } from "../abstractions/cipher-form-config.service";
|
||||||
import { CipherFormService } from "../abstractions/cipher-form.service";
|
import { CipherFormService } from "../abstractions/cipher-form.service";
|
||||||
@@ -23,6 +27,10 @@ describe("CipherFormComponent", () => {
|
|||||||
|
|
||||||
const decryptCipher = jest.fn().mockResolvedValue(new CipherView());
|
const decryptCipher = jest.fn().mockResolvedValue(new CipherView());
|
||||||
|
|
||||||
|
const mockAccountService = mock<AccountService>();
|
||||||
|
const mockCipherArchiveService = mock<CipherArchiveService>();
|
||||||
|
const mockAddEditFormService = { saveCipher: jest.fn(), decryptCipher };
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
decryptCipher.mockClear();
|
decryptCipher.mockClear();
|
||||||
|
|
||||||
@@ -32,13 +40,15 @@ describe("CipherFormComponent", () => {
|
|||||||
{ provide: ChangeDetectorRef, useValue: {} },
|
{ provide: ChangeDetectorRef, useValue: {} },
|
||||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
{ provide: ToastService, useValue: { showToast: jest.fn() } },
|
{ provide: ToastService, useValue: { showToast: jest.fn() } },
|
||||||
{ provide: CipherFormService, useValue: { saveCipher: jest.fn(), decryptCipher } },
|
{ provide: CipherFormService, useValue: mockAddEditFormService },
|
||||||
{
|
{
|
||||||
provide: CipherFormCacheService,
|
provide: CipherFormCacheService,
|
||||||
useValue: { init: jest.fn(), getCachedCipherView: jest.fn() },
|
useValue: { init: jest.fn(), getCachedCipherView: jest.fn() },
|
||||||
},
|
},
|
||||||
{ provide: ViewCacheService, useValue: { signal: jest.fn(() => (): any => null) } },
|
{ provide: ViewCacheService, useValue: { signal: jest.fn(() => (): any => null) } },
|
||||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||||
|
{ provide: AccountService, useValue: mockAccountService },
|
||||||
|
{ provide: CipherArchiveService, useValue: mockCipherArchiveService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
@@ -53,6 +63,29 @@ describe("CipherFormComponent", () => {
|
|||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("submit", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.config = { mode: "edit" } as CipherFormConfig;
|
||||||
|
|
||||||
|
component["updatedCipherView"] = new CipherView();
|
||||||
|
component["updatedCipherView"].archivedDate = new Date();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove archivedDate when user cannot archive and cipher is archived", async () => {
|
||||||
|
mockAccountService.activeAccount$ = of({ id: "user-id" as UserId } as Account);
|
||||||
|
mockCipherArchiveService.userCanArchive$.mockReturnValue(of(false));
|
||||||
|
mockAddEditFormService.saveCipher = jest.fn().mockResolvedValue(new CipherView());
|
||||||
|
|
||||||
|
const originalArchivedDate = component["updatedCipherView"]?.archivedDate;
|
||||||
|
expect(originalArchivedDate).not.toBeNull();
|
||||||
|
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
expect(component["updatedCipherView"]?.archivedDate).toBeNull();
|
||||||
|
expect(mockCipherArchiveService.userCanArchive$).toHaveBeenCalledWith("user-id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("website", () => {
|
describe("website", () => {
|
||||||
it("should return null if updatedCipherView is null", () => {
|
it("should return null if updatedCipherView is null", () => {
|
||||||
component["updatedCipherView"] = null as any;
|
component["updatedCipherView"] = null as any;
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ import {
|
|||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||||
import { BehaviorSubject, Subject } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, Subject, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import {
|
import {
|
||||||
@@ -301,6 +304,8 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private cipherFormCacheService: CipherFormCacheService,
|
private cipherFormCacheService: CipherFormCacheService,
|
||||||
|
private cipherArchiveService: CipherArchiveService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,6 +347,18 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userCanArchive = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the item is archived but user has lost archive permissions, unarchive the item.
|
||||||
|
if (!userCanArchive && this.updatedCipherView.archivedDate) {
|
||||||
|
this.updatedCipherView.archivedDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
const savedCipher = await this.addEditFormService.saveCipher(
|
const savedCipher = await this.addEditFormService.saveCipher(
|
||||||
this.updatedCipherView,
|
this.updatedCipherView,
|
||||||
this.config,
|
this.config,
|
||||||
|
|||||||
Reference in New Issue
Block a user