Completed
Push — master ( e88c19...17669b )
by Rain
03:00
created

dev/View/Popup/Contacts.js   F

Complexity

Total Complexity 160
Complexity/F 1.88

Size

Lines of Code 776
Function Count 85

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 0
nc 49152
dl 0
loc 776
rs 2.1818
c 2
b 0
f 0
wmc 160
mnd 5
bc 121
fnc 85
bpm 1.4235
cpm 1.8823
noi 0

How to fix   Complexity   

Complexity

Complex classes like dev/View/Popup/Contacts.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
2
import window from 'window';
3
import _ from '_';
4
import $ from '$';
5
import ko from 'ko';
6
import key from 'key';
7
import Jua from 'Jua';
8
9
import {
10
	SaveSettingsStep, ContactPropertyType, ComposeType,
11
	Capa, Magics, StorageResultType, Notification, KeyState
12
} from 'Common/Enums';
13
14
import {
15
	delegateRunOnDestroy, computedPagenatorHelper,
16
	inArray, trim, windowResizeCallback, createCommand,
17
	isNonEmptyArray, fakeMd5, pInt, isUnd
18
} from 'Common/Utils';
19
20
import {CONTACTS_PER_PAGE} from 'Common/Consts';
21
import {bMobileDevice} from 'Common/Globals';
22
23
import {Selector} from 'Common/Selector';
24
import {exportContactsVcf, exportContactsCsv, uploadContacts} from 'Common/Links';
25
import {i18n, getNotification} from 'Common/Translator';
26
27
import SettingsStore from 'Stores/User/Settings';
28
import ContactStore from 'Stores/User/Contact';
29
30
import Remote from 'Remote/User/Ajax';
31
32
import * as Settings from 'Storage/Settings';
33
34
import {EmailModel} from 'Model/Email';
35
import {ContactModel} from 'Model/Contact';
36
import {ContactPropertyModel} from 'Model/ContactProperty';
37
38
import {getApp} from 'Helper/Apps/User';
39
40
import {view, ViewType, showScreenPopup, hideScreenPopup, routeOn, routeOff} from 'Knoin/Knoin';
41
import {AbstractViewNext} from 'Knoin/AbstractViewNext';
42
43
@view({
44
	name: 'View/Popup/Contacts',
45
	type: ViewType.Popup,
46
	templateID: 'PopupsContacts'
47
})
48
class ContactsPopupView extends AbstractViewNext
49
{
50
	constructor() {
51
		super();
52
53
		const
54
			fFastClearEmptyListHelper = (list) => {
55
				if (list && 0 < list.length) {
56
					this.viewProperties.removeAll(list);
57
					delegateRunOnDestroy(list);
58
				}
59
			};
60
61
		this.bBackToCompose = false;
62
		this.sLastComposeFocusedField = '';
63
64
		this.allowContactsSync = ContactStore.allowContactsSync;
65
		this.enableContactsSync = ContactStore.enableContactsSync;
66
		this.allowExport = !bMobileDevice;
67
68
		this.search = ko.observable('');
69
		this.contactsCount = ko.observable(0);
70
		this.contacts = ContactStore.contacts;
71
72
		this.currentContact = ko.observable(null);
73
74
		this.importUploaderButton = ko.observable(null);
75
76
		this.contactsPage = ko.observable(1);
77
		this.contactsPageCount = ko.computed(() => {
78
			const iPage = window.Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE);
79
			return 0 >= iPage ? 1 : iPage;
80
		});
81
82
		this.contactsPagenator = ko.computed(computedPagenatorHelper(this.contactsPage, this.contactsPageCount));
83
84
		this.emptySelection = ko.observable(true);
85
		this.viewClearSearch = ko.observable(false);
86
87
		this.viewID = ko.observable('');
88
		this.viewReadOnly = ko.observable(false);
89
		this.viewProperties = ko.observableArray([]);
90
91
		this.viewSaveTrigger = ko.observable(SaveSettingsStep.Idle);
92
93
		this.viewPropertiesNames = this.viewProperties.filter(
94
			(property) => -1 < inArray(property.type(), [ContactPropertyType.FirstName, ContactPropertyType.LastName])
95
		);
96
97
		this.viewPropertiesOther = this.viewProperties.filter(
98
			(property) => -1 < inArray(property.type(), [ContactPropertyType.Note])
99
		);
100
101
		this.viewPropertiesOther = ko.computed(() => {
102
			const list = _.filter(this.viewProperties(),
103
				(property) => -1 < inArray(property.type(), [ContactPropertyType.Nick])
104
			);
105
			return _.sortBy(list, (property) => property.type());
106
		});
107
108
		this.viewPropertiesEmails = this.viewProperties.filter(
109
			(property) => ContactPropertyType.Email === property.type()
110
		);
111
112
		this.viewPropertiesWeb = this.viewProperties.filter(
113
			(property) => ContactPropertyType.Web === property.type()
114
		);
115
116
		this.viewHasNonEmptyRequaredProperties = ko.computed(() => {
117
			const
118
				names = this.viewPropertiesNames(),
119
				emails = this.viewPropertiesEmails(),
120
				fFilter = (property) => '' !== trim(property.value());
121
122
			return !!(_.find(names, fFilter) || _.find(emails, fFilter));
123
		});
124
125
		this.viewPropertiesPhones = this.viewProperties.filter(
126
			(property) => ContactPropertyType.Phone === property.type()
127
		);
128
129
		this.viewPropertiesEmailsNonEmpty = this.viewPropertiesNames.filter(
130
			(property) => '' !== trim(property.value())
131
		);
132
133
		this.viewPropertiesEmailsEmptyAndOnFocused = this.viewPropertiesEmails.filter((property) => {
134
			const foc = property.focused();
135
			return '' === trim(property.value()) && !foc;
136
		});
137
138
		this.viewPropertiesPhonesEmptyAndOnFocused = this.viewPropertiesPhones.filter((property) => {
139
			const foc = property.focused();
140
			return '' === trim(property.value()) && !foc;
141
		});
142
143
		this.viewPropertiesWebEmptyAndOnFocused = this.viewPropertiesWeb.filter((property) => {
144
			const foc = property.focused();
145
			return '' === trim(property.value()) && !foc;
146
		});
147
148
		this.viewPropertiesOtherEmptyAndOnFocused = ko.computed(
149
			() => _.filter(this.viewPropertiesOther(), (property) => {
150
				const foc = property.focused();
151
				return '' === trim(property.value()) && !foc;
152
			})
153
		);
154
155
		this.viewPropertiesEmailsEmptyAndOnFocused.subscribe((list) => {
156
			fFastClearEmptyListHelper(list);
157
		});
158
159
		this.viewPropertiesPhonesEmptyAndOnFocused.subscribe((list) => {
160
			fFastClearEmptyListHelper(list);
161
		});
162
163
		this.viewPropertiesWebEmptyAndOnFocused.subscribe((list) => {
164
			fFastClearEmptyListHelper(list);
165
		});
166
167
		this.viewPropertiesOtherEmptyAndOnFocused.subscribe((list) => {
168
			fFastClearEmptyListHelper(list);
169
		});
170
171
		this.viewSaving = ko.observable(false);
172
173
		this.useCheckboxesInList = SettingsStore.useCheckboxesInList;
174
175
		this.search.subscribe(() => {
176
			this.reloadContactList();
177
		});
178
179
		this.contacts.subscribe(windowResizeCallback);
180
		this.viewProperties.subscribe(windowResizeCallback);
181
182
		this.contactsChecked = ko.computed(
183
			() => _.filter(this.contacts(), (item) => item.checked())
184
		);
185
186
		this.contactsCheckedOrSelected = ko.computed(() => {
187
			const
188
				checked = this.contactsChecked(),
189
				selected = this.currentContact();
190
191
			return _.union(checked, selected ? [selected] : []);
192
		});
193
194
		this.contactsCheckedOrSelectedUids = ko.computed(
195
			() => _.map(this.contactsCheckedOrSelected(), (contact) => contact.idContact)
196
		);
197
198
		this.selector = new Selector(
199
			this.contacts, this.currentContact, null,
200
			'.e-contact-item .actionHandle', '.e-contact-item.selected',
201
			'.e-contact-item .checkboxItem', '.e-contact-item.focused');
202
203
		this.selector.on('onItemSelect', (contact) => {
204
			this.populateViewContact(contact ? contact : null);
205
			if (!contact)
206
			{
207
				this.emptySelection(true);
208
			}
209
		});
210
211
		this.selector.on('onItemGetUid', (contact) => (contact ? contact.generateUid() : ''));
212
213
		this.newCommand = createCommand(() => {
214
			this.populateViewContact(null);
215
			this.currentContact(null);
216
		});
217
218
		this.deleteCommand = createCommand(() => {
219
			this.deleteSelectedContacts();
220
			this.emptySelection(true);
221
		}, () => 0 < this.contactsCheckedOrSelected().length);
222
223
		this.newMessageCommand = createCommand(() => {
224
225
			if (!Settings.capa(Capa.Composer))
226
			{
227
				return false;
228
			}
229
230
			let
231
				aE = [],
232
				toEmails = null,
233
				ccEmails = null,
234
				bccEmails = null;
235
236
			const aC = this.contactsCheckedOrSelected();
237
			if (isNonEmptyArray(aC))
238
			{
239
				aE = _.map(aC, (oItem) => {
240
					if (oItem)
241
					{
242
						const
243
							data = oItem.getNameAndEmailHelper(),
244
							email = data ? new EmailModel(data[0], data[1]) : null;
245
246
						if (email && email.validate())
247
						{
248
							return email;
249
						}
250
					}
251
252
					return null;
253
				});
254
255
				aE = _.compact(aE);
256
			}
257
258
			if (isNonEmptyArray(aE))
259
			{
260
				this.bBackToCompose = false;
261
262
				hideScreenPopup(require('View/Popup/Contacts'));
263
264
				switch (this.sLastComposeFocusedField)
265
				{
266
					case 'cc':
267
						ccEmails = aE;
268
						break;
269
					case 'bcc':
270
						bccEmails = aE;
271
						break;
272
					case 'to':
273
					default:
274
						toEmails = aE;
275
						break;
276
				}
277
278
				this.sLastComposeFocusedField = '';
279
280
				_.delay(() => {
281
					showScreenPopup(require('View/Popup/Compose'), [ComposeType.Empty, null, toEmails, ccEmails, bccEmails]);
282
				}, Magics.Time200ms);
283
			}
284
285
			return true;
286
287
		}, () => 0 < this.contactsCheckedOrSelected().length);
288
289
		this.clearCommand = createCommand(() => {
290
			this.search('');
291
		});
292
293
		this.saveCommand = createCommand(() => {
294
295
			this.viewSaving(true);
296
			this.viewSaveTrigger(SaveSettingsStep.Animate);
297
298
			const
299
				requestUid = fakeMd5(),
300
				properties = [];
301
302
			_.each(this.viewProperties(), (oItem) => {
303
				if (oItem.type() && '' !== trim(oItem.value()))
304
				{
305
					properties.push([oItem.type(), oItem.value(), oItem.typeStr()]);
306
				}
307
			});
308
309
			Remote.contactSave((sResult, oData) => {
310
311
				let res = false;
312
				this.viewSaving(false);
313
314
				if (StorageResultType.Success === sResult && oData && oData.Result &&
315
					oData.Result.RequestUid === requestUid && 0 < pInt(oData.Result.ResultID))
316
				{
317
					if ('' === this.viewID())
318
					{
319
						this.viewID(pInt(oData.Result.ResultID));
320
					}
321
322
					this.reloadContactList();
323
					res = true;
324
				}
325
326
				_.delay(() => {
327
					this.viewSaveTrigger(res ? SaveSettingsStep.TrueResult : SaveSettingsStep.FalseResult);
328
				}, Magics.Time350ms);
329
330
				if (res)
331
				{
332
					this.watchDirty(false);
333
334
					_.delay(() => {
335
						this.viewSaveTrigger(SaveSettingsStep.Idle);
336
					}, Magics.Time1s);
337
				}
338
339
			}, requestUid, this.viewID(), properties);
340
341
		}, () => {
342
			const
343
				bV = this.viewHasNonEmptyRequaredProperties(),
344
				bReadOnly = this.viewReadOnly();
345
			return !this.viewSaving() && bV && !bReadOnly;
346
		});
347
348
		this.syncCommand = createCommand(() => {
349
350
			getApp().contactsSync((result, data) => {
351
				if (StorageResultType.Success !== result || !data || !data.Result)
352
				{
353
					window.alert(getNotification(
354
						data && data.ErrorCode ? data.ErrorCode : Notification.ContactsSyncError));
355
				}
356
357
				this.reloadContactList(true);
358
			});
359
360
		}, () => !this.contacts.syncing() && !this.contacts.importing());
361
362
		this.bDropPageAfterDelete = false;
363
364
		this.watchDirty = ko.observable(false);
365
		this.watchHash = ko.observable(false);
366
367
		this.viewHash = ko.computed(() => '' + _.map(this.viewProperties(), (oItem) => oItem.value()).join(''));
368
369
	//	this.saveCommandDebounce = _.debounce(_.bind(this.saveCommand, this), 1000);
370
371
		this.viewHash.subscribe(() => {
372
			if (this.watchHash() && !this.viewReadOnly() && !this.watchDirty())
373
			{
374
				this.watchDirty(true);
375
			}
376
		});
377
378
		this.sDefaultKeyScope = KeyState.ContactList;
379
	}
380
381
	getPropertyPlaceholder(type) {
382
		let result = '';
383
		switch (type)
384
		{
385
			case ContactPropertyType.LastName:
386
				result = 'CONTACTS/PLACEHOLDER_ENTER_LAST_NAME';
387
				break;
388
			case ContactPropertyType.FirstName:
389
				result = 'CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME';
390
				break;
391
			case ContactPropertyType.Nick:
392
				result = 'CONTACTS/PLACEHOLDER_ENTER_NICK_NAME';
393
				break;
394
			// no default
395
		}
396
397
		return result;
398
	}
399
400
	addNewProperty(type, typeStr) {
401
		this.viewProperties.push(new ContactPropertyModel(type, typeStr || '', '', true, this.getPropertyPlaceholder(type)));
402
	}
403
404
	addNewOrFocusProperty(type, typeStr) {
405
		const item = _.find(this.viewProperties(), (prop) => type === prop.type());
406
		if (item)
407
		{
408
			item.focused(true);
409
		}
410
		else
411
		{
412
			this.addNewProperty(type, typeStr);
413
		}
414
	}
415
416
	addNewEmail() {
417
		this.addNewProperty(ContactPropertyType.Email, 'Home');
418
	}
419
420
	addNewPhone() {
421
		this.addNewProperty(ContactPropertyType.Phone, 'Mobile');
422
	}
423
424
	addNewWeb() {
425
		this.addNewProperty(ContactPropertyType.Web);
426
	}
427
428
	addNewNickname() {
429
		this.addNewOrFocusProperty(ContactPropertyType.Nick);
430
	}
431
432
	addNewNotes() {
433
		this.addNewOrFocusProperty(ContactPropertyType.Note);
434
	}
435
436
	addNewBirthday() {
437
		this.addNewOrFocusProperty(ContactPropertyType.Birthday);
438
	}
439
440
	exportVcf() {
441
		getApp().download(exportContactsVcf());
442
	}
443
444
	exportCsv() {
445
		getApp().download(exportContactsCsv());
446
	}
447
448
	initUploader() {
449
		if (this.importUploaderButton())
450
		{
451
			const
452
				j = new Jua({
453
					'action': uploadContacts(),
454
					'name': 'uploader',
455
					'queueSize': 1,
456
					'multipleSizeLimit': 1,
457
					'disableDragAndDrop': true,
458
					'disableMultiple': true,
459
					'disableDocumentDropPrevent': true,
460
					'clickElement': this.importUploaderButton()
461
				});
462
463
			if (j)
464
			{
465
				j
466
					.on('onStart', () => {
467
						this.contacts.importing(true);
468
					})
469
					.on('onComplete', (id, result, data) => {
470
						this.contacts.importing(false);
471
						this.reloadContactList();
472
						if (!id || !result || !data || !data.Result)
473
						{
474
							window.alert(i18n('CONTACTS/ERROR_IMPORT_FILE'));
475
						}
476
					});
477
			}
478
		}
479
	}
480
481
	removeCheckedOrSelectedContactsFromList() {
482
		const
483
			koContacts = this.contacts,
484
			contacts = this.contactsCheckedOrSelected();
485
486
		let
487
			currentContact = this.currentContact(),
488
			count = this.contacts().length;
489
490
		if (0 < contacts.length)
491
		{
492
			_.each(contacts, (contact) => {
493
494
				if (currentContact && currentContact.idContact === contact.idContact)
495
				{
496
					currentContact = null;
497
					this.currentContact(null);
498
				}
499
500
				contact.deleted(true);
501
				count -= 1;
502
			});
503
504
			if (0 >= count)
505
			{
506
				this.bDropPageAfterDelete = true;
507
			}
508
509
			_.delay(() => {
510
				_.each(contacts, (contact) => {
511
					koContacts.remove(contact);
512
					delegateRunOnDestroy(contact);
513
				});
514
			}, Magics.Time500ms);
515
		}
516
	}
517
518
	deleteSelectedContacts() {
519
		if (0 < this.contactsCheckedOrSelected().length)
520
		{
521
			Remote.contactsDelete(
522
				_.bind(this.deleteResponse, this),
523
				this.contactsCheckedOrSelectedUids()
524
			);
525
526
			this.removeCheckedOrSelectedContactsFromList();
527
		}
528
	}
529
530
	/**
531
	 * @param {string} sResult
532
	 * @param {AjaxJsonDefaultResponse} oData
533
	 */
534
	deleteResponse(sResult, oData) {
535
		if (Magics.Time500ms < (StorageResultType.Success === sResult && oData && oData.Time ? pInt(oData.Time) : 0))
536
		{
537
			this.reloadContactList(this.bDropPageAfterDelete);
538
		}
539
		else
540
		{
541
			_.delay(() => {
542
				this.reloadContactList(this.bDropPageAfterDelete);
543
			}, Magics.Time500ms);
544
		}
545
	}
546
547
	removeProperty(oProp) {
548
		this.viewProperties.remove(oProp);
549
		delegateRunOnDestroy(oProp);
550
	}
551
552
	/**
553
	 * @param {?ContactModel} contact
554
	 */
555
	populateViewContact(contact) {
556
		let
557
			id = '',
558
			lastName = '',
559
			firstName = '';
560
		const
561
			list = [];
562
563
		this.watchHash(false);
564
565
		this.emptySelection(false);
566
		this.viewReadOnly(false);
567
568
		if (contact)
569
		{
570
			id = contact.idContact;
571
			if (isNonEmptyArray(contact.properties))
572
			{
573
				_.each(contact.properties, (property) => {
574
					if (property && property[0])
575
					{
576
						if (ContactPropertyType.LastName === property[0])
577
						{
578
							lastName = property[1];
579
						}
580
						else if (ContactPropertyType.FirstName === property[0])
581
						{
582
							firstName = property[1];
583
						}
584
						else
585
						{
586
							list.push(new ContactPropertyModel(property[0], property[2] || '', property[1]));
587
						}
588
					}
589
				});
590
			}
591
592
			this.viewReadOnly(!!contact.readOnly);
593
		}
594
595
		list.unshift(new ContactPropertyModel(ContactPropertyType.LastName, '', lastName, false,
596
			this.getPropertyPlaceholder(ContactPropertyType.LastName)));
597
598
		list.unshift(new ContactPropertyModel(ContactPropertyType.FirstName, '', firstName, !contact,
599
			this.getPropertyPlaceholder(ContactPropertyType.FirstName)));
600
601
		this.viewID(id);
602
603
		delegateRunOnDestroy(this.viewProperties());
604
605
		this.viewProperties([]);
606
		this.viewProperties(list);
607
608
		this.watchDirty(false);
609
		this.watchHash(true);
610
	}
611
612
	/**
613
	 * @param {boolean=} dropPagePosition = false
614
	 */
615
	reloadContactList(dropPagePosition = false) {
616
617
		let offset = (this.contactsPage() - 1) * CONTACTS_PER_PAGE;
618
619
		this.bDropPageAfterDelete = false;
620
621
		if (dropPagePosition)
622
		{
623
			this.contactsPage(1);
624
			offset = 0;
625
		}
626
627
		this.contacts.loading(true);
628
		Remote.contacts((result, data) => {
629
630
			let
631
				count = 0,
632
				list = [];
633
634
			if (StorageResultType.Success === result && data && data.Result && data.Result.List)
635
			{
636
				if (isNonEmptyArray(data.Result.List))
637
				{
638
					list = _.map(data.Result.List, (item) => {
639
						const contact = new ContactModel();
640
						return contact.parse(item) ? contact : null;
641
					});
642
643
					list = _.compact(list);
644
645
					count = pInt(data.Result.Count);
646
					count = 0 < count ? count : 0;
647
				}
648
			}
649
650
			this.contactsCount(count);
651
652
			delegateRunOnDestroy(this.contacts());
653
			this.contacts(list);
654
655
			this.contacts.loading(false);
656
			this.viewClearSearch('' !== this.search());
657
658
		}, offset, CONTACTS_PER_PAGE, this.search());
659
	}
660
661
	onBuild(dom) {
662
		this.oContentVisible = $('.b-list-content', dom);
663
		this.oContentScrollable = $('.content', this.oContentVisible);
664
665
		this.selector.init(this.oContentVisible, this.oContentScrollable, KeyState.ContactList);
666
667
		key('delete', KeyState.ContactList, () => {
668
			this.deleteCommand();
669
			return false;
670
		});
671
672
		key('c, w', KeyState.ContactList, () => {
673
			this.newMessageCommand();
674
			return false;
675
		});
676
677
		const self = this;
678
679
		dom
680
			.on('click', '.e-pagenator .e-page', function() { // eslint-disable-line prefer-arrow-callback
681
				const page = ko.dataFor(this); // eslint-disable-line no-invalid-this
682
				if (page)
683
				{
684
					self.contactsPage(pInt(page.value));
685
					self.reloadContactList();
686
				}
687
			});
688
689
		this.initUploader();
690
	}
691
692
	onShow(bBackToCompose, sLastComposeFocusedField) {
693
		this.bBackToCompose = isUnd(bBackToCompose) ? false : !!bBackToCompose;
694
		this.sLastComposeFocusedField = isUnd(sLastComposeFocusedField) ? '' : sLastComposeFocusedField;
695
696
		routeOff();
697
		this.reloadContactList(true);
698
	}
699
700
	onHide() {
701
		routeOn();
702
703
		this.currentContact(null);
704
		this.emptySelection(true);
705
		this.search('');
706
		this.contactsCount(0);
707
708
		delegateRunOnDestroy(this.contacts());
709
		this.contacts([]);
710
711
		this.sLastComposeFocusedField = '';
712
713
		if (this.bBackToCompose)
714
		{
715
			this.bBackToCompose = false;
716
717
			if (Settings.capa(Capa.Composer))
718
			{
719
				showScreenPopup(require('View/Popup/Compose'));
720
			}
721
		}
722
	}
723
}
724
725
module.exports = ContactsPopupView;
726