1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-31 07:33:23 +00:00
Files
browser/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts
cyprain-okeke ef06e9f03c [PM-15442]Upgrade modal additional instances (#13557)
* display inline information error message

* Add collection service

* Refactor the code

* Add a feature flag to the change

* Add the modal pop for free org

* Use custom error messages passed from the validator

* Add the js document

* Merge changes in main

* Add the changes after file movement

* remove these floating promises

* Adding unit test and seprating the validation

* fix the unit test request

* Remove  the conditional statment in test
2025-03-11 14:42:10 +01:00

275 lines
8.5 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import {
Unassigned,
CollectionView,
CollectionAdminService,
} from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import {
BreadcrumbsModule,
DialogService,
MenuModule,
SimpleDialogOptions,
} from "@bitwarden/components";
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { PipesModule } from "../pipes/pipes.module";
import {
All,
RoutedVaultFilterModel,
} from "../vault-filter/shared/models/routed-vault-filter.model";
@Component({
standalone: true,
selector: "app-vault-header",
templateUrl: "./vault-header.component.html",
imports: [
CommonModule,
MenuModule,
SharedModule,
BreadcrumbsModule,
HeaderModule,
PipesModule,
JslibModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultHeaderComponent implements OnInit {
protected Unassigned = Unassigned;
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
protected CipherType = CipherType;
/**
* Boolean to determine the loading state of the header.
* Shows a loading spinner if set to true
*/
@Input() loading: boolean;
/** Current active filter */
@Input() filter: RoutedVaultFilterModel;
/** All organizations that can be shown */
@Input() organizations: Organization[] = [];
/** Currently selected collection */
@Input() collection?: TreeNode<CollectionView>;
/** Whether 'Collection' option is shown in the 'New' dropdown */
@Input() canCreateCollections: boolean;
/** Emits an event when the new item button is clicked in the header */
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
/** Emits an event when the new collection button is clicked in the 'New' dropdown menu */
@Output() onAddCollection = new EventEmitter<null>();
/** Emits an event when the new folder button is clicked in the 'New' dropdown menu */
@Output() onAddFolder = new EventEmitter<null>();
/** Emits an event when the edit collection button is clicked in the header */
@Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType }>();
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
constructor(
private i18nService: I18nService,
private collectionAdminService: CollectionAdminService,
private dialogService: DialogService,
private router: Router,
private configService: ConfigService,
) {}
async ngOnInit() {}
/**
* The id of the organization that is currently being filtered on.
* This can come from a collection filter or organization filter, if applied.
*/
protected get activeOrganizationId() {
if (this.collection != undefined) {
return this.collection.node.organizationId;
}
if (this.filter.organizationId !== undefined) {
return this.filter.organizationId;
}
return undefined;
}
protected get activeOrganization() {
const organizationId = this.activeOrganizationId;
return this.organizations?.find((org) => org.id === organizationId);
}
protected get showBreadcrumbs() {
return this.filter.collectionId !== undefined && this.filter.collectionId !== All;
}
protected get title() {
if (this.filter.collectionId === Unassigned) {
return this.i18nService.t("unassigned");
}
if (this.collection) {
return this.collection.node.name;
}
if (this.filter.organizationId === Unassigned) {
return this.i18nService.t("myVault");
}
const activeOrganization = this.activeOrganization;
if (activeOrganization) {
return `${activeOrganization.name} ${this.i18nService.t("vault").toLowerCase()}`;
}
return this.i18nService.t("allVaults");
}
protected get icon() {
return this.filter.collectionId && this.filter.collectionId !== All ? "bwi-collection" : "";
}
/**
* A list of collection filters that form a chain from the organization root to currently selected collection.
* Begins from the organization root and excludes the currently selected collection.
*/
protected get collections() {
if (this.collection == undefined) {
return [];
}
const collections = [this.collection];
while (collections[collections.length - 1].parent != undefined) {
collections.push(collections[collections.length - 1].parent);
}
return collections
.slice(1)
.reverse()
.map((treeNode) => treeNode.node);
}
get canEditCollection(): boolean {
// Only edit collections if not editing "Unassigned"
if (this.collection == null) {
return false;
}
// Otherwise, check if we can edit the specified collection
const organization = this.organizations.find(
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canEdit(organization);
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {
this.onEditCollection.emit({ tab });
}
get canDeleteCollection(): boolean {
// Only delete collections if not deleting "Unassigned"
if (this.collection === undefined) {
return false;
}
// Otherwise, check if we can delete the specified collection
const organization = this.organizations.find(
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canDelete(organization);
}
deleteCollection() {
this.onDeleteCollection.emit();
}
protected addCipher(cipherType?: CipherType) {
this.onAddCipher.emit(cipherType);
}
async addFolder(): Promise<void> {
this.onAddFolder.emit();
}
async addCollection(): Promise<void> {
const organization = this.organizations?.find(
(org) => org.productTierType === ProductTierType.Free,
);
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
);
if (
this.organizations.length == 1 &&
organization.productTierType === ProductTierType.Free &&
isBreadcrumbEventLogsEnabled
) {
const collections = await this.collectionAdminService.getAll(organization.id);
if (collections.length === organization.maxCollections) {
await this.showFreeOrgUpgradeDialog(organization);
return;
}
}
this.onAddCollection.emit();
}
private async showFreeOrgUpgradeDialog(organization: Organization): Promise<void> {
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
title: this.i18nService.t("upgradeOrganization"),
content: this.i18nService.t(
organization.canEditSubscription
? "freeOrgMaxCollectionReachedManageBilling"
: "freeOrgMaxCollectionReachedNoManageBilling",
organization.maxCollections,
),
type: "primary",
};
if (organization.canEditSubscription) {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
} else {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn
}
const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts);
const result: boolean | undefined = await firstValueFrom(simpleDialog.closed);
if (!result) {
return;
}
if (organization.canEditSubscription) {
await this.router.navigate(["/organizations", organization.id, "billing", "subscription"], {
queryParams: { upgrade: true },
});
}
}
}