src/Oro/Bundle/NavigationBundle/Resources/public/js/app/views/page-state-view.js   F
last analyzed

Complexity

Total Complexity 73
Complexity/F 1.83

Size

Lines of Code 492
Function Count 40

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
wmc 73
nc 122880
mnd 2
bc 65
fnc 40
dl 0
loc 492
rs 3.9761
bpm 1.625
cpm 1.825
noi 3
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
B page-state-view.js ➔ define 0 482 1

How to fix   Complexity   

Complexity

Complex classes like src/Oro/Bundle/NavigationBundle/Resources/public/js/app/views/page-state-view.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
define([
2
    'jquery',
3
    'underscore',
4
    'routing',
5
    'orotranslation/js/translator',
6
    'oroui/js/mediator',
7
    'oroui/js/modal',
8
    'oroui/js/app/views/base/view',
9
    'base64',
10
    'oroui/js/tools'
11
], function($, _, routing, __, mediator, Modal, BaseView, base64, tools) {
0 ignored issues
show
Unused Code introduced by
The parameter tools is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
12
    'use strict';
13
14
    var PageStateView;
15
16
    PageStateView = BaseView.extend({
17
        listen: {
18
            'change:data model': '_saveModel',
19
            'change model': '_updateCache',
20
21
            'page:request mediator': 'onPageRequest',
22
            'page:update mediator': 'onPageUpdate',
23
            'page:afterChange mediator': 'afterPageChange',
24
            'page:afterPagePartChange mediator': 'afterPageChange',
25
            'page:beforeRefresh mediator': 'beforePageRefresh',
26
            'openLink:before mediator': 'beforePageChange',
27
28
            'add collection': 'toggleStateTrace',
29
            'remove collection': 'toggleStateTrace'
30
        },
31
32
        /**
33
         * @inheritDoc
34
         */
35
        constructor: function PageStateView() {
36
            PageStateView.__super__.constructor.apply(this, arguments);
37
        },
38
39
        /**
40
         * @inheritDoc
41
         */
42
        initialize: function() {
43
            var confirmModal;
44
45
            this._initialState = null;
46
            this._resetChanges = false;
47
48
            confirmModal = new Modal({
49
                title: __('Refresh Confirmation'),
50
                content: __('Your local changes will be lost. Are you sure you want to refresh the page?'),
51
                okText: __('OK, got it.'),
52
                className: 'modal modal-primary',
53
                cancelText: __('Cancel')
54
            });
55
            this.subview('confirmModal', confirmModal);
56
57
            $(window).on('beforeunload' + this.eventNamespace(), _.bind(this.onWindowUnload, this));
58
59
            PageStateView.__super__.initialize.apply(this, arguments);
60
        },
61
62
        /**
63
         * @inheritDoc
64
         */
65
        dispose: function() {
66
            if (this.disposed) {
67
                return;
68
            }
69
            $(window).off(this.eventNamespace());
70
            PageStateView.__super__.dispose.apply(this, arguments);
71
        },
72
73
        /**
74
         * Handle page's refresh action
75
         * if confirmation is required:
76
         *  - prepares deferred object
77
         *  - puts deferred object into refresh
78
         *
79
         * @param {Array} queue
80
         */
81
        beforePageRefresh: function(queue) {
82
            var deferred;
83
            var confirmModal;
84
            var self;
85
            if (!this.model.get('data')) {
86
                // data is not set, nothing to compare with
87
                return;
88
            }
89
            var preservedState = JSON.parse(this.model.get('data'));
90
            if (this._isStateChanged(preservedState)) {
91
                self = this;
92
                confirmModal = this.subview('confirmModal');
93
                deferred = $.Deferred();
94
95
                deferred.always(function() {
96
                    self.stopListening(confirmModal);
97
                });
98
                this.listenTo(confirmModal, 'ok', function() {
99
                    deferred.resolve({resetChanges: true});
100
                });
101
                this.listenTo(confirmModal, 'cancel', function() {
102
                    deferred.reject();
103
                });
104
105
                queue.push(deferred);
106
                confirmModal.open();
107
            }
108
        },
109
110
        /**
111
         * Returns if form state was changed
112
         *
113
         * @returns {Boolean}
114
         */
115
        isStateChanged: function() {
116
            return this._isStateChanged();
117
        },
118
119
        /**
120
         * Handles navigation action and shows confirm dialog
121
         * if page changes is not preserved and the state is changed from initial
122
         * (excludes cancel action)
123
         */
124
        beforePageChange: function(e) {
125
            var action = $(e.target).data('action');
126
            if (action !== 'cancel' && !this._isStateTraceRequired() && this._isStateChanged()) {
127
                e.prevented = !window.confirm(__('oro.ui.leave_page_with_unsaved_data_confirm'));
128
            }
129
        },
130
131
        /**
132
         * Clear page state timer and model on page request is started
133
         */
134
        onPageRequest: function() {
135
            this._initialState = null;
136
            this._resetChanges = false;
137
            this._switchOffTrace();
138
        },
139
140
        /**
141
         * Init page state on page updated
142
         * @param {Object} attributes
143
         * @param {Object} args
144
         */
145
        onPageUpdate: function(attributes, args) {
146
            var options;
147
            options = (args || {}).options;
148
            this._resetChanges = Boolean(options && options.resetChanges);
149
        },
150
151
        /**
152
         * Handles window unload event and shows confirm dialog
153
         * if page changes is not preserved and the state is changed from initial
154
         */
155
        onWindowUnload: function() {
156
            if (!this._isStateTraceRequired() && this._isStateChanged()) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if !this._isStateTraceRequi... this._isStateChanged() is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
157
                return __('oro.ui.leave_page_with_unsaved_data_confirm');
158
            }
159
        },
160
161
        /**
162
         * Fetches model's attributes from cache on page changes is done
163
         */
164
        afterPageChange: function() {
165
            var options;
166
167
            if (this._hasForm()) {
168
                this._initialState = this._collectFormsData();
169
            }
170
            if (!this._hasForm() || !this._isStateTraceRequired()) {
171
                return;
172
            }
173
174
            if (this._resetChanges) {
175
                // delete cache if changes are discarded
176
                mediator.execute('pageCache:state:save', 'form', null);
177
                options = {initial: true};
178
            }
179
180
            this._switchOnTrace(options);
0 ignored issues
show
Bug introduced by
The variable options does not seem to be initialized in case this._resetChanges on line 174 is false. Are you sure the function _switchOnTrace handles undefined variables?
Loading history...
181
        },
182
183
        /**
184
         * Switch on/off form state trace
185
         */
186
        toggleStateTrace: function() {
187
            var switchOn = this._isStateTraceRequired();
188
            if (switchOn) {
189
                this._switchOnTrace({initial: true});
190
            } else {
191
                this._switchOffTrace();
192
            }
193
        },
194
195
        /**
196
         * Switch on form state trace
197
         * @param {Object=} options
198
         * @protected
199
         */
200
        _switchOnTrace: function(options) {
201
            var attributes;
202
            attributes = mediator.execute('pageCache:state:fetch', 'form');
203
            if (attributes && attributes.id) {
204
                this._initStateTracer(attributes, options);
205
            } else {
206
                this._loadState(options);
207
            }
208
        },
209
210
        /**
211
         * Switch off form state trace
212
         * @protected
213
         */
214
        _switchOffTrace: function() {
215
            this.$el.off('change.page-state');
216
            this.model.clear({silent: true});
217
        },
218
219
        /**
220
         * Initializes form changes trace
221
         *  - if attributes is not in a cache, loads data from server
222
         * @param {Object=} options
223
         * @protected
224
         */
225
        _loadState: function(options) {
226
            var self = this;
227
            var checkIdRoute = 'oro_api_get_pagestate_checkid';
228
            var pageStateRoutes = this.$el.find('#pagestate-routes');
229
            if (pageStateRoutes.data()) {
230
                this.model.postRoute = pageStateRoutes.data('pagestate-put-route');
231
                this.model.putRoute = pageStateRoutes.data('pagestate-put-route');
232
                checkIdRoute = pageStateRoutes.data('pagestate-checkid-route');
233
            }
234
235
            var url = routing.generate(checkIdRoute, {pageId: this._combinePageId()});
236
            $.get(url).done(function(data) {
237
                var attributes;
238
                attributes = {
239
                    pageId: data.pagestate.pageId || self._combinePageId(),
240
                    data: self._resetChanges ? '' : data.pagestate.data,
241
                    pagestate: data.pagestate
242
                };
243
                if (data.id) {
244
                    attributes.id = data.id;
245
                }
246
                self._initStateTracer(attributes, options);
247
            });
248
        },
249
250
        /**
251
         * Resets page state model, restores page forms and start tracing changes
252
         * @param {Object} attributes
253
         * @param {Object=} options
254
         * @protected
255
         */
256
        _initStateTracer: function(attributes, options) {
257
            var currentData;
258
            options = options || {};
259
            currentData = JSON.stringify(this._collectFormsData());
260
            if (!attributes.data || options.initial) {
261
                attributes.data = currentData;
262
            }
263
            this.model.set(attributes);
264
            if (attributes.data !== currentData) {
265
                this._restoreState();
266
            }
267
            this.$el.on('change.page-state', _.bind(this._collectState, this));
268
        },
269
270
        /**
271
         * Updates state in cache on model sync
272
         * @protected
273
         */
274
        _updateCache: function() {
275
            var attributes;
276
            attributes = {};
277
            _.extend(attributes, this.model.getAttributes());
278
            mediator.execute('pageCache:state:save', 'form', attributes);
279
        },
280
281
        /**
282
         * Defines if page has forms and state tracing is required
283
         * @returns {boolean}
284
         * @protected
285
         */
286
        _hasForm: function() {
287
            return Boolean(this.$('form[data-collect=true]').length);
288
        },
289
290
        /**
291
         * Handles model save
292
         * @protected
293
         */
294
        _saveModel: function() {
295
            // page state is the same -- nothing to save
296
            if (this.model.get('pagestate').data === this.model.get('data')) {
297
                return;
298
            }
299
            // @TODO why data duplication is required?
300
            this.model.save({
301
                pagestate: {
302
                    pageId: this.model.get('pageId'),
303
                    data: this.model.get('data')
304
                }
305
            });
306
        },
307
308
        /**
309
         * Collects data of page forms and update model if state is changed
310
         *  - collects data
311
         *  - updates model
312
         * @protected
313
         */
314
        _collectState: function() {
315
            var pageId = this._combinePageId();
316
            if (!pageId) {
317
                return;
318
            }
319
320
            var data = JSON.stringify(this._collectFormsData());
321
322
            if (data === this.model.get('data')) {
323
                return;
324
            }
325
326
            this.model.set({
327
                pageId: pageId,
328
                data: data
329
            });
330
        },
331
332
        /**
333
         * Goes through the form and collects data
334
         * @returns {Array}
335
         * @protected
336
         */
337
        _collectFormsData: function() {
338
            var data;
339
            data = [];
340
            $('form[data-collect=true]').each(function(index, el) {
341
                var items = $(el)
342
                    .find('input, textarea, select')
343
                    .not(':input[type=button],   :input[type=submit], :input[type=reset], ' +
344
                         ':input[type=password], :input[type=file],   :input[name$="[_token]"], ' +
345
                         '.select2[type=hidden]');
346
347
                data[index] = items.serializeArray();
348
349
                // collect select2 selected data
350
                items = $(el).find('.select2[type=hidden], .select2[type=select]');
351
                _.each(items, function(item) {
352
                    var $item = $(item);
353
                    var selectedData = $item.inputWidget('data');
354
                    var itemData = {name: item.name, value: $item.val()};
355
356
                    if (!_.isEmpty(selectedData)) {
357
                        itemData.selectedData = selectedData;
358
                    }
359
360
                    data[index].push(itemData);
361
                });
362
            });
363
            return data;
364
        },
365
366
        /**
367
         * Reads data from model and restores page forms
368
         * @protected
369
         */
370
        _restoreState: function() {
371
            var data;
372
            data = this.model.get('data');
373
374
            if (data) {
375
                this._restoreForms(JSON.parse(data));
376
                mediator.trigger('pagestate_restored');
377
            }
378
        },
379
380
        /**
381
         * Updates form from data
382
         * @param {Array} data
383
         * @protected
384
         */
385
        _restoreForms: function(data) {
386
            $.each(data, function(index, el) {
387
                var form = $('form[data-collect=true]').eq(index);
388
389
                $.each(el, function(i, input) {
390
                    var element = form.find('[name="' + input.name + '"]');
391
                    switch (element.prop('type')) {
392
                        case 'checkbox':
393
                            element.filter('[value="' + input.value + '"]').prop('checked', true);
394
                            break;
395
                        case 'select-multiple':
396
                            element
397
                                .find('option').prop('selected', false).end()
398
                                .find('option[value="' + input.value + '"]').prop('selected', true);
399
                            break;
400
                        default:
401
                            if (input.selectedData) {
402
                                element.data('selected-data', input.selectedData);
403
                            }
404
                            if (input.value !== element.val()) {
405
                                element.val(input.value).trigger('change');
406
                            }
407
                    }
408
                });
409
            });
410
        },
411
412
        /**
413
         * Combines pageId
414
         * @returns {string}
415
         * @protected
416
         */
417
        _combinePageId: function() {
418
            var route;
419
            route = this._parseCurrentURL();
420
            return base64.encode(route.path);
421
        },
422
423
        /**
424
         * Parses URL for current page
425
         * @returns {Object}
426
         * @protected
427
         */
428
        _parseCurrentURL: function() {
429
            var route = mediator.execute('currentUrl');
430
            var _ref = route.split('?');
431
            route = {
432
                path: _ref[0],
433
                query: _ref[1] || ''
434
            };
435
            return route;
436
        },
437
438
        /**
439
         * Defines if page is in cache and state trace is required
440
         * @protected
441
         */
442
        _isStateTraceRequired: function() {
443
            return Boolean(this.collection.getCurrentModel());
444
        },
445
446
        /**
447
         * Check if passed or current state is different from initial sate
448
         *
449
         * @param {Array=} state if not passed collects current state
450
         * @returns {boolean}
451
         * @protected
452
         */
453
        _isStateChanged: function(state) {
454
            state = state || this._collectFormsData();
455
            return this._initialState !== null && this._isDifferentFromInitialState(state);
456
        },
457
458
        /**
459
         * Check if passed state is different from initial state
460
         * compares just name-value pairs
461
         * (comparison of JSON strings is not in use, because field items can contain extra-data)
462
         *
463
         * @param {Array} state
464
         * @returns {boolean}
465
         * @protected
466
         */
467
        _isDifferentFromInitialState: function(state) {
468
            var initialState = this._initialState;
469
470
            if (_.isArray(state)) {
471
                $.each(state, function(index, item) {
472
                    if (_.isArray(item)) {
473
                        item = $.grep(item, function(field) {
474
                            return _.isObject(field) && field.name.indexOf('temp-validation-name-') === -1;
475
                        });
476
                        state[index] = item;
477
                    }
478
                });
479
            }
480
481
            var isSame = initialState && _.every(initialState, function(form, i) {
482
                return _.isArray(state[i]) && _.every(form, function(field, j) {
483
                    return _.isObject(state[i][j]) &&
484
                        state[i][j].name === field.name && state[i][j].value === field.value;
485
                });
486
            });
487
            return !isSame;
488
        }
489
    });
490
491
    return PageStateView;
492
});
493