mirror of
https://github.com/bitwarden/browser
synced 2026-01-07 11:03:30 +00:00
Move web to apps/web and bitwarden_license/bit-web
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { IndividualVaultComponent } from "./individual-vault.component";
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: IndividualVaultComponent,
|
||||
data: { titleId: "vaults" },
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class IndividualVaultRoutingModule {}
|
||||
@@ -0,0 +1,121 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-vault-filter
|
||||
#vaultFilter
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event.id)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
></app-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||
<div class="page-header d-flex">
|
||||
<h1>
|
||||
{{ "vaultItems" | i18n }}
|
||||
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
||||
<ng-container *ngIf="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">
|
||||
<app-vault-bulk-actions
|
||||
[ciphersComponent]="ciphersComponent"
|
||||
[deleted]="activeFilter.status === 'trash'"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
(click)="addCipher()"
|
||||
*ngIf="activeFilter.status !== 'trash'"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-callout
|
||||
type="warning"
|
||||
*ngIf="activeFilter.status === 'trash'"
|
||||
icon="bwi-exclamation-triangle"
|
||||
>
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
<app-vault-ciphers
|
||||
(onCipherClicked)="editCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)"
|
||||
(onAddCipher)="addCipher()"
|
||||
(onShareClicked)="shareCipher($event)"
|
||||
(onCollectionsClicked)="editCipherCollections($event)"
|
||||
(onCloneClicked)="cloneCipher($event)"
|
||||
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
|
||||
>
|
||||
</app-vault-ciphers>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "updateKeyTitle" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "updateEncryptionKeyShortDesc" | i18n }}</p>
|
||||
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
|
||||
{{ "updateEncryptionKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-verify-email *ngIf="showVerifyEmail" class="d-block mb-4"></app-verify-email>
|
||||
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "updateBrowserDesc" | i18n }}</p>
|
||||
<a
|
||||
class="btn btn-block btn-outline-secondary"
|
||||
target="_blank"
|
||||
href="https://browser-update.org/update-browser.html"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="bwi bwi-star-f bwi-fw" aria-hidden="true"></i> {{ "goPremium" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<a
|
||||
class="btn btn-block btn-outline-secondary"
|
||||
routerLink="/settings/subscription/premium"
|
||||
>
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #folderAddEdit></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #share></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #updateKeyTemplate></ng-template>
|
||||
@@ -0,0 +1,415 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||
import { CipherType } from "jslib-common/enums/cipherType";
|
||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||
|
||||
import { UpdateKeyComponent } from "../../../../settings/update-key.component";
|
||||
import { AddEditComponent } from "../../../../vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../../../../vault/attachments.component";
|
||||
import { CiphersComponent } from "../../../../vault/ciphers.component";
|
||||
import { CollectionsComponent } from "../../../../vault/collections.component";
|
||||
import { FolderAddEditComponent } from "../../../../vault/folder-add-edit.component";
|
||||
import { ShareComponent } from "../../../../vault/share.component";
|
||||
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
|
||||
import { VaultService } from "../../vault.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault",
|
||||
templateUrl: "individual-vault.component.html",
|
||||
})
|
||||
export class IndividualVaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
folderAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
|
||||
@ViewChild("collections", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
updateKeyModalRef: ViewContainerRef;
|
||||
|
||||
favorites = false;
|
||||
folderId: string = null;
|
||||
collectionId: string = null;
|
||||
organizationId: string = null;
|
||||
myVaultOnly = false;
|
||||
showVerifyEmail = false;
|
||||
showBrowserOutdated = false;
|
||||
showUpdateKey = false;
|
||||
showPremiumCallout = false;
|
||||
deleted = false;
|
||||
trashCleanupWarning: string = null;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private tokenService: TokenService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private vaultService: VaultService,
|
||||
private cipherService: CipherService,
|
||||
private passwordRepromptService: PasswordRepromptService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
: "trashCleanupWarning"
|
||||
);
|
||||
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
await this.syncService.fullSync(false);
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
this.showPremiumCallout =
|
||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
this.filterComponent.reloadOrganizations();
|
||||
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
||||
|
||||
if (params.cipherId) {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = params.cipherId;
|
||||
if (params.action === "clone") {
|
||||
await this.cloneCipher(cipherView);
|
||||
} else if (params.action === "edit") {
|
||||
await this.editCipher(cipherView);
|
||||
}
|
||||
}
|
||||
await this.ciphersComponent.reload();
|
||||
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
if ((await this.cipherService.get(params.cipherId)) != null) {
|
||||
this.editCipherId(params.cipherId);
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("unknownCipher")
|
||||
);
|
||||
this.router.navigate([], {
|
||||
queryParams: { cipherId: null },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter),
|
||||
this.filterComponent.reloadOrganizations(),
|
||||
this.ciphersComponent.load(this.ciphersComponent.filter),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get isShowingCards() {
|
||||
return (
|
||||
this.showBrowserOutdated ||
|
||||
this.showPremiumCallout ||
|
||||
this.showUpdateKey ||
|
||||
this.showVerifyEmail
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
||||
this.activeFilter = vaultFilter;
|
||||
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
|
||||
this.filterComponent.searchPlaceholder = this.vaultService.calculateSearchBarLocalizationString(
|
||||
this.activeFilter
|
||||
);
|
||||
this.go();
|
||||
}
|
||||
|
||||
async applyOrganizationFilter(orgId: string) {
|
||||
if (orgId == null) {
|
||||
this.activeFilter.resetOrganization();
|
||||
this.activeFilter.myVaultOnly = true;
|
||||
} else {
|
||||
this.activeFilter.selectedOrganizationId = orgId;
|
||||
}
|
||||
await this.applyVaultFilter(this.activeFilter);
|
||||
}
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
}
|
||||
|
||||
private buildFilter(): (cipher: CipherView) => boolean {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
|
||||
}
|
||||
if (
|
||||
this.activeFilter.selectedFolder &&
|
||||
this.activeFilter.selectedFolderId != "none" &&
|
||||
cipherPassesFilter
|
||||
) {
|
||||
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
|
||||
}
|
||||
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null &&
|
||||
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
|
||||
}
|
||||
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
|
||||
}
|
||||
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
|
||||
async editCipherAttachments(cipher: CipherView) {
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
if (cipher.organizationId == null && !canAccessPremium) {
|
||||
this.messagingService.send("premiumRequired");
|
||||
return;
|
||||
} else if (cipher.organizationId != null) {
|
||||
const org = await this.organizationService.get(cipher.organizationId);
|
||||
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
|
||||
this.messagingService.send("upgradeOrganization", {
|
||||
organizationId: cipher.organizationId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let madeAttachmentChanges = false;
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
AttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
comp.onReuploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
}
|
||||
);
|
||||
|
||||
modal.onClosed.subscribe(async () => {
|
||||
if (madeAttachmentChanges) {
|
||||
await this.ciphersComponent.refresh();
|
||||
}
|
||||
madeAttachmentChanges = false;
|
||||
});
|
||||
}
|
||||
|
||||
async shareCipher(cipher: CipherView) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
ShareComponent,
|
||||
this.shareModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onSharedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
CollectionsComponent,
|
||||
this.collectionsModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onSavedCollections.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addFolder() {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
FolderAddEditComponent,
|
||||
this.folderAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.folderId = null;
|
||||
comp.onSavedFolder.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async editFolder(folderId: string) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
FolderAddEditComponent,
|
||||
this.folderAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.folderId = folderId;
|
||||
comp.onSavedFolder.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
});
|
||||
comp.onDeletedFolder.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
const component = await this.editCipher(null);
|
||||
component.type = this.activeFilter.cipherType;
|
||||
component.folderId = this.folderId === "none" ? null : this.folderId;
|
||||
if (this.activeFilter.selectedCollectionId != null) {
|
||||
const collection = this.filterComponent.collections.fullList.filter(
|
||||
(c) => c.id === this.activeFilter.selectedCollectionId
|
||||
);
|
||||
if (collection.length > 0) {
|
||||
component.organizationId = collection[0].organizationId;
|
||||
component.collectionIds = [this.activeFilter.selectedCollectionId];
|
||||
}
|
||||
}
|
||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||
component.folderId = this.activeFilter.selectedFolderId;
|
||||
}
|
||||
if (this.activeFilter.selectedOrganizationId) {
|
||||
component.organizationId = this.activeFilter.selectedOrganizationId;
|
||||
}
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView) {
|
||||
return this.editCipherId(cipher?.id);
|
||||
}
|
||||
|
||||
async editCipherId(id: string) {
|
||||
const cipher = await this.cipherService.get(id);
|
||||
if (cipher != null && cipher.reprompt != 0) {
|
||||
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
this.go({ cipherId: null });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
AddEditComponent,
|
||||
this.cipherAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = id;
|
||||
comp.onSavedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
comp.onDeletedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
comp.onRestoredCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
modal.onClosedPromise().then(() => {
|
||||
this.go({ cipherId: null });
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async cloneCipher(cipher: CipherView) {
|
||||
const component = await this.editCipher(cipher);
|
||||
component.cloneMode = true;
|
||||
}
|
||||
|
||||
async updateKey() {
|
||||
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
favorites: this.favorites ? true : null,
|
||||
type: this.activeFilter.cipherType,
|
||||
folderId: this.folderId,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
};
|
||||
}
|
||||
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { VaultModule } from "../../vault.module";
|
||||
|
||||
import { IndividualVaultRoutingModule } from "./individual-vault-routing.module";
|
||||
import { IndividualVaultComponent } from "./individual-vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultModule, IndividualVaultRoutingModule],
|
||||
declarations: [IndividualVaultComponent],
|
||||
exports: [IndividualVaultComponent],
|
||||
})
|
||||
export class IndividualVaultModule {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
|
||||
import { OrganizationNameBadgeComponent } from "./organization-name-badge.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [OrganizationNameBadgeComponent],
|
||||
exports: [OrganizationNameBadgeComponent],
|
||||
})
|
||||
export class OrganizationBadgeModule {}
|
||||
@@ -0,0 +1,9 @@
|
||||
<button
|
||||
bitBadge
|
||||
[style.color]="textColor"
|
||||
[style.background-color]="color"
|
||||
appA11yTitle="{{ organizationName }}"
|
||||
(click)="emitOnOrganizationClicked()"
|
||||
>
|
||||
{{ organizationName | ellipsis: 13 }}
|
||||
</button>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-badge",
|
||||
templateUrl: "organization-name-badge.component.html",
|
||||
})
|
||||
export class OrganizationNameBadgeComponent implements OnInit {
|
||||
@Input() organizationName: string;
|
||||
@Input() profileName: string;
|
||||
|
||||
@Output() onOrganizationClicked = new EventEmitter<string>();
|
||||
|
||||
color: string;
|
||||
textColor: string;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.organizationName == null || this.organizationName === "") {
|
||||
this.organizationName = this.i18nService.t("me");
|
||||
this.color = this.stringToColor(this.profileName.toUpperCase());
|
||||
}
|
||||
if (this.color == null) {
|
||||
this.color = this.stringToColor(this.organizationName.toUpperCase());
|
||||
}
|
||||
this.textColor = this.pickTextColorBasedOnBgColor();
|
||||
}
|
||||
|
||||
// This value currently isn't stored anywhere, only calculated in the app-avatar component
|
||||
// Once we are allowing org colors to be changed and saved, change this out
|
||||
private stringToColor(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
let color = "#";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += ("00" + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// There are a few ways to calculate text color for contrast, this one seems to fit accessibility guidelines best.
|
||||
// https://stackoverflow.com/a/3943023/6869691
|
||||
private pickTextColorBasedOnBgColor() {
|
||||
const color = this.color.charAt(0) === "#" ? this.color.substring(1, 7) : this.color;
|
||||
const r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
const g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
const b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? "black !important" : "white !important";
|
||||
}
|
||||
|
||||
emitOnOrganizationClicked() {
|
||||
this.onOrganizationClicked.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { OrganizationVaultComponent } from "./organization-vault.component";
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: OrganizationVaultComponent,
|
||||
data: { titleId: "vaults" },
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OrganizationVaultRoutingModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-vault-filter
|
||||
#vaultFilter
|
||||
[showFolders]="false"
|
||||
[showFavorites]="false"
|
||||
[activeFilter]="activeFilter"
|
||||
[showOrgFilter]="false"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
></app-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="page-header d-flex">
|
||||
<h1>
|
||||
{{ "vaultItems" | i18n }}
|
||||
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
||||
<ng-container *ngIf="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">
|
||||
<app-vault-bulk-actions
|
||||
[ciphersComponent]="ciphersComponent"
|
||||
[deleted]="deleted"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm ml-auto"
|
||||
(click)="addCipher()"
|
||||
*ngIf="!deleted"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-callout type="warning" *ngIf="deleted" icon="bwi bwi-exclamation-triangle">
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
<app-org-vault-ciphers
|
||||
(onCipherClicked)="editCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)"
|
||||
(onAddCipher)="addCipher()"
|
||||
(onCollectionsClicked)="editCipherCollections($event)"
|
||||
(onEventsClicked)="viewEvents($event)"
|
||||
(onCloneClicked)="cloneCipher($event)"
|
||||
>
|
||||
</app-org-vault-ciphers>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
@@ -0,0 +1,350 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
import { CipherType } from "jslib-common/enums/cipherType";
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||
|
||||
import { EntityEventsComponent } from "../../../../organizations/manage/entity-events.component";
|
||||
import { AddEditComponent } from "../../../../organizations/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
|
||||
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
|
||||
import { CollectionsComponent } from "../../../../organizations/vault/collections.component";
|
||||
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
|
||||
import { VaultService } from "../../vault.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-vault",
|
||||
templateUrl: "organization-vault.component.html",
|
||||
})
|
||||
export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
|
||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild("collections", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
|
||||
eventsModalRef: ViewContainerRef;
|
||||
|
||||
organization: Organization;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
deleted = false;
|
||||
trashCleanupWarning: string = null;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private router: Router,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private messagingService: MessagingService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private vaultService: VaultService,
|
||||
private cipherService: CipherService,
|
||||
private passwordRepromptService: PasswordRepromptService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
: "trashCleanupWarning"
|
||||
);
|
||||
this.route.parent.params.subscribe(async (params: any) => {
|
||||
this.organization = await this.organizationService.get(params.organizationId);
|
||||
this.vaultFilterComponent.organization = this.organization;
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
|
||||
if (!this.organization.canViewAllCollections) {
|
||||
await this.syncService.fullSync(false);
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.vaultFilterComponent.reloadCollectionsAndFolders(
|
||||
new VaultFilter({
|
||||
selectedOrganizationId: this.organization.id,
|
||||
} as Partial<VaultFilter>)
|
||||
),
|
||||
this.ciphersComponent.refresh(),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders(
|
||||
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
|
||||
);
|
||||
await this.ciphersComponent.reload();
|
||||
|
||||
if (qParams.viewEvents != null) {
|
||||
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
||||
if (cipher.length > 0) {
|
||||
this.viewEvents(cipher[0]);
|
||||
}
|
||||
}
|
||||
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
if (
|
||||
// Handle users with implicit collection access since they use the admin endpoint
|
||||
this.organization.canEditAnyCollection ||
|
||||
(await this.cipherService.get(params.cipherId)) != null
|
||||
) {
|
||||
this.editCipherId(params.cipherId);
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("unknownCipher")
|
||||
);
|
||||
this.router.navigate([], {
|
||||
queryParams: { cipherId: null },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
||||
this.activeFilter = vaultFilter;
|
||||
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
|
||||
this.vaultFilterComponent.searchPlaceholder =
|
||||
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
|
||||
this.go();
|
||||
}
|
||||
|
||||
private buildFilter(): (cipher: CipherView) => boolean {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
|
||||
}
|
||||
if (
|
||||
this.activeFilter.selectedFolder != null &&
|
||||
this.activeFilter.selectedFolderId != "none" &&
|
||||
cipherPassesFilter
|
||||
) {
|
||||
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
|
||||
}
|
||||
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null &&
|
||||
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
|
||||
}
|
||||
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
|
||||
}
|
||||
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
}
|
||||
|
||||
async editCipherAttachments(cipher: CipherView) {
|
||||
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
|
||||
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });
|
||||
return;
|
||||
}
|
||||
|
||||
let madeAttachmentChanges = false;
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
AttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.organization = this.organization;
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
}
|
||||
);
|
||||
|
||||
modal.onClosed.subscribe(async () => {
|
||||
if (madeAttachmentChanges) {
|
||||
await this.ciphersComponent.refresh();
|
||||
}
|
||||
madeAttachmentChanges = false;
|
||||
});
|
||||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
CollectionsComponent,
|
||||
this.collectionsModalRef,
|
||||
(comp) => {
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
comp.collectionIds = cipher.collectionIds;
|
||||
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => !c.readOnly
|
||||
);
|
||||
}
|
||||
comp.organization = this.organization;
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onSavedCollections.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
const component = await this.editCipher(null);
|
||||
component.organizationId = this.organization.id;
|
||||
component.type = this.type;
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => !c.readOnly
|
||||
);
|
||||
}
|
||||
if (this.collectionId != null) {
|
||||
component.collectionIds = [this.collectionId];
|
||||
}
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView) {
|
||||
return this.editCipherId(cipher?.id);
|
||||
}
|
||||
|
||||
async editCipherId(cipherId: string) {
|
||||
const cipher = await this.cipherService.get(cipherId);
|
||||
if (cipher != null && cipher.reprompt != 0) {
|
||||
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
this.go({ cipherId: null });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
AddEditComponent,
|
||||
this.cipherAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.organization = this.organization;
|
||||
comp.cipherId = cipherId;
|
||||
comp.onSavedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
comp.onDeletedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
comp.onRestoredCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
modal.onClosedPromise().then(() => {
|
||||
this.go({ cipherId: null });
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async cloneCipher(cipher: CipherView) {
|
||||
const component = await this.editCipher(cipher);
|
||||
component.cloneMode = true;
|
||||
component.organizationId = this.organization.id;
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => !c.readOnly
|
||||
);
|
||||
}
|
||||
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
|
||||
// in the add-edit componenet
|
||||
component.collectionIds = cipher.collectionIds;
|
||||
}
|
||||
|
||||
async viewEvents(cipher: CipherView) {
|
||||
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
||||
comp.name = cipher.name;
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.entityId = cipher.id;
|
||||
comp.showUser = true;
|
||||
comp.entity = "cipher";
|
||||
});
|
||||
}
|
||||
|
||||
private clearFilters() {
|
||||
this.collectionId = null;
|
||||
this.type = null;
|
||||
this.deleted = false;
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
type: this.type,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
};
|
||||
}
|
||||
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { VaultModule } from "../../vault.module";
|
||||
|
||||
import { OrganizationVaultRoutingModule } from "./organization-vault-routing.module";
|
||||
import { OrganizationVaultComponent } from "./organization-vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultModule, OrganizationVaultRoutingModule],
|
||||
declarations: [OrganizationVaultComponent],
|
||||
exports: [OrganizationVaultComponent],
|
||||
})
|
||||
export class OrganizationVaultModule {}
|
||||
19
apps/web/src/app/modules/vault/vault.module.ts
Normal file
19
apps/web/src/app/modules/vault/vault.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule } from "../loose-components.module";
|
||||
import { SharedModule } from "../shared.module";
|
||||
import { VaultFilterModule } from "../vault-filter/vault-filter.module";
|
||||
|
||||
import { VaultService } from "./vault.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, VaultFilterModule, LooseComponentsModule],
|
||||
exports: [SharedModule, VaultFilterModule, LooseComponentsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultService,
|
||||
useClass: VaultService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultModule {}
|
||||
29
apps/web/src/app/modules/vault/vault.service.ts
Normal file
29
apps/web/src/app/modules/vault/vault.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
|
||||
|
||||
export class VaultService {
|
||||
calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (vaultFilter.status === "trash") {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (vaultFilter.cipherType != null) {
|
||||
return "searchType";
|
||||
}
|
||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (vaultFilter.selectedCollectionId != null) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (vaultFilter.selectedOrganizationId != null) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
if (vaultFilter.myVaultOnly) {
|
||||
return "searchMyVault";
|
||||
}
|
||||
|
||||
return "searchVault";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user