mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
[EC-886] Add functionality to vault header when viewing collections (#4602)
* [EC-886] Fix i18n key in vault filter section * [EC-886] Add shared functionality to vault-filter model Common patterns for determining the current filter status are used in the vault header, filter, and items components that could be extracted to the filter model to be consistent and less repetitive. * [EC-886] Add the individual vault header component Create a vault header component for encapsulation and to reduce the complexity of the vault component. * [EC-886] Add the organizational vault header component Create a vault header component for encapsulation and to reduce the complexity of the organizational vault component. * [EC-886] Use the new vault header component in the individual vault - Remove the old header template from the vault component and introduce the <app-vault-header> component instead. - Remove redundant logic from vault component that was moved to the header component and/or vault filter model. * [EC-886] Use the new vault header component in the organization vault - Remove the old header template from the org vault component and introduce the <app-org-vault-header> component instead. - Remove redundant logic from vault component that was moved to the header component and/or vault filter model. * [EC-886] Adjust vault header to make the word "vault" lowercase * [EC-886] Top align vault header to prevent button jumping * [EC-886] Center align collection icon/button with header text
This commit is contained in:
parent
37a374713c
commit
d3539a4a44
@ -0,0 +1,112 @@
|
|||||||
|
<div class="tw-mb-4 tw-flex tw-items-start tw-justify-between">
|
||||||
|
<div>
|
||||||
|
<bit-breadcrumbs *ngIf="activeFilter.collectionBreadcrumbs.length > 0">
|
||||||
|
<bit-breadcrumb
|
||||||
|
*ngFor="let collection of activeFilter.collectionBreadcrumbs; let first = first"
|
||||||
|
[icon]="first ? undefined : 'bwi-collection'"
|
||||||
|
(click)="applyCollectionFilter(collection)"
|
||||||
|
>
|
||||||
|
<!-- First node in the tree is the "Org Name Vault" item. The rest come from user input. -->
|
||||||
|
<ng-container *ngIf="first">
|
||||||
|
{{ activeOrganizationId | orgNameFromId: (organizations$ | async) }}
|
||||||
|
{{ "vault" | i18n | lowercase }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!first">{{ collection.node.name }}</ng-container>
|
||||||
|
</bit-breadcrumb>
|
||||||
|
</bit-breadcrumbs>
|
||||||
|
<h1 class="tw-mb-0 tw-mt-1 tw-flex tw-items-center tw-space-x-2">
|
||||||
|
<i
|
||||||
|
*ngIf="activeFilter.isCollectionSelected"
|
||||||
|
class="bwi bwi-collection"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="activeFilter.isCollectionSelected && !activeFilter.isUnassignedCollectionSelected"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-angle-down"
|
||||||
|
[bitMenuTriggerFor]="editCollectionMenu"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
aria-haspopup
|
||||||
|
></button>
|
||||||
|
<bit-menu #editCollectionMenu>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
*ngIf="canEditCollection(activeFilter.selectedCollectionNode.node)"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="editCollection(activeFilter.selectedCollectionNode.node, 'info')"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||||
|
{{ "editInfo" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
*ngIf="canEditCollection(activeFilter.selectedCollectionNode.node)"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="editCollection(activeFilter.selectedCollectionNode.node, 'access')"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
|
{{ "access" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
*ngIf="canDeleteCollection(activeFilter.selectedCollectionNode.node)"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="deleteCollection(activeFilter.selectedCollectionNode.node)"
|
||||||
|
>
|
||||||
|
<span class="tw-text-danger">
|
||||||
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
|
{{ "delete" | i18n }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</ng-container>
|
||||||
|
<small #actionSpinner [appApiAction]="actionPromise">
|
||||||
|
<ng-container *ngIf="$any(actionSpinner).loading">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!activeFilter.isDeleted" class="tw-shrink-0">
|
||||||
|
<div *ngIf="organization.canCreateNewCollections" appListDropdown>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
type="button"
|
||||||
|
[bitMenuTriggerFor]="addOptions"
|
||||||
|
id="newItemDropdown"
|
||||||
|
appA11yTitle="{{ 'new' | i18n }}"
|
||||||
|
>
|
||||||
|
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||||
|
<button type="button" bitMenuItem (click)="addCipher()">
|
||||||
|
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
|
||||||
|
{{ "item" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="addCollection()">
|
||||||
|
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||||
|
{{ "collection" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
*ngIf="!organization.canCreateNewCollections"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
(click)="addCipher()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,243 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom, lastValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { ProductType } from "@bitwarden/common/enums/productType";
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||||
|
import {
|
||||||
|
DialogService,
|
||||||
|
SimpleDialogCloseType,
|
||||||
|
SimpleDialogOptions,
|
||||||
|
SimpleDialogType,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { VaultFilterService } from "../../../../vault/app/vault/vault-filter/services/abstractions/vault-filter.service";
|
||||||
|
import { VaultFilter } from "../../../../vault/app/vault/vault-filter/shared/models/vault-filter.model";
|
||||||
|
import { CollectionFilter } from "../../../../vault/app/vault/vault-filter/shared/models/vault-filter.type";
|
||||||
|
import { CollectionAdminService, CollectionAdminView } from "../../core";
|
||||||
|
import {
|
||||||
|
CollectionDialogResult,
|
||||||
|
CollectionDialogTabType,
|
||||||
|
openCollectionDialog,
|
||||||
|
} from "../../shared";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-vault-header",
|
||||||
|
templateUrl: "./vault-header.component.html",
|
||||||
|
})
|
||||||
|
export class VaultHeaderComponent {
|
||||||
|
/**
|
||||||
|
* The organization currently being viewed
|
||||||
|
*/
|
||||||
|
@Input() organization: Organization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise that is used to determine the loading state of the header via the ApiAction directive.
|
||||||
|
* When the promise exists and is not resolved, the loading spinner will be shown.
|
||||||
|
*/
|
||||||
|
@Input() actionPromise: Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filter being actively applied to the vault view
|
||||||
|
*/
|
||||||
|
@Input() activeFilter: VaultFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the active filter has been modified by the header
|
||||||
|
*/
|
||||||
|
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event when a collection is modified or deleted via the header collection dropdown menu
|
||||||
|
*/
|
||||||
|
@Output() onCollectionChanged = new EventEmitter<CollectionView | null>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event when the new item button is clicked in the header
|
||||||
|
*/
|
||||||
|
@Output() onAddCipher = new EventEmitter<void>();
|
||||||
|
|
||||||
|
protected organizations$ = this.organizationService.organizations$;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private vaultFilterService: VaultFilterService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private logService: LogService,
|
||||||
|
private collectionAdminService: CollectionAdminService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the organization that is currently being filtered on.
|
||||||
|
* This can come from a collection filter, organization filter, or the current organization when viewed
|
||||||
|
* in the organization admin console and no other filters are applied.
|
||||||
|
*/
|
||||||
|
get activeOrganizationId() {
|
||||||
|
if (this.activeFilter.selectedCollectionNode != null) {
|
||||||
|
return this.activeFilter.selectedCollectionNode.node.organizationId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedOrganizationNode != null) {
|
||||||
|
return this.activeFilter.selectedOrganizationNode.node.id;
|
||||||
|
}
|
||||||
|
return this.organization.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
if (this.activeFilter.isCollectionSelected) {
|
||||||
|
return this.activeFilter.selectedCollectionNode.node.name;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.isUnassignedCollectionSelected) {
|
||||||
|
return this.i18nService.t("unassigned");
|
||||||
|
}
|
||||||
|
return `${this.organization.name} ${this.i18nService.t("vault").toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private showFreeOrgUpgradeDialog(): void {
|
||||||
|
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
||||||
|
title: this.i18nService.t("upgradeOrganization"),
|
||||||
|
content: this.i18nService.t(
|
||||||
|
this.organization.canManageBilling
|
||||||
|
? "freeOrgMaxCollectionReachedManageBilling"
|
||||||
|
: "freeOrgMaxCollectionReachedNoManageBilling",
|
||||||
|
this.organization.maxCollections
|
||||||
|
),
|
||||||
|
type: SimpleDialogType.PRIMARY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.organization.canManageBilling) {
|
||||||
|
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
|
||||||
|
} else {
|
||||||
|
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
|
||||||
|
orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleDialog = this.dialogService.openSimpleDialog(orgUpgradeSimpleDialogOpts);
|
||||||
|
|
||||||
|
firstValueFrom(simpleDialog.closed).then((result: SimpleDialogCloseType | undefined) => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) {
|
||||||
|
this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], {
|
||||||
|
queryParams: { upgrade: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
|
||||||
|
const filter = this.activeFilter;
|
||||||
|
filter.resetFilter();
|
||||||
|
filter.selectedCollectionNode = collection;
|
||||||
|
this.activeFilterChanged.emit(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
canEditCollection(c: CollectionAdminView): boolean {
|
||||||
|
// Only edit collections if we're in the org vault and not editing "Unassigned"
|
||||||
|
if (this.organization === undefined || c.id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check if we can edit the specified collection
|
||||||
|
return (
|
||||||
|
this.organization.canEditAnyCollection ||
|
||||||
|
(this.organization.canEditAssignedCollections && c.assigned)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCipher() {
|
||||||
|
this.onAddCipher.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCollection() {
|
||||||
|
if (this.organization.planProductType === ProductType.Free) {
|
||||||
|
const collections = await this.collectionAdminService.getAll(this.organization.id);
|
||||||
|
if (collections.length === this.organization.maxCollections) {
|
||||||
|
this.showFreeOrgUpgradeDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = openCollectionDialog(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
organizationId: this.organization?.id,
|
||||||
|
parentCollectionId: this.activeFilter.collectionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
|
||||||
|
this.onCollectionChanged.emit(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async editCollection(c: CollectionView, tab: "info" | "access"): Promise<void> {
|
||||||
|
const tabType = tab == "info" ? CollectionDialogTabType.Info : CollectionDialogTabType.Access;
|
||||||
|
|
||||||
|
const dialog = openCollectionDialog(this.dialogService, {
|
||||||
|
data: { collectionId: c?.id, organizationId: this.organization?.id, initialTab: tabType },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
|
||||||
|
this.onCollectionChanged.emit(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteCollection(c: CollectionAdminView): boolean {
|
||||||
|
// Only delete collections if we're in the org vault and not deleting "Unassigned"
|
||||||
|
if (this.organization === undefined || c.id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check if we can delete the specified collection
|
||||||
|
return (
|
||||||
|
this.organization?.canDeleteAnyCollection ||
|
||||||
|
(this.organization?.canDeleteAssignedCollections && c.assigned)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCollection(collection: CollectionView): Promise<void> {
|
||||||
|
if (!this.organization.canDeleteAssignedCollections) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("missingPermissions")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("deleteCollectionConfirmation"),
|
||||||
|
collection.name,
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.actionPromise = this.apiService.deleteCollection(this.organization?.id, collection.id);
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("deletedCollectionId", collection.name)
|
||||||
|
);
|
||||||
|
this.onCollectionChanged.emit(collection);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,65 +18,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
<div class="col-9">
|
||||||
<bit-breadcrumbs *ngIf="breadcrumbs.length > 0">
|
<app-org-vault-header
|
||||||
<bit-breadcrumb
|
[activeFilter]="activeFilter"
|
||||||
*ngFor="let collection of breadcrumbs; let first = first"
|
(activeFilterChanged)="applyVaultFilter($event)"
|
||||||
[icon]="first ? undefined : 'bwi-collection'"
|
(onCollectionChanged)="refreshItems()"
|
||||||
(click)="applyCollectionFilter(collection)"
|
[actionPromise]="vaultItemsComponent.actionPromise"
|
||||||
>
|
(onAddCipher)="addCipher()"
|
||||||
<!-- First node in the tree contains a translation key. The rest come from user input. -->
|
[organization]="organization"
|
||||||
<ng-container *ngIf="first">{{ collection.node.name | i18n }}</ng-container>
|
></app-org-vault-header>
|
||||||
<ng-container *ngIf="!first">{{ collection.node.name }}</ng-container>
|
|
||||||
</bit-breadcrumb>
|
|
||||||
</bit-breadcrumbs>
|
|
||||||
<div class="tw-mb-4 tw-flex">
|
|
||||||
<h1>
|
|
||||||
{{ "vaultItems" | i18n }}
|
|
||||||
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
|
|
||||||
<ng-container *ngIf="$any(actionSpinner).loading">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
</small>
|
|
||||||
</h1>
|
|
||||||
<div *ngIf="!activeFilter.isDeleted" class="ml-auto d-flex">
|
|
||||||
<div *ngIf="organization.canCreateNewCollections" class="dropdown mr-2" appListDropdown>
|
|
||||||
<button
|
|
||||||
bitButton
|
|
||||||
buttonType="primary"
|
|
||||||
type="button"
|
|
||||||
[bitMenuTriggerFor]="addOptions"
|
|
||||||
id="newItemDropdown"
|
|
||||||
appA11yTitle="{{ 'new' | i18n }}"
|
|
||||||
>
|
|
||||||
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
|
||||||
<button type="button" bitMenuItem (click)="addCipher()">
|
|
||||||
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
|
|
||||||
{{ "item" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="addCollection()">
|
|
||||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
|
||||||
{{ "collection" | i18n }}
|
|
||||||
</button>
|
|
||||||
</bit-menu>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
*ngIf="!organization?.canCreateNewCollections"
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="primary"
|
|
||||||
(click)="addCipher()"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<app-callout
|
<app-callout
|
||||||
type="warning"
|
type="warning"
|
||||||
*ngIf="activeFilter.isDeleted"
|
*ngIf="activeFilter.isDeleted"
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
import { combineLatest, firstValueFrom, lastValueFrom, Subject } from "rxjs";
|
import { combineLatest, firstValueFrom, Subject } from "rxjs";
|
||||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -17,29 +17,16 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|||||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { ProductType } from "@bitwarden/common/enums/productType";
|
|
||||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import {
|
import { DialogService } from "@bitwarden/components";
|
||||||
DialogService,
|
|
||||||
SimpleDialogCloseType,
|
|
||||||
SimpleDialogOptions,
|
|
||||||
SimpleDialogType,
|
|
||||||
} from "@bitwarden/components";
|
|
||||||
|
|
||||||
import { VaultFilterService } from "../../../vault/app/vault/vault-filter/services/abstractions/vault-filter.service";
|
import { VaultFilterService } from "../../../vault/app/vault/vault-filter/services/abstractions/vault-filter.service";
|
||||||
import { VaultFilter } from "../../../vault/app/vault/vault-filter/shared/models/vault-filter.model";
|
import { VaultFilter } from "../../../vault/app/vault/vault-filter/shared/models/vault-filter.model";
|
||||||
import { CollectionFilter } from "../../../vault/app/vault/vault-filter/shared/models/vault-filter.type";
|
|
||||||
import { CollectionAdminService } from "../core";
|
|
||||||
import { EntityEventsComponent } from "../manage/entity-events.component";
|
import { EntityEventsComponent } from "../manage/entity-events.component";
|
||||||
import {
|
|
||||||
CollectionDialogResult,
|
|
||||||
openCollectionDialog,
|
|
||||||
} from "../shared/components/collection-dialog";
|
|
||||||
|
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AddEditComponent } from "./add-edit.component";
|
||||||
import { AttachmentsComponent } from "./attachments.component";
|
import { AttachmentsComponent } from "./attachments.component";
|
||||||
@ -86,8 +73,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService
|
||||||
private collectionAdminService: CollectionAdminService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -108,7 +94,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
// verifies that the organization has been set
|
// verifies that the organization has been set
|
||||||
combineLatest([this.route.queryParams, this.route.parent.params])
|
combineLatest([this.route.queryParams, this.route.parent.params])
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(async ([qParams, params]) => {
|
switchMap(async ([qParams]) => {
|
||||||
const cipherId = getCipherIdFromParams(qParams);
|
const cipherId = getCipherIdFromParams(qParams);
|
||||||
if (!cipherId) {
|
if (!cipherId) {
|
||||||
return;
|
return;
|
||||||
@ -171,68 +157,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.go();
|
this.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshItems() {
|
||||||
|
this.vaultItemsComponent.actionPromise = this.vaultItemsComponent.refresh();
|
||||||
|
await this.vaultItemsComponent.actionPromise;
|
||||||
|
this.vaultItemsComponent.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
filterSearchText(searchText: string) {
|
filterSearchText(searchText: string) {
|
||||||
this.vaultItemsComponent.searchText = searchText;
|
this.vaultItemsComponent.searchText = searchText;
|
||||||
this.vaultItemsComponent.search(200);
|
this.vaultItemsComponent.search(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showFreeOrgUpgradeDialog(): void {
|
|
||||||
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
|
||||||
title: this.i18nService.t("upgradeOrganization"),
|
|
||||||
content: this.i18nService.t(
|
|
||||||
this.organization.canManageBilling
|
|
||||||
? "freeOrgMaxCollectionReachedManageBilling"
|
|
||||||
: "freeOrgMaxCollectionReachedNoManageBilling",
|
|
||||||
this.organization.maxCollections
|
|
||||||
),
|
|
||||||
type: SimpleDialogType.PRIMARY,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.organization.canManageBilling) {
|
|
||||||
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
|
|
||||||
} else {
|
|
||||||
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
|
|
||||||
orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn
|
|
||||||
}
|
|
||||||
|
|
||||||
const simpleDialog = this.dialogService.openSimpleDialog(orgUpgradeSimpleDialogOpts);
|
|
||||||
|
|
||||||
firstValueFrom(simpleDialog.closed).then((result: SimpleDialogCloseType | undefined) => {
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) {
|
|
||||||
this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], {
|
|
||||||
queryParams: { upgrade: true },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async addCollection() {
|
|
||||||
if (this.organization.planProductType === ProductType.Free) {
|
|
||||||
const collections = await this.collectionAdminService.getAll(this.organization.id);
|
|
||||||
if (collections.length === this.organization.maxCollections) {
|
|
||||||
this.showFreeOrgUpgradeDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialog = openCollectionDialog(this.dialogService, {
|
|
||||||
data: {
|
|
||||||
organizationId: this.organization?.id,
|
|
||||||
parentCollectionId: this.activeFilter.collectionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = await lastValueFrom(dialog.closed);
|
|
||||||
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
|
|
||||||
this.vaultItemsComponent.actionPromise = this.vaultItemsComponent.refresh();
|
|
||||||
await this.vaultItemsComponent.actionPromise;
|
|
||||||
this.vaultItemsComponent.actionPromise = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async editCipherAttachments(cipher: CipherView) {
|
async editCipherAttachments(cipher: CipherView) {
|
||||||
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
|
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
|
||||||
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });
|
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });
|
||||||
@ -362,26 +297,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get breadcrumbs(): TreeNode<CollectionFilter>[] {
|
|
||||||
if (!this.activeFilter.selectedCollectionNode) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const collections = [this.activeFilter.selectedCollectionNode];
|
|
||||||
while (collections[collections.length - 1].parent != undefined) {
|
|
||||||
collections.push(collections[collections.length - 1].parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return collections.map((c) => c).reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
|
|
||||||
const filter = this.activeFilter;
|
|
||||||
filter.resetFilter();
|
|
||||||
filter.selectedCollectionNode = collection;
|
|
||||||
this.applyVaultFilter(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private go(queryParams: any = null) {
|
private go(queryParams: any = null) {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
|
@ -10,6 +10,7 @@ import { SharedModule } from "../../shared/shared.module";
|
|||||||
import { CollectionBadgeModule } from "./collection-badge/collection-badge.module";
|
import { CollectionBadgeModule } from "./collection-badge/collection-badge.module";
|
||||||
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
import { VaultItemsComponent } from "./vault-items.component";
|
import { VaultItemsComponent } from "./vault-items.component";
|
||||||
import { VaultRoutingModule } from "./vault-routing.module";
|
import { VaultRoutingModule } from "./vault-routing.module";
|
||||||
import { VaultComponent } from "./vault.component";
|
import { VaultComponent } from "./vault.component";
|
||||||
@ -26,7 +27,7 @@ import { VaultComponent } from "./vault.component";
|
|||||||
PipesModule,
|
PipesModule,
|
||||||
BreadcrumbsModule,
|
BreadcrumbsModule,
|
||||||
],
|
],
|
||||||
declarations: [VaultComponent, VaultItemsComponent],
|
declarations: [VaultComponent, VaultItemsComponent, VaultHeaderComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
})
|
})
|
||||||
export class VaultModule {}
|
export class VaultModule {}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="headerInfo.isSelectable"
|
*ngIf="headerInfo.isSelectable"
|
||||||
appA11yTitle="{{ isOrganizationFilter ? 'vault' : ('filter' | i18n) }}: {{
|
appA11yTitle="{{ (isOrganizationFilter ? 'vault' : 'filter') | i18n }}: {{
|
||||||
headerNode.node.name | i18n
|
headerNode.node.name | i18n
|
||||||
}}"
|
}}"
|
||||||
class="filter-button"
|
class="filter-button"
|
||||||
|
@ -20,6 +20,48 @@ export class VaultFilter {
|
|||||||
selectedFolderNode: TreeNode<FolderFilter>;
|
selectedFolderNode: TreeNode<FolderFilter>;
|
||||||
selectedCollectionNode: TreeNode<CollectionFilter>;
|
selectedCollectionNode: TreeNode<CollectionFilter>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of collection filters that form a chain from the organization root to currently selected collection.
|
||||||
|
* To be used when rendering a breadcrumb UI for navigating the collection hierarchy.
|
||||||
|
* Begins from the organization root and excludes the currently selected collection.
|
||||||
|
*/
|
||||||
|
get collectionBreadcrumbs(): TreeNode<CollectionFilter>[] {
|
||||||
|
if (!this.isCollectionSelected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const collections = [this.selectedCollectionNode];
|
||||||
|
while (collections[collections.length - 1].parent != undefined) {
|
||||||
|
collections.push(collections[collections.length - 1].parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collections.slice(1).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vault is filtered by a specific collection
|
||||||
|
*/
|
||||||
|
get isCollectionSelected(): boolean {
|
||||||
|
return (
|
||||||
|
this.selectedCollectionNode != null &&
|
||||||
|
this.selectedCollectionNode.node.id !== "AllCollections"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vault is filtered by the "Unassigned" collection
|
||||||
|
*/
|
||||||
|
get isUnassignedCollectionSelected(): boolean {
|
||||||
|
return this.selectedCollectionNode != null && this.selectedCollectionNode.node.id === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vault is filtered by the users individual vault
|
||||||
|
*/
|
||||||
|
get isMyVaultSelected(): boolean {
|
||||||
|
return this.selectedOrganizationNode?.node.id === "MyVault";
|
||||||
|
}
|
||||||
|
|
||||||
get isFavorites(): boolean {
|
get isFavorites(): boolean {
|
||||||
return this.selectedCipherTypeNode?.node.type === "favorites";
|
return this.selectedCipherTypeNode?.node.type === "favorites";
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<div class="tw-mb-4 tw-flex tw-items-start tw-justify-between">
|
||||||
|
<div>
|
||||||
|
<bit-breadcrumbs *ngIf="activeFilter.collectionBreadcrumbs.length > 0">
|
||||||
|
<bit-breadcrumb
|
||||||
|
*ngFor="let collection of activeFilter.collectionBreadcrumbs; let first = first"
|
||||||
|
[icon]="first ? undefined : 'bwi-collection'"
|
||||||
|
(click)="applyCollectionFilter(collection)"
|
||||||
|
>
|
||||||
|
<!-- First node in the tree is the "Org Name Vault" item. The rest come from user input. -->
|
||||||
|
<ng-container *ngIf="first">
|
||||||
|
{{ activeOrganizationId | orgNameFromId: (organizations$ | async) }}
|
||||||
|
{{ "vault" | i18n | lowercase }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!first">{{ collection.node.name }}</ng-container>
|
||||||
|
</bit-breadcrumb>
|
||||||
|
</bit-breadcrumbs>
|
||||||
|
<h1 class="tw-mb-0 tw-mt-1 tw-flex tw-items-center tw-space-x-2">
|
||||||
|
<i
|
||||||
|
*ngIf="activeFilter.isCollectionSelected"
|
||||||
|
class="bwi bwi-collection"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<small #actionSpinner [appApiAction]="actionPromise">
|
||||||
|
<ng-container *ngIf="$any(actionSpinner).loading">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!activeFilter.isDeleted" class="tw-shrink-0">
|
||||||
|
<button type="button" bitButton buttonType="primary" (click)="addCipher()">
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,85 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||||
|
|
||||||
|
import { VaultFilter } from "../vault-filter/shared/models/vault-filter.model";
|
||||||
|
import { CollectionFilter } from "../vault-filter/shared/models/vault-filter.type";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-header",
|
||||||
|
templateUrl: "./vault-header.component.html",
|
||||||
|
})
|
||||||
|
export class VaultHeaderComponent {
|
||||||
|
/**
|
||||||
|
* Promise that is used to determine the loading state of the header via the ApiAction directive.
|
||||||
|
* When the promise exists and is not resolved, the loading spinner will be shown.
|
||||||
|
*/
|
||||||
|
@Input() actionPromise: Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filter being actively applied to the vault view
|
||||||
|
*/
|
||||||
|
@Input() activeFilter: VaultFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the active filter has been modified by the header
|
||||||
|
*/
|
||||||
|
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event when the new item button is clicked in the header
|
||||||
|
*/
|
||||||
|
@Output() onAddCipher = new EventEmitter<void>();
|
||||||
|
|
||||||
|
organizations$ = this.organizationService.organizations$;
|
||||||
|
|
||||||
|
constructor(private organizationService: OrganizationService, private i18nService: I18nService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the organization that is currently being filtered on.
|
||||||
|
* This can come from a collection filter or organization filter, if applied.
|
||||||
|
*/
|
||||||
|
get activeOrganizationId() {
|
||||||
|
if (this.activeFilter.selectedCollectionNode != null) {
|
||||||
|
return this.activeFilter.selectedCollectionNode.node.organizationId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedOrganizationNode != null) {
|
||||||
|
return this.activeFilter.selectedOrganizationNode.node.id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
if (this.activeFilter.isCollectionSelected) {
|
||||||
|
if (this.activeFilter.isUnassignedCollectionSelected) {
|
||||||
|
return this.i18nService.t("unassigned");
|
||||||
|
}
|
||||||
|
return this.activeFilter.selectedCollectionNode.node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeFilter.isMyVaultSelected) {
|
||||||
|
return this.i18nService.t("myVault");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeFilter?.selectedOrganizationNode != null) {
|
||||||
|
return `${this.activeFilter.selectedOrganizationNode.node.name} ${this.i18nService
|
||||||
|
.t("vault")
|
||||||
|
.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.i18nService.t("allVaults");
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
|
||||||
|
const filter = this.activeFilter;
|
||||||
|
filter.resetFilter();
|
||||||
|
filter.selectedCollectionNode = collection;
|
||||||
|
this.activeFilterChanged.emit(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCipher() {
|
||||||
|
this.onAddCipher.emit();
|
||||||
|
}
|
||||||
|
}
|
@ -17,43 +17,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||||
<bit-breadcrumbs *ngIf="breadcrumbs.length > 0">
|
<app-vault-header
|
||||||
<bit-breadcrumb
|
[activeFilter]="activeFilter"
|
||||||
*ngFor="let collection of breadcrumbs; let first = first"
|
(activeFilterChanged)="applyVaultFilter($event)"
|
||||||
[icon]="first ? undefined : 'bwi-collection'"
|
[actionPromise]="vaultItemsComponent.actionPromise"
|
||||||
(click)="applyCollectionFilter(collection)"
|
(onAddCipher)="addCipher()"
|
||||||
>
|
></app-vault-header>
|
||||||
<!-- First node in the tree contains a translation key. The rest come from user input. -->
|
|
||||||
<ng-container *ngIf="first">{{ collection.node.name | i18n }}</ng-container>
|
|
||||||
<ng-container *ngIf="!first">{{ collection.node.name }}</ng-container>
|
|
||||||
</bit-breadcrumb>
|
|
||||||
</bit-breadcrumbs>
|
|
||||||
<div class="tw-mb-4 tw-flex">
|
|
||||||
<h1>
|
|
||||||
{{ "vaultItems" | i18n }}
|
|
||||||
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
|
|
||||||
<ng-container *ngIf="$any(actionSpinner).loading">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
</small>
|
|
||||||
</h1>
|
|
||||||
<div class="ml-auto d-flex">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="primary"
|
|
||||||
(click)="addCipher()"
|
|
||||||
*ngIf="!activeFilter.isDeleted"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
@ -38,11 +38,7 @@ import { ShareComponent } from "./share.component";
|
|||||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||||
import {
|
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||||
CollectionFilter,
|
|
||||||
FolderFilter,
|
|
||||||
OrganizationFilter,
|
|
||||||
} from "./vault-filter/shared/models/vault-filter.type";
|
|
||||||
import { VaultItemsComponent } from "./vault-items.component";
|
import { VaultItemsComponent } from "./vault-items.component";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "VaultComponent";
|
const BroadcasterSubscriptionId = "VaultComponent";
|
||||||
@ -394,26 +390,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < DEFAULT_PBKDF2_ITERATIONS;
|
return kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < DEFAULT_PBKDF2_ITERATIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
get breadcrumbs(): TreeNode<CollectionFilter>[] {
|
|
||||||
if (!this.activeFilter.selectedCollectionNode) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const collections = [this.activeFilter.selectedCollectionNode];
|
|
||||||
while (collections[collections.length - 1].parent != undefined) {
|
|
||||||
collections.push(collections[collections.length - 1].parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return collections.map((c) => c).reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
|
|
||||||
const filter = this.activeFilter;
|
|
||||||
filter.resetFilter();
|
|
||||||
filter.selectedCollectionNode = collection;
|
|
||||||
this.applyVaultFilter(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private go(queryParams: any = null) {
|
private go(queryParams: any = null) {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
|
@ -4,12 +4,13 @@ import { BreadcrumbsModule } from "@bitwarden/components";
|
|||||||
|
|
||||||
import { CollectionBadgeModule } from "../../../app/organizations/vault/collection-badge/collection-badge.module";
|
import { CollectionBadgeModule } from "../../../app/organizations/vault/collection-badge/collection-badge.module";
|
||||||
import { GroupBadgeModule } from "../../../app/organizations/vault/group-badge/group-badge.module";
|
import { GroupBadgeModule } from "../../../app/organizations/vault/group-badge/group-badge.module";
|
||||||
import { SharedModule, LooseComponentsModule } from "../../../app/shared";
|
import { LooseComponentsModule, SharedModule } from "../../../app/shared";
|
||||||
|
|
||||||
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||||
import { PipesModule } from "./pipes/pipes.module";
|
import { PipesModule } from "./pipes/pipes.module";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
import { VaultItemsComponent } from "./vault-items.component";
|
import { VaultItemsComponent } from "./vault-items.component";
|
||||||
import { VaultRoutingModule } from "./vault-routing.module";
|
import { VaultRoutingModule } from "./vault-routing.module";
|
||||||
import { VaultComponent } from "./vault.component";
|
import { VaultComponent } from "./vault.component";
|
||||||
@ -27,7 +28,7 @@ import { VaultComponent } from "./vault.component";
|
|||||||
BulkDialogsModule,
|
BulkDialogsModule,
|
||||||
BreadcrumbsModule,
|
BreadcrumbsModule,
|
||||||
],
|
],
|
||||||
declarations: [VaultComponent, VaultItemsComponent],
|
declarations: [VaultComponent, VaultItemsComponent, VaultHeaderComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
})
|
})
|
||||||
export class VaultModule {}
|
export class VaultModule {}
|
||||||
|
Loading…
Reference in New Issue
Block a user