[feature] Refine content of the organization delete request confirmation warning (#1508)

This commit updates the organization delete request confirmation warning based on new copy from the product team.

Changes are as follows:
* Add a load toggle to the organization delete modal, as we now have data to collect.
* Adjust how the families for enterprise error state for invalid sponserships connects with the organization delete component. Previously it just sent in a localization key to use for the description, but this commit adds a union type for identifying different delete flows and moves the FOE description localization key into the template with a condition.
* Move the callout on the organization delete component to above the description text.
* Adjust content of the typical organization delete request description based on copy from the product team.
  * This includes a list of item types in use by the organization that will be deleted and the amount of each type that exist in the organization.
This commit is contained in:
Addison Beck 2022-03-04 09:03:48 -05:00 committed by GitHub
parent 8242989b9d
commit e103ddf02f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 11 deletions

View File

@ -6,6 +6,7 @@
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="loaded"
>
<div class="modal-header">
<h2 class="modal-title" id="deleteOrganizationTitle">{{ "deleteOrganization" | i18n }}</h2>
@ -19,8 +20,30 @@
</button>
</div>
<div class="modal-body">
<p>{{ descriptionKey | i18n }}</p>
<app-callout type="warning">{{ "deleteOrganizationWarning" | i18n }}</app-callout>
<app-callout type="warning">{{
"deletingOrganizationIsPermanentWarning" | i18n: organizationName
}}</app-callout>
<p id="organizationDeleteDescription">
<ng-container
*ngIf="
deleteOrganizationRequestType === 'InvalidFamiliesForEnterprise';
else regularDelete
"
>
{{ "orgCreatedSponsorshipInvalid" | i18n }}
</ng-container>
<ng-template #regularDelete>
<ng-container *ngIf="organizationContentSummary.totalItemCount > 0">
{{ "deletingOrganizationContentWarning" | i18n: organizationName }}
<ul>
<li *ngFor="let type of organizationContentSummary.itemCountByType">
{{ type.count }} {{ type.localizationKey | i18n }}
</li>
</ul>
{{ "deletingOrganizationActiveUserAccountsWarning" | i18n }}
</ng-container>
</ng-template>
</p>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>

View File

@ -1,19 +1,58 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { Utils } from "jslib-common/misc/utils";
import { CipherView } from "jslib-common/models/view/cipherView";
import { Verification } from "jslib-common/types/verification";
class CountBasedLocalizationKey {
singular: string;
plural: string;
getKey(count: number) {
return count == 1 ? this.singular : this.plural;
}
constructor(singular: string, plural: string) {
this.singular = singular;
this.plural = plural;
}
}
class OrganizationContentSummaryItem {
count: number;
get localizationKey(): string {
return this.localizationKeyOptions.getKey(this.count);
}
private localizationKeyOptions: CountBasedLocalizationKey;
constructor(count: number, localizationKeyOptions: CountBasedLocalizationKey) {
this.count = count;
this.localizationKeyOptions = localizationKeyOptions;
}
}
class OrganizationContentSummary {
totalItemCount = 0;
itemCountByType: OrganizationContentSummaryItem[] = [];
}
@Component({
selector: "app-delete-organization",
templateUrl: "delete-organization.component.html",
})
export class DeleteOrganizationComponent {
export class DeleteOrganizationComponent implements OnInit {
organizationId: string;
descriptionKey = "deleteOrganizationDesc";
loaded: boolean;
deleteOrganizationRequestType: "InvalidFamiliesForEnterprise" | "RegularDelete" = "RegularDelete";
organizationName: string;
organizationContentSummary: OrganizationContentSummary = new OrganizationContentSummary();
@Output() onSuccess: EventEmitter<any> = new EventEmitter();
masterPassword: Verification;
@ -24,9 +63,15 @@ export class DeleteOrganizationComponent {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private userVerificationService: UserVerificationService,
private logService: LogService
private logService: LogService,
private cipherService: CipherService,
private organizationService: OrganizationService
) {}
async ngOnInit(): Promise<void> {
await this.load();
}
async submit() {
try {
this.formPromise = this.userVerificationService
@ -43,4 +88,44 @@ export class DeleteOrganizationComponent {
this.logService.error(e);
}
}
private async load() {
this.organizationName = (await this.organizationService.get(this.organizationId)).name;
this.organizationContentSummary = await this.buildOrganizationContentSummary();
this.loaded = true;
}
private async buildOrganizationContentSummary(): Promise<OrganizationContentSummary> {
const organizationContentSummary = new OrganizationContentSummary();
const organizationItems = (
await this.cipherService.getAllFromApiForOrganization(this.organizationId)
).filter((item) => item.deletedDate == null);
if (organizationItems.length < 1) {
return organizationContentSummary;
}
organizationContentSummary.totalItemCount = organizationItems.length;
for (const cipherType of Utils.iterateEnum(CipherType)) {
const count = this.getOrganizationItemCountByType(organizationItems, cipherType);
if (count > 0) {
organizationContentSummary.itemCountByType.push(
new OrganizationContentSummaryItem(
count,
this.getOrganizationItemLocalizationKeysByType(CipherType[cipherType])
)
);
}
}
return organizationContentSummary;
}
private getOrganizationItemCountByType(items: CipherView[], type: CipherType) {
return items.filter((item) => item.type == type).length;
}
private getOrganizationItemLocalizationKeysByType(type: string): CountBasedLocalizationKey {
return new CountBasedLocalizationKey(`type${type}`, `type${type}Plural`);
}
}

View File

@ -129,7 +129,7 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit {
this.deleteModalRef,
(comp) => {
comp.organizationId = organizationId;
comp.descriptionKey = "orgCreatedSponsorshipInvalid";
comp.deleteOrganizationRequestType = "InvalidFamiliesForEnterprise";
comp.onSuccess.subscribe(() => {
this.router.navigate(["/"]);
});

View File

@ -300,6 +300,18 @@
"typeSecureNote": {
"message": "Secure Note"
},
"typeLoginPlural": {
"message": "Logins"
},
"typeCardPlural": {
"message": "Cards"
},
"typeIdentityPlural": {
"message": "Identities"
},
"typeSecureNotePlural": {
"message": "Secure Notes"
},
"folders": {
"message": "Folders"
},
@ -2782,11 +2794,26 @@
"deleteOrganization": {
"message": "Delete Organization"
},
"deleteOrganizationDesc": {
"message": "Proceed below to delete this organization and all associated data. Individual user accounts will remain, though they will not be associated to this organization anymore. "
"deletingOrganizationContentWarning": {
"message": "Enter the master password to confirm deletion of $ORGANIZATION$ and all associated data. Vault data in $ORGANIZATION$ includes:",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"deleteOrganizationWarning": {
"message": "Deleting the organization is permanent. It cannot be undone."
"deletingOrganizationActiveUserAccountsWarning": {
"message": "User accounts will remain active after deletion but will no longer be associated to this organization."
},
"deletingOrganizationIsPermanentWarning": {
"message": "Deleting $ORGANIZATION$ is permanent and irreversible.",
"placeholders": {
"organization": {
"content": "$1",
"example": "My Org Name"
}
}
},
"organizationDeleted": {
"message": "Organization Deleted"