bitwarden-estensione-browser/src/app/vault/vault.component.ts

548 lines
20 KiB
TypeScript
Raw Normal View History

import { remote } from 'electron';
2018-02-08 16:37:54 +01:00
import { Location } from '@angular/common';
2018-01-16 22:12:26 +01:00
import {
2018-02-08 21:58:47 +01:00
ChangeDetectorRef,
2018-01-16 22:12:26 +01:00
Component,
2018-01-29 15:33:43 +01:00
ComponentFactoryResolver,
2018-02-08 21:58:47 +01:00
NgZone,
2018-02-10 05:25:18 +01:00
OnDestroy,
2018-01-16 22:12:26 +01:00
OnInit,
2018-01-26 21:44:02 +01:00
ViewChild,
2018-01-29 15:33:43 +01:00
ViewContainerRef,
2018-01-16 22:12:26 +01:00
} from '@angular/core';
2018-01-25 17:21:08 +01:00
import {
ActivatedRoute,
Router,
} from '@angular/router';
2018-02-09 19:47:59 +01:00
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
2018-04-06 21:33:53 +02:00
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
2018-02-08 21:58:47 +01:00
2018-04-25 05:23:10 +02:00
import { ModalComponent } from 'jslib/angular/components/modal.component';
2018-01-29 22:33:38 +01:00
import { AddEditComponent } from './add-edit.component';
2018-02-08 16:37:54 +01:00
import { AttachmentsComponent } from './attachments.component';
2018-01-26 21:44:02 +01:00
import { CiphersComponent } from './ciphers.component';
2018-08-02 05:21:32 +02:00
import { ExportComponent } from './export.component';
2018-01-30 21:40:06 +01:00
import { FolderAddEditComponent } from './folder-add-edit.component';
2018-01-27 14:52:39 +01:00
import { GroupingsComponent } from './groupings.component';
2018-01-29 15:33:43 +01:00
import { PasswordGeneratorComponent } from './password-generator.component';
import { PasswordHistoryComponent } from './password-history.component';
2018-01-27 05:32:03 +01:00
import { CipherType } from 'jslib/enums/cipherType';
2018-01-24 18:20:01 +01:00
import { CipherView } from 'jslib/models/view/cipherView';
2018-01-27 05:32:03 +01:00
import { FolderView } from 'jslib/models/view/folderView';
2018-01-24 18:20:01 +01:00
2018-01-30 03:54:39 +01:00
import { I18nService } from 'jslib/abstractions/i18n.service';
2018-02-09 21:49:00 +01:00
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
2018-02-09 17:18:37 +01:00
import { SyncService } from 'jslib/abstractions/sync.service';
2018-01-30 03:54:39 +01:00
2018-02-09 21:49:00 +01:00
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
2018-02-10 21:22:07 +01:00
const BroadcasterSubscriptionId = 'VaultComponent';
2018-02-09 21:49:00 +01:00
2018-01-16 22:12:26 +01:00
@Component({
selector: 'app-vault',
templateUrl: 'vault.component.html',
2018-01-16 22:12:26 +01:00
})
2018-02-10 05:25:18 +01:00
export class VaultComponent implements OnInit, OnDestroy {
2018-01-29 22:33:38 +01:00
@ViewChild(AddEditComponent) addEditComponent: AddEditComponent;
2018-01-26 21:44:02 +01:00
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
2018-01-27 14:52:39 +01:00
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
2018-02-09 18:12:41 +01:00
@ViewChild('passwordGenerator', { read: ViewContainerRef }) passwordGeneratorModalRef: ViewContainerRef;
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
@ViewChild('folderAddEdit', { read: ViewContainerRef }) folderAddEditModalRef: ViewContainerRef;
@ViewChild('passwordHistory', { read: ViewContainerRef }) passwordHistoryModalRef: ViewContainerRef;
2018-08-02 05:21:32 +02:00
@ViewChild('exportVault', { read: ViewContainerRef }) exportVaultModalRef: ViewContainerRef;
2018-01-26 21:44:02 +01:00
2018-01-25 17:21:08 +01:00
action: string;
2018-01-27 05:56:43 +01:00
cipherId: string = null;
favorites: boolean = false;
type: CipherType = null;
folderId: string = null;
collectionId: string = null;
2018-02-08 21:58:47 +01:00
addType: CipherType = null;
2018-02-09 18:36:33 +01:00
private modal: ModalComponent = null;
2018-02-09 18:12:41 +01:00
2018-01-29 15:33:43 +01:00
constructor(private route: ActivatedRoute, private router: Router, private location: Location,
2018-02-08 21:58:47 +01:00
private componentFactoryResolver: ComponentFactoryResolver, private i18nService: I18nService,
private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef,
2018-02-09 19:47:59 +01:00
private ngZone: NgZone, private syncService: SyncService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
2018-02-10 21:22:07 +01:00
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
2018-02-08 21:58:47 +01:00
this.ngZone.run(async () => {
let detectChanges = true;
switch (message.command) {
case 'newLogin':
this.addCipher(CipherType.Login);
break;
case 'newCard':
this.addCipher(CipherType.Card);
break;
case 'newIdentity':
this.addCipher(CipherType.Identity);
break;
case 'newSecureNote':
this.addCipher(CipherType.SecureNote);
break;
case 'newFolder':
await this.addFolder();
break;
2018-02-09 18:12:41 +01:00
case 'focusSearch':
(document.querySelector('#search') as HTMLInputElement).select();
2018-02-09 18:12:41 +01:00
detectChanges = false;
break;
case 'openPasswordGenerator':
await this.openPasswordGenerator(false);
break;
2018-08-02 05:21:32 +02:00
case 'exportVault':
await this.openExportVault();
break;
2018-02-09 19:47:59 +01:00
case 'syncVault':
try {
await this.syncService.fullSync(true);
this.toasterService.popAsync('success', null, this.i18nService.t('syncingComplete'));
this.analytics.eventTrack.next({ action: 'Synced Full' });
} catch {
this.toasterService.popAsync('error', null, this.i18nService.t('syncingFailed'));
}
break;
2018-02-09 21:49:00 +01:00
case 'checkSyncVault':
try {
const lastSync = await this.syncService.getLastSync();
let lastSyncAgo = SyncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= SyncInterval) {
await this.syncService.fullSync(false);
}
} catch { }
this.messagingService.send('scheduleNextSync');
break;
2018-02-09 19:47:59 +01:00
case 'syncCompleted':
2018-02-09 21:49:00 +01:00
if (message.successfully) {
await this.load();
}
2018-02-09 19:47:59 +01:00
break;
2018-02-12 21:06:39 +01:00
case 'refreshCiphers':
this.ciphersComponent.refresh();
break;
2018-02-08 21:58:47 +01:00
default:
detectChanges = false;
break;
}
if (detectChanges) {
this.changeDetectorRef.detectChanges();
}
});
});
2018-02-09 19:47:59 +01:00
if (!this.syncService.syncInProgress) {
await this.load();
2018-02-09 17:18:37 +01:00
}
2018-02-09 19:47:59 +01:00
}
2018-02-09 17:18:37 +01:00
2018-02-10 05:25:18 +01:00
ngOnDestroy() {
2018-02-10 21:22:07 +01:00
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
2018-02-10 05:25:18 +01:00
}
2018-02-09 19:47:59 +01:00
async load() {
2018-01-27 05:56:43 +01:00
this.route.queryParams.subscribe(async (params) => {
2018-02-09 19:47:59 +01:00
await this.groupingsComponent.load();
2018-01-27 05:56:43 +01:00
2018-02-09 19:47:59 +01:00
if (params == null) {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.load();
return;
}
2018-02-09 18:12:41 +01:00
2018-02-09 19:47:59 +01:00
if (params.cipherId) {
const cipherView = new CipherView();
cipherView.id = params.cipherId;
if (params.action === 'edit') {
this.editCipher(cipherView);
} else {
this.viewCipher(cipherView);
}
} else if (params.action === 'add') {
this.addCipher();
}
2018-02-09 17:18:37 +01:00
2018-02-09 19:47:59 +01:00
if (params.favorites) {
this.groupingsComponent.selectedFavorites = true;
await this.filterFavorites();
} else if (params.type) {
const t = parseInt(params.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t);
} else if (params.folderId) {
this.groupingsComponent.selectedFolder = true;
this.groupingsComponent.selectedFolderId = params.folderId;
await this.filterFolder(params.folderId);
} else if (params.collectionId) {
this.groupingsComponent.selectedCollectionId = params.collectionId;
await this.filterCollection(params.collectionId);
2018-01-27 05:56:43 +01:00
} else {
2018-02-09 19:47:59 +01:00
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.load();
2018-01-27 05:56:43 +01:00
}
2018-02-09 19:47:59 +01:00
});
2018-01-16 22:12:26 +01:00
}
2018-01-24 06:06:05 +01:00
2018-01-26 20:56:54 +01:00
viewCipher(cipher: CipherView) {
if (this.action === 'view' && this.cipherId === cipher.id) {
2018-01-25 17:28:52 +01:00
return;
}
2018-01-26 20:56:54 +01:00
this.cipherId = cipher.id;
2018-01-25 17:21:08 +01:00
this.action = 'view';
2018-01-27 14:52:39 +01:00
this.go();
2018-01-24 06:06:05 +01:00
}
2018-01-24 23:41:57 +01:00
viewCipherMenu(cipher: CipherView) {
const menu = new remote.Menu();
menu.append(new remote.MenuItem({
label: this.i18nService.t('view'),
2018-02-23 22:31:52 +01:00
click: () => this.functionWithChangeDetection(() => {
this.viewCipher(cipher);
}),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('edit'),
2018-02-23 22:31:52 +01:00
click: () => this.functionWithChangeDetection(() => {
this.editCipher(cipher);
}),
}));
switch (cipher.type) {
case CipherType.Login:
if (cipher.login.canLaunch || cipher.login.username != null || cipher.login.password != null) {
menu.append(new remote.MenuItem({ type: 'separator' }));
}
if (cipher.login.canLaunch) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('launch'),
click: () => this.platformUtilsService.launchUri(cipher.login.uri),
}));
}
if (cipher.login.username != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyUsername'),
2018-02-23 22:31:52 +01:00
click: () => this.copyValue(cipher.login.username, 'username'),
}));
}
if (cipher.login.password != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyPassword'),
2018-02-23 22:31:52 +01:00
click: () => this.copyValue(cipher.login.password, 'password'),
}));
}
break;
case CipherType.Card:
if (cipher.card.number != null || cipher.card.code != null) {
menu.append(new remote.MenuItem({ type: 'separator' }));
}
if (cipher.card.number != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyNumber'),
2018-02-23 22:31:52 +01:00
click: () => this.copyValue(cipher.card.number, 'number'),
}));
}
if (cipher.card.code != null) {
menu.append(new remote.MenuItem({
label: this.i18nService.t('copySecurityCode'),
2018-02-23 22:31:52 +01:00
click: () => this.copyValue(cipher.card.code, 'securityCode'),
}));
}
break;
default:
break;
}
2018-05-31 14:10:02 +02:00
menu.popup({ window: remote.getCurrentWindow() });
}
2018-01-26 20:56:54 +01:00
editCipher(cipher: CipherView) {
if (this.action === 'edit' && this.cipherId === cipher.id) {
2018-01-25 17:28:52 +01:00
return;
}
2018-01-26 20:56:54 +01:00
this.cipherId = cipher.id;
2018-01-25 17:21:08 +01:00
this.action = 'edit';
2018-01-27 14:52:39 +01:00
this.go();
2018-01-24 23:41:57 +01:00
}
2018-02-08 21:58:47 +01:00
addCipher(type: CipherType = null) {
2018-01-25 17:28:52 +01:00
if (this.action === 'add') {
return;
}
2018-02-08 21:58:47 +01:00
this.addType = type;
2018-01-25 17:21:08 +01:00
this.action = 'add';
2018-01-26 20:56:54 +01:00
this.cipherId = null;
2018-01-27 14:52:39 +01:00
this.go();
2018-01-26 20:56:54 +01:00
}
addCipherOptions() {
const menu = new remote.Menu();
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeLogin'),
click: () => this.addCipherWithChangeDetection(CipherType.Login),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeCard'),
click: () => this.addCipherWithChangeDetection(CipherType.Card),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeIdentity'),
click: () => this.addCipherWithChangeDetection(CipherType.Identity),
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('typeSecureNote'),
click: () => this.addCipherWithChangeDetection(CipherType.SecureNote),
}));
2018-05-31 14:10:02 +02:00
menu.popup({ window: remote.getCurrentWindow() });
}
async savedCipher(cipher: CipherView) {
2018-01-26 20:56:54 +01:00
this.cipherId = cipher.id;
this.action = 'view';
2018-01-27 14:52:39 +01:00
this.go();
await this.ciphersComponent.refresh();
2018-01-26 20:56:54 +01:00
}
async deletedCipher(cipher: CipherView) {
2018-01-26 21:44:02 +01:00
this.cipherId = null;
this.action = null;
2018-01-27 14:52:39 +01:00
this.go();
await this.ciphersComponent.refresh();
2018-01-26 20:56:54 +01:00
}
editCipherAttachments(cipher: CipherView) {
2018-02-09 18:36:33 +01:00
if (this.modal != null) {
this.modal.close();
2018-02-09 18:12:41 +01:00
}
2018-01-30 05:19:55 +01:00
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
2018-02-09 18:36:33 +01:00
this.modal = this.attachmentsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AttachmentsComponent>(AttachmentsComponent, this.attachmentsModalRef);
2018-01-26 20:56:54 +01:00
2018-01-30 05:19:55 +01:00
childComponent.cipherId = cipher.id;
2018-06-09 19:29:23 +02:00
let madeAttachmentChanges = false;
childComponent.onUploadedAttachment.subscribe(() => madeAttachmentChanges = true);
childComponent.onDeletedAttachment.subscribe(() => madeAttachmentChanges = true);
2018-02-09 18:12:41 +01:00
2018-06-09 19:29:23 +02:00
this.modal.onClosed.subscribe(async () => {
2018-02-09 18:36:33 +01:00
this.modal = null;
2018-06-09 19:29:23 +02:00
if (madeAttachmentChanges) {
await this.ciphersComponent.refresh();
}
madeAttachmentChanges = false;
2018-02-09 18:12:41 +01:00
});
2018-01-26 20:56:54 +01:00
}
viewCipherPasswordHistory(cipher: CipherView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.passwordHistoryModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<PasswordHistoryComponent>(PasswordHistoryComponent,
this.passwordHistoryModalRef);
childComponent.cipherId = cipher.id;
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
2018-01-26 20:56:54 +01:00
cancelledAddEdit(cipher: CipherView) {
this.cipherId = cipher.id;
this.action = this.cipherId != null ? 'view' : null;
2018-01-27 14:52:39 +01:00
this.go();
2018-01-25 17:21:08 +01:00
}
2018-01-27 05:32:03 +01:00
async clearGroupingFilters() {
2018-01-30 03:54:39 +01:00
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchVault');
2018-01-27 05:32:03 +01:00
await this.ciphersComponent.load();
2018-01-27 05:56:43 +01:00
this.clearFilters();
this.go();
2018-01-27 05:32:03 +01:00
}
async filterFavorites() {
2018-01-30 03:54:39 +01:00
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchFavorites');
2018-01-27 05:32:03 +01:00
await this.ciphersComponent.load((c) => c.favorite);
2018-01-27 05:56:43 +01:00
this.clearFilters();
this.favorites = true;
this.go();
2018-01-27 05:32:03 +01:00
}
async filterCipherType(type: CipherType) {
2018-01-30 03:54:39 +01:00
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchType');
2018-01-27 05:32:03 +01:00
await this.ciphersComponent.load((c) => c.type === type);
2018-01-27 05:56:43 +01:00
this.clearFilters();
this.type = type;
this.go();
}
async filterFolder(folderId: string) {
folderId = folderId === 'none' ? null : folderId;
2018-01-30 03:54:39 +01:00
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchFolder');
2018-01-27 05:56:43 +01:00
await this.ciphersComponent.load((c) => c.folderId === folderId);
this.clearFilters();
this.folderId = folderId == null ? 'none' : folderId;
this.go();
2018-01-27 05:32:03 +01:00
}
2018-01-27 05:56:43 +01:00
async filterCollection(collectionId: string) {
2018-01-30 03:54:39 +01:00
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchCollection');
2018-09-11 14:45:29 +02:00
await this.ciphersComponent.load((c) => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1);
2018-01-27 05:56:43 +01:00
this.clearFilters();
this.collectionId = collectionId;
this.go();
2018-01-27 05:32:03 +01:00
}
2018-02-09 18:12:41 +01:00
async openPasswordGenerator(showSelect: boolean) {
2018-02-09 18:36:33 +01:00
if (this.modal != null) {
this.modal.close();
2018-02-09 18:12:41 +01:00
}
2018-01-30 05:19:55 +01:00
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
2018-02-09 18:36:33 +01:00
this.modal = this.passwordGeneratorModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<PasswordGeneratorComponent>(PasswordGeneratorComponent,
2018-02-09 18:12:41 +01:00
this.passwordGeneratorModalRef);
2018-01-29 22:33:38 +01:00
2018-02-09 18:12:41 +01:00
childComponent.showSelect = showSelect;
2018-01-29 22:33:38 +01:00
childComponent.onSelected.subscribe((password: string) => {
2018-02-09 18:36:33 +01:00
this.modal.close();
2018-01-29 22:33:38 +01:00
if (this.addEditComponent != null && this.addEditComponent.cipher != null &&
this.addEditComponent.cipher.login != null) {
this.addEditComponent.cipher.login.password = password;
}
});
2018-02-09 18:12:41 +01:00
2018-02-09 18:36:33 +01:00
this.modal.onClosed.subscribe(() => {
2018-08-02 05:21:32 +02:00
this.modal = null;
});
}
async openExportVault() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.exportVaultModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ExportComponent>(ExportComponent, this.exportVaultModalRef);
childComponent.onSaved.subscribe(() => {
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
2018-02-09 18:36:33 +01:00
this.modal = null;
2018-02-09 18:12:41 +01:00
});
2018-01-29 15:33:43 +01:00
}
2018-01-30 21:40:06 +01:00
async addFolder() {
2018-02-09 18:36:33 +01:00
if (this.modal != null) {
this.modal.close();
}
2018-01-30 21:40:06 +01:00
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
2018-02-09 18:36:33 +01:00
this.modal = this.folderAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<FolderAddEditComponent>(
2018-02-09 18:12:41 +01:00
FolderAddEditComponent, this.folderAddEditModalRef);
2018-01-30 21:40:06 +01:00
childComponent.folderId = null;
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
2018-02-09 18:36:33 +01:00
this.modal.close();
2018-01-30 21:40:06 +01:00
await this.groupingsComponent.loadFolders();
});
2018-02-09 18:12:41 +01:00
2018-02-09 18:36:33 +01:00
this.modal.onClosed.subscribe(() => {
this.modal = null;
2018-02-09 18:12:41 +01:00
});
2018-01-30 21:40:06 +01:00
}
async editFolder(folderId: string) {
2018-02-09 18:36:33 +01:00
if (this.modal != null) {
this.modal.close();
}
2018-01-30 21:40:06 +01:00
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
2018-02-09 18:36:33 +01:00
this.modal = this.folderAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<FolderAddEditComponent>(
2018-02-09 18:12:41 +01:00
FolderAddEditComponent, this.folderAddEditModalRef);
2018-01-30 21:40:06 +01:00
childComponent.folderId = folderId;
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
2018-02-09 18:36:33 +01:00
this.modal.close();
2018-01-30 21:40:06 +01:00
await this.groupingsComponent.loadFolders();
});
childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => {
2018-02-09 18:36:33 +01:00
this.modal.close();
2018-01-30 21:40:06 +01:00
await this.groupingsComponent.loadFolders();
});
2018-02-09 18:12:41 +01:00
2018-02-09 18:36:33 +01:00
this.modal.onClosed.subscribe(() => {
this.modal = null;
2018-02-09 18:12:41 +01:00
});
2018-01-30 21:40:06 +01:00
}
2018-01-27 05:56:43 +01:00
private clearFilters() {
this.folderId = null;
this.collectionId = null;
this.favorites = false;
this.type = null;
2018-01-27 05:32:03 +01:00
}
2018-01-27 05:56:43 +01:00
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
action: this.action,
cipherId: this.cipherId,
favorites: this.favorites ? true : null,
type: this.type,
folderId: this.folderId,
collectionId: this.collectionId,
};
}
2018-01-25 17:21:08 +01:00
const url = this.router.createUrlTree(['vault'], { queryParams: queryParams }).toString();
this.location.go(url);
2018-01-24 23:41:57 +01:00
}
private addCipherWithChangeDetection(type: CipherType = null) {
2018-02-23 22:31:52 +01:00
this.functionWithChangeDetection(() => this.addCipher(type));
}
private copyValue(value: string, labelI18nKey: string) {
this.functionWithChangeDetection(() => {
this.platformUtilsService.copyToClipboard(value);
this.toasterService.popAsync('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(labelI18nKey)));
});
}
private functionWithChangeDetection(func: Function) {
this.ngZone.run(async () => {
2018-02-23 22:31:52 +01:00
func();
this.changeDetectorRef.detectChanges();
});
}
2018-01-16 22:12:26 +01:00
}