Passed
Push — master ( c3d610...926549 )
by Marcel
02:48
created

js/filter.js   B

Complexity

Total Complexity 48
Complexity/F 3.69

Size

Lines of Code 393
Function Count 13

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 285
c 0
b 0
f 0
dl 0
loc 393
rs 8.5599
wmc 48
mnd 35
bc 35
fnc 13
bpm 2.6923
cpm 3.6923
noi 4

How to fix   Complexity   

Complexity

Complex classes like js/filter.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
 * Analytics
3
 *
4
 * This file is licensed under the Affero General Public License version 3 or
5
 * later. See the LICENSE.md file.
6
 *
7
 * @author Marcel Scherello <[email protected]>
8
 * @copyright 2020 Marcel Scherello
9
 */
10
/** global: OCA */
11
/** global: OCP */
12
/** global: OC */
13
/** global: table */
14
/** global: Chart */
15
/** global: cloner */
16
17
'use strict';
18
/**
19
 * @namespace OCA.Analytics.Filter
20
 */
21
OCA.Analytics.Filter = {
22
    optionTextsArray: {
23
        'EQ': t('analytics', 'equal to'),
24
        'GT': t('analytics', 'greater than'),
25
        'LT': t('analytics', 'less than'),
26
        'LIKE': t('analytics', 'contains'),
27
        'IN': t('analytics', 'list of values'),
28
    },
29
30
    openDrilldownDialog: function () {
31
        let drilldownRows = '';
32
        let availableDimensions = OCA.Analytics.currentReportData.dimensions;
33
        let filterOptions = OCA.Analytics.currentReportData.options.filteroptions;
34
35
        for (let i = 0; i < Object.keys(availableDimensions).length; i++) {
36
            let checkboxStatus = 'checked';
37
            if (filterOptions['drilldown'] !== undefined && filterOptions['drilldown'][Object.keys(availableDimensions)[i]] !== undefined) {
38
                checkboxStatus = '';
39
            }
40
            drilldownRows = drilldownRows + '<div style="display: table-row;">'
41
                + '<div style="display: table-cell;">'
42
                + Object.values(availableDimensions)[i]
43
                + '</div>'
44
                + '<div style="display: table-cell;">'
45
                + '<input type="checkbox" id="drilldownColumn' + [i] + '" class="checkbox" name="drilldownColumn" value="' + Object.keys(availableDimensions)[i] + '" ' + checkboxStatus + '>'
46
                + '<label for="drilldownColumn' + [i] + '"> </label>'
47
                + '</div>'
48
                //+ '<div style="display: table-cell;">'
49
                //+ '<input type="checkbox" id="drilldownRow' + [i] + '" class="checkbox" name="drilldownRow" value="' + Object.keys(availableDimensions)[i] + '" disabled>'
50
                //+ '<label for="drilldownRow' + [i] + '"> </label>'
51
                //+ '</div>'
52
                + '</div>';
53
        }
54
55
        document.body.insertAdjacentHTML('beforeend',
56
            '<div id="analytics_dialog_overlay" class="oc-dialog-dim"></div>'
57
            + '<div id="analytics_dialog_container" class="oc-dialog" style="position: fixed;">'
58
            + '<div id="analytics_dialog">'
59
            + '<a class="oc-dialog-close" id="btnClose"></a>'
60
            + '<h2 class="oc-dialog-title" style="display:flex;margin-right:30px;">'
61
            + t('analytics', 'Drilldown')
62
            + '</h2>'
63
            + '<div class="table" style="display: table;">'
64
65
            + '<div style="display: table-row;">'
66
            + '<div style="display: table-cell; width: 150px;">'
67
            + '</div>'
68
            + '<div style="display: table-cell; width: 50px;">'
69
            + '<img src="img/column.svg" style="height: 20px;" alt="column">'
70
            + '</div>'
71
            //+ '<div style="display: table-cell; width: 50px;">'
72
            //+ '<img src="img/row.svg" style="height: 20px;" alt="row">'
73
            //+ '</div>'
74
            + '</div>'
75
            + drilldownRows
76
            + '</div>'
77
            + '<div class="oc-dialog-buttonrow boutons" id="buttons">'
78
            + '<a class="button primary" id="drilldownDialogGo">' + t('analytics', 'OK') + '</a>'
79
            + '<a class="button primary" id="drilldownDialogCancel">' + t('analytics', 'Cancel') + '</a>'
80
            + '</div>'
81
        );
82
83
        document.getElementById("btnClose").addEventListener("click", OCA.Analytics.Filter.close);
84
        document.getElementById("drilldownDialogCancel").addEventListener("click", OCA.Analytics.Filter.close);
85
        document.getElementById("drilldownDialogGo").addEventListener("click", OCA.Analytics.Filter.processDrilldownDialog);
86
    },
87
88
    processDrilldownDialog: function () {
89
        let filterOptions = OCA.Analytics.currentReportData.options.filteroptions;
90
        let drilldownColumns = document.getElementsByName('drilldownColumn');
91
92
        for (let i = 0; i < drilldownColumns.length; i++) {
93
            let dimension = drilldownColumns[i].value;
94
            if (drilldownColumns[i].checked === false) {
95
                if (filterOptions['drilldown'] === undefined) {
96
                    filterOptions['drilldown'] = {};
97
                }
98
                filterOptions['drilldown'][dimension] = false;
99
            } else {
100
                if (filterOptions['drilldown'] !== undefined && filterOptions['drilldown'][dimension] !== undefined) {
101
                    delete filterOptions['drilldown'][dimension];
102
                }
103
                if (filterOptions['drilldown'] !== undefined && Object.keys(filterOptions['drilldown']).length === 0) {
104
                    delete filterOptions['drilldown'];
105
                }
106
            }
107
        }
108
109
        OCA.Analytics.currentReportData.options.filteroptions = filterOptions;
110
        OCA.Analytics.Filter.Backend.updateDataset();
111
        OCA.Analytics.Filter.close();
112
    },
113
114
    openFilterDialog: function () {
115
        document.body.insertAdjacentHTML('beforeend',
116
            '<div id="analytics_dialog_overlay" class="oc-dialog-dim"></div>'
117
            + '<div id="analytics_dialog_container" class="oc-dialog" style="position: fixed;">'
118
            + '<div id="analytics_dialog">'
119
            + '<a class="oc-dialog-close" id="btnClose"></a>'
120
            + '<h2 class="oc-dialog-title" style="display:flex;margin-right:30px;">'
121
            + t('analytics', 'Filter')
122
            + '</h2>'
123
            + '<div class="table" style="display: table;">'
124
            + '<div style="display: table-row;">'
125
            + '<div style="display: table-cell; width: 50px;"></div>'
126
            + '<div style="display: table-cell; width: 80px;"></div>'
127
            + '<div style="display: table-cell; width: 100px;">'
128
            + '<label for="filterDialogDimension">' + t('analytics', 'Filter by') + '</label>'
129
            + '</div>'
130
            + '<div style="display: table-cell; width: 100px;">'
131
            + '<label for="filterDialogOption">' + t('analytics', 'Operator') + '</label>'
132
            + '</div>'
133
            + '<div style="display: table-cell;">'
134
            + '<label for="filterDialogValue">' + t('analytics', 'Value') + '</label>'
135
            + '</div>'
136
            + '</div>'
137
            + '<div style="display: table-row;">'
138
            + '<div style="display: table-cell;">'
139
            + '<img src="img/filteradd.svg" alt="filter">'
140
            + '</div>'
141
            + '<div style="display: table-cell;">'
142
            + '<select id="filterDialogType" class="checkbox" disabled>'
143
            + '<option value="and">' + t('analytics', 'and') + '</option>'
144
            + '</select>'
145
            + '</div>'
146
            + '<div style="display: table-cell;">'
147
            + '<select id="filterDialogDimension" class="checkbox">'
148
            + '</select>'
149
            + '</div>'
150
            + '<div style="display: table-cell;">'
151
            + '<select id="filterDialogOption" class="checkbox">'
152
            + '</select>'
153
            + '</div>'
154
            + '<div style="display: table-cell;">'
155
            + '<input type="text" id="filterDialogValue">'
156
            + '</div></div></div>'
157
            + '<div class="oc-dialog-buttonrow boutons" id="buttons">'
158
            + '<a class="button primary" id="filterDialogGo">' + t('analytics', 'Add') + '</a>'
159
            + '<a class="button primary" id="filterDialogCancel">' + t('analytics', 'Cancel') + '</a>'
160
            + '</div>'
161
        );
162
163
        let dimensionSelectOptions;
164
        let availableDimensions = OCA.Analytics.currentReportData.dimensions;
165
        for (let i = 0; i < Object.keys(availableDimensions).length; i++) {
166
            dimensionSelectOptions = dimensionSelectOptions + '<option value="' + Object.keys(availableDimensions)[i] + '">' + Object.values(availableDimensions)[i] + '</option>';
0 ignored issues
show
Bug introduced by
The variable dimensionSelectOptions seems to not be initialized for all possible execution paths.
Loading history...
167
        }
168
        document.getElementById('filterDialogDimension').innerHTML = dimensionSelectOptions;
169
170
        let optionSelectOptions;
171
        for (let i = 0; i < Object.keys(OCA.Analytics.Filter.optionTextsArray).length; i++) {
172
            optionSelectOptions = optionSelectOptions + '<option value="' + Object.keys(OCA.Analytics.Filter.optionTextsArray)[i] + '">' + Object.values(OCA.Analytics.Filter.optionTextsArray)[i] + '</option>';
0 ignored issues
show
Bug introduced by
The variable optionSelectOptions seems to not be initialized for all possible execution paths.
Loading history...
173
        }
174
        document.getElementById('filterDialogOption').innerHTML = optionSelectOptions;
175
176
        document.getElementById("btnClose").addEventListener("click", OCA.Analytics.Filter.close);
177
        document.getElementById("filterDialogCancel").addEventListener("click", OCA.Analytics.Filter.close);
178
        document.getElementById("filterDialogGo").addEventListener("click", OCA.Analytics.Filter.processFilterDialog);
179
        document.getElementById('filterDialogValue').addEventListener('keydown', function (event) {
180
            if (event.key === 'Enter') {
181
                OCA.Analytics.Filter.processFilterDialog();
182
            }
183
        });
184
    },
185
186
    processFilterDialog: function () {
187
        let filterOptions = OCA.Analytics.currentReportData.options.filteroptions;
188
        let dimension = document.getElementById('filterDialogDimension').value;
189
        if (filterOptions['filter'] === undefined) {
190
            filterOptions['filter'] = {};
191
        }
192
        if (filterOptions['filter'][dimension] === undefined) {
193
            filterOptions['filter'][dimension] = {};
194
        }
195
        filterOptions['filter'][dimension]['option'] = document.getElementById('filterDialogOption').value;
196
        filterOptions['filter'][dimension]['value'] = document.getElementById('filterDialogValue').value;
197
198
        OCA.Analytics.currentReportData.options.filteroptions = filterOptions;
199
        OCA.Analytics.Filter.Backend.updateDataset();
200
        OCA.Analytics.Filter.close();
201
    },
202
203
    refreshFilterVisualisation: function () {
204
        document.getElementById('filterVisualisation').innerHTML = '';
205
        let filterDimensions = OCA.Analytics.currentReportData.dimensions;
206
        let filterOptions = OCA.Analytics.currentReportData.options.filteroptions;
207
        if (filterOptions !== null && filterOptions['filter'] !== undefined) {
208
            for (let filterDimension of Object.keys(filterOptions['filter'])) {
209
                let optionText = OCA.Analytics.Filter.optionTextsArray[filterOptions['filter'][filterDimension]['option']];
210
                let span = document.createElement('span');
211
                span.innerText = filterDimensions[filterDimension] + ' ' + optionText + ' ' + filterOptions['filter'][filterDimension]['value'];
212
                span.classList.add('filterVisualizationItem');
213
                span.id = filterDimension;
214
                span.addEventListener('click', OCA.Analytics.Filter.removeFilter)
215
                document.getElementById('filterVisualisation').appendChild(span);
216
            }
217
        }
218
    },
219
220
    removeFilter: function (evt) {
221
        let filterDimension = evt.target.id;
222
        let filterOptions = OCA.Analytics.currentReportData.options.filteroptions;
223
        delete filterOptions['filter'][filterDimension];
224
        if (Object.keys(filterOptions['filter']).length === 0) {
225
            delete filterOptions['filter'];
226
        }
227
        OCA.Analytics.currentReportData.options.filteroptions = filterOptions;
228
        OCA.Analytics.Filter.Backend.updateDataset();
229
    },
230
231
    openOptionsDialog: function () {
232
        let drilldownRows = '';
233
        let dataOptions;
234
        try {
235
            dataOptions = JSON.parse(OCA.Analytics.currentReportData.options.dataoptions);
236
        } catch (e) {
237
            dataOptions = '';
238
        }
239
        let distinctCategories = OCA.Analytics.Core.getDistinctValues(OCA.Analytics.currentReportData.data);
240
        if (dataOptions === null) dataOptions = {};
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
241
242
        // check if defined dataoptions don´t match the number of dataseries anymore
243
        if (Object.keys(dataOptions).length !== Object.keys(distinctCategories).length) {
244
            dataOptions = '';
245
        }
246
247
        // get the default chart type to preset the drop downs
248
        let defaultChartType = OCA.Analytics.chartTypeMapping[OCA.Analytics.currentReportData.options.chart];
249
250
        for (let i = 0; i < Object.keys(distinctCategories).length; i++) {
251
            drilldownRows = drilldownRows + '<div style="display: table-row;">'
252
                + '<div style="display: table-cell;">'
253
                + Object.values(distinctCategories)[i]
254
                + '</div>'
255
                + '<div style="display: table-cell;">'
256
                + '<select id="optionsYAxis' + [i] + '" name="optionsYAxis">'
257
                + '<option value="primary" ' + OCA.Analytics.Filter.checkOption(dataOptions, i, 'yAxisID', 'primary', 'primary') + '>' + t('analytics', 'Primary') + '</option>'
258
                + '<option value="secondary" ' + OCA.Analytics.Filter.checkOption(dataOptions, i, 'yAxisID', 'secondary', 'primary') + '>' + t('analytics', 'Secondary') + '</option>'
259
                + '</select>'
260
                + '</div>'
261
                + '<div style="display: table-cell;">'
262
                + '<select id="optionsChartType' + [i] + '" name="optionsChartType">'
263
                + '<option value="line" ' + OCA.Analytics.Filter.checkOption(dataOptions, i, 'type', 'line', defaultChartType) + '>' + t('analytics', 'Line') + '</option>'
264
                + '<option value="bar" ' + OCA.Analytics.Filter.checkOption(dataOptions, i, 'type', 'bar', defaultChartType) + '>' + t('analytics', 'Column') + '</option>'
265
                + '</select>'
266
                + '</div>'
267
                + '</div>';
268
        }
269
270
        document.body.insertAdjacentHTML('beforeend',
271
            '<div id="analytics_dialog_overlay" class="oc-dialog-dim"></div>'
272
            + '<div id="analytics_dialog_container" class="oc-dialog" style="position: fixed;">'
273
            + '<div id="analytics_dialog">'
274
            + '<a class="oc-dialog-close" id="btnClose"></a>'
275
            + '<h2 class="oc-dialog-title" style="display:flex;margin-right:30px;">'
276
            + t('analytics', 'Options')
277
            + '</h2>'
278
            + '<div class="table" style="display: table;">'
279
280
            + '<div style="display: table-row;">'
281
            + '<div style="display: table-cell; width: 150px;">' + t('analytics', 'Data series')
282
            + '</div>'
283
            + '<div style="display: table-cell; width: 150px;">' + t('analytics', 'Vertical axis')
284
            + '</div>'
285
            + '<div style="display: table-cell; width: 150px;">' + t('analytics', 'Chart type')
286
            + '</div>'
287
            + '</div>'
288
            + drilldownRows
289
            + '</div>'
290
            + '<div class="oc-dialog-buttonrow boutons" id="buttons">'
291
            + '<a class="button primary" id="drilldownDialogGo">' + t('analytics', 'OK') + '</a>'
292
            + '<a class="button primary" id="drilldownDialogCancel">' + t('analytics', 'Cancel') + '</a>'
293
            + '</div>'
294
        );
295
296
        document.getElementById("btnClose").addEventListener("click", OCA.Analytics.Filter.close);
297
        document.getElementById("drilldownDialogCancel").addEventListener("click", OCA.Analytics.Filter.close);
298
        document.getElementById("drilldownDialogGo").addEventListener("click", OCA.Analytics.Filter.processOptionsDialog);
299
    },
300
301
    processOptionsDialog: function () {
302
        let dataOptions = OCA.Analytics.currentReportData.options.dataoptions;
303
        dataOptions === '' ? dataOptions = [] : dataOptions;
304
        let chartOptions = OCA.Analytics.currentReportData.options.chartoptions;
305
        chartOptions === '' ? chartOptions = {} : chartOptions;
306
        let userDatasetOptions = [];
307
        let nonDefaultValues, seondaryAxisRequired = false;
308
        // get the default chart types (e.g. line or bar) to derive if there is any relevant change by the user
309
        let defaultChartType = OCA.Analytics.chartTypeMapping[OCA.Analytics.currentReportData.options.chart];
310
311
        // loop all selections from the option dialog and add them to an array
312
        let optionsYAxis = document.getElementsByName('optionsYAxis');
313
        let optionsChartType = document.getElementsByName('optionsChartType');
314
        for (let i = 0; i < optionsYAxis.length; i++) {
315
            if (optionsYAxis[i].value !== 'primary') {
316
                seondaryAxisRequired = nonDefaultValues = true;     // secondary y-axis enabled
317
            } else if (optionsChartType[i].value !== defaultChartType) {
318
                nonDefaultValues = true;    // just line/bar changed
319
            }
320
            userDatasetOptions.push({yAxisID: optionsYAxis[i].value, type: optionsChartType[i].value});
321
        }
322
323
        // decide of the dataseries array is relevant to be saved or not.
324
        // if all settings are default, all options can be removed can be removed completely
325
        if (nonDefaultValues === true) {
0 ignored issues
show
Bug introduced by
The variable nonDefaultValues seems to not be initialized for all possible execution paths.
Loading history...
326
            try {
327
                // if there are existing settings, merge them
328
                dataOptions = JSON.stringify(cloner.deep.merge(JSON.parse(dataOptions), userDatasetOptions));
329
            } catch (e) {
330
                dataOptions = JSON.stringify(userDatasetOptions);
331
            }
332
        } else {
333
            dataOptions = '';
334
        }
335
336
        // if any dataseries is tied to the secondary yAxis or not
337
        //if yes, it needs to be enabled in the chart options (in addition to the dataseries options)
338
        let enableAxis = '{"scales":{"yAxes":[{},{"display":true}]}}';
339
        if (seondaryAxisRequired === true) {
340
            try {
341
                // if there are existing settings, merge them
342
                chartOptions = JSON.stringify(cloner.deep.merge(JSON.parse(chartOptions), JSON.parse(enableAxis)));
343
            } catch (e) {
344
                chartOptions = enableAxis;
345
            }
346
        } else {
347
            if (chartOptions === enableAxis) {
348
                // if the secondary axis is not required anymore but was enabled before
349
                // the options are cleared all together
350
                // this does only apply when ONLY the axis was enabled before
351
                // this does not do anything, if the user had own custom settings
352
                chartOptions = '';
353
            }
354
        }
355
356
        OCA.Analytics.currentReportData.options.dataoptions = dataOptions;
357
        OCA.Analytics.currentReportData.options.chartoptions = chartOptions;
358
        OCA.Analytics.Filter.Backend.updateDataset();
359
        OCA.Analytics.Filter.close();
360
    },
361
362
    close: function () {
363
        document.getElementById('analytics_dialog_container').remove();
364
        document.getElementById('analytics_dialog_overlay').remove();
365
    },
366
367
    // function for shorter coding of the dialog creation
368
    checkOption: function (array, index, field, check, defaultChartType) {
369
        if (Array.isArray(array) && array.length) {
370
            if (field in array[index]) {
371
                return array[index][field] === check ? 'selected' : '';
372
            } else if (check === defaultChartType) {
373
                return 'selected';
374
            } else {
375
                return '';
376
            }
377
        } else if (check === defaultChartType) {
378
            return 'selected';
379
        } else {
380
            return '';
381
        }
382
    },
383
384
};
385
386
OCA.Analytics.Filter.Backend = {
387
    updateDataset: function () {
388
        const datasetId = parseInt(OCA.Analytics.currentReportData.options.id);
389
390
        if (Object.keys(OCA.Analytics.currentReportData.options.filteroptions).length === 0) {
391
            OCA.Analytics.currentReportData.options.filteroptions = '';
392
        } else {
393
            OCA.Analytics.currentReportData.options.filteroptions = JSON.stringify(OCA.Analytics.currentReportData.options.filteroptions);
394
        }
395
396
        $.ajax({
397
            type: 'POST',
398
            url: OC.generateUrl('apps/analytics/dataset/') + datasetId,
399
            data: {
400
                'chartoptions': OCA.Analytics.currentReportData.options.chartoptions,
401
                'dataoptions': OCA.Analytics.currentReportData.options.dataoptions,
402
                'filteroptions': OCA.Analytics.currentReportData.options.filteroptions,
403
            },
404
            success: function () {
405
                OCA.Analytics.Backend.getData();
406
            }
407
        });
408
    },
409
};