@datarailsshared/dr_renderer 1.2.325 → 1.2.327-dragons

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarailsshared/dr_renderer",
3
- "version": "1.2.325",
3
+ "version": "1.2.327-dragons",
4
4
  "description": "DataRails charts and tables renderer",
5
5
  "keywords": [
6
6
  "datarails",
@@ -199,10 +199,11 @@ let initDRPivotTable = function($, window, document) {
199
199
  let flatColKey = colKey.join(delim);
200
200
 
201
201
  if (this.keysLength === rowKey.length + colKey.length) {
202
- if (!this.rowKeys.some(rKey => rKey.join(delim) === flatRowKey)) {
202
+ const isDrValuesRows = this.rowAttrs.length === 1 && this.rowAttrs[0] === 'DR_Values';
203
+ if ((!this.isKeysSortingDoneOnBackendSide || isDrValuesRows) && !this.rowKeys.some(rKey => rKey.join(delim) === flatRowKey)) {
203
204
  this.rowKeys.push(rowKey);
204
205
  }
205
- if (!this.colKeys.some(cKey => cKey.join(delim) === flatColKey)) {
206
+ if (!this.isKeysSortingDoneOnBackendSide && !this.colKeys.some(cKey => cKey.join(delim) === flatColKey)) {
206
207
  this.colKeys.push(colKey);
207
208
  }
208
209
  }
@@ -234,7 +235,6 @@ let initDRPivotTable = function($, window, document) {
234
235
  insight: insight,
235
236
  });
236
237
  }
237
-
238
238
  return;
239
239
  }
240
240
 
@@ -1157,6 +1157,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
1157
1157
  ob.name = row_n_value.join(highchartsRenderer.delimer)
1158
1158
  .replace(highchartsRenderer.DR_OTHERS_KEY, othersName);
1159
1159
  }
1160
+
1160
1161
  lodash.forEach(col_n_keys, function (col_n_value, index) {
1161
1162
  var agg = pivotData.getAggregator(row_n_value, col_n_value);
1162
1163
  var val = agg.value();
@@ -1398,6 +1399,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
1398
1399
  if (opts.trendLine) {
1399
1400
  const a = ((ySum * squareXSum) - (xSum * xySum)) / ((n * squareXSum) - (xSum * xSum));
1400
1401
  const b = ((n * xySum) - (xSum* ySum)) / ((n * squareXSum) - (xSum * xSum));
1402
+
1401
1403
  const trendSeries = lodash.clone(chart_series[chart_series.length - 1]);
1402
1404
  trendSeries.className = 'trendSeries';
1403
1405
  trendSeries.name = highchartsRenderer.getTrendSeriesName(trendSeries);
@@ -1408,6 +1410,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
1408
1410
  if (colors && colors[i]) {
1409
1411
  trendSeries.color = colors[i];
1410
1412
  }
1413
+
1411
1414
  trendSerieses.push(trendSeries);
1412
1415
  }
1413
1416
  i++;
@@ -1424,6 +1427,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
1424
1427
  }
1425
1428
 
1426
1429
  let weights = { line: 2,spline: 3 ,area:-2, areaspline: -1, scatter:4, column: 1 };
1430
+
1427
1431
  if (opts.comboOptions && lodash.includes(chartType,'combo') && !lodash.isEqual(row_n_keys, EMPTY_ROW_N_KEYS)) {
1428
1432
  chart_series.forEach((series, seriesIndex) => {
1429
1433
  const savedSeriesOption = lodash.find(opts.comboOptions.seriesOptions, {series: series.name});
@@ -1510,6 +1514,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
1510
1514
  const chart_series = [];
1511
1515
  const row_n_keys = pivotData.getRowKeys();
1512
1516
  const col_n_keys = pivotData.getColKeys();
1517
+ const rows_by_cols = pivotData.rowKeysByCols;
1513
1518
 
1514
1519
  let resultObject = {
1515
1520
  data: [],
@@ -1538,7 +1543,9 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
1538
1543
  });
1539
1544
 
1540
1545
  if (col_index !== col_n_keys.length - 1) {
1541
- lodash.forEach(row_n_keys, function (row_n_value) {
1546
+
1547
+ const rowKeys = rows_by_cols ? rows_by_cols[col_index] : row_n_keys;
1548
+ lodash.forEach(rowKeys, function (row_n_value) {
1542
1549
  const agg = pivotData.getAggregator(row_n_value, col_n_value);
1543
1550
  let val = agg.value();
1544
1551
 
@@ -4778,21 +4785,23 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
4778
4785
  opts.rendererOptions.onlyOptions = true;
4779
4786
  }
4780
4787
 
4781
- const sortByValueSettings = lodash.filter(
4782
- lodash.get(widget, 'options.sortingFields', []),
4783
- sortingField => lodash.includes(['field_values', 'variance'], lodash.get(sortingField, 'sorting.sort_by'))
4784
- );
4785
-
4786
- if (sortByValueSettings.length) {
4787
- pivotData.sortByValueAttrs = lodash.map(sortByValueSettings, fieldSorting => fieldSorting.name);
4788
- let new_sorting_function = highchartsRenderer.generateSortingFunctionByValues(sortByValueSettings, pivotData, opts, widget);
4789
- opts.sorters = new_sorting_function;
4790
- optsFiltered.sorters = new_sorting_function;
4791
- pivotData.sorters = new_sorting_function;
4788
+ if (!highchartsRenderer.isSortingOnBackendEnabled()) {
4789
+ const sortByValueSettings = lodash.filter(
4790
+ lodash.get(widget, 'options.sortingFields', []),
4791
+ sortingField => lodash.includes(['field_values', 'variance'], lodash.get(sortingField, 'sorting.sort_by'))
4792
+ );
4793
+
4794
+ if (sortByValueSettings.length) {
4795
+ pivotData.sortByValueAttrs = lodash.map(sortByValueSettings, fieldSorting => fieldSorting.name);
4796
+ let new_sorting_function = highchartsRenderer.generateSortingFunctionByValues(sortByValueSettings, pivotData, opts, widget);
4797
+ opts.sorters = new_sorting_function;
4798
+ optsFiltered.sorters = new_sorting_function;
4799
+ pivotData.sorters = new_sorting_function;
4792
4800
 
4793
4801
  if (lodash.isObject(lodash.get(widget, 'pivot'))) {
4794
4802
  widget.pivot.sorters = new_sorting_function;
4795
4803
  }
4804
+ }
4796
4805
  }
4797
4806
 
4798
4807
  result = opts.renderer(pivotData, opts.rendererOptions);
@@ -4851,6 +4860,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
4851
4860
  rows: lodash.map(pivotOptions.legendArray, 'name'),
4852
4861
  rendererOptions: widget.options,
4853
4862
  dateValuesDictionary: pivotOptions ? pivotOptions.dateValuesDictionary : null,
4863
+ keysObject: pivotOptions ? pivotOptions.keysObject : null,
4854
4864
  };
4855
4865
 
4856
4866
  if (!subopts.rendererOptions) {
@@ -8717,6 +8727,8 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8717
8727
  };
8718
8728
 
8719
8729
  highchartsRenderer.getWidgetDataSorters = function (res, widget, defaultDateFormat) {
8730
+ let sorters;
8731
+
8720
8732
  if ($.pivotUtilities && !$.pivotUtilities.additionalFieldsList) {
8721
8733
  $.pivotUtilities.additionalFieldsList = [
8722
8734
  {key: 'DR_Average', name: 'DR_Average'},
@@ -8724,7 +8736,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8724
8736
  ];
8725
8737
  }
8726
8738
 
8727
- var datesFields = [];
8739
+ let datesFields = [];
8728
8740
  datesFields = lodash.filter(widget.rows, element => element.type == 'Date');
8729
8741
  datesFields = datesFields.concat(lodash.filter(widget.cols, element => element.type == 'Date'));
8730
8742
 
@@ -8742,14 +8754,13 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8742
8754
  return { "format": highchartsRenderer.getDateFieldFormat(widget, row), "name": row.name, "type": row.type, "values": [], "sorting": row.sorting } //'MMM - yyyy' format
8743
8755
  });
8744
8756
 
8745
- var data = res;
8746
-
8747
8757
  lodash.forEach(datesFields, function (row) {
8748
8758
  row.val_not_convert = highchartsRenderer.check_values_not_for_convert(widget, row.name);
8749
8759
  });
8750
8760
 
8751
8761
  if (datesFields.length > 0) {
8752
- lodash.forEach(data, function (element) {
8762
+ const invertedDateStringMap = {};
8763
+ lodash.forEach(res, function (element) {
8753
8764
  for (var i in datesFields) {
8754
8765
  if (element.hasOwnProperty(datesFields[i].name)) {
8755
8766
  datesFields[i].values.push(element[datesFields[i].name]);
@@ -8762,125 +8773,147 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8762
8773
  widget.pivot.dateValuesDictionary = {}
8763
8774
  }
8764
8775
  widget.pivot.dateValuesDictionary[dateStringValue] = element[datesFields[i].name];
8776
+ invertedDateStringMap[element[datesFields[i].name]] = dateStringValue;
8765
8777
  }
8766
8778
  element[datesFields[i].name] = dateStringValue;
8767
8779
  }
8768
8780
  }
8769
8781
  });
8782
+
8783
+ if (highchartsRenderer.isSortingOnBackendEnabled()) {
8784
+ lodash.forEach(['col_keys', 'row_keys'], (keysListName, index) => {
8785
+ const widgetFields = index ? widget.rows : widget.cols;
8786
+ const uniqueFormattedKeysList = [];
8787
+ lodash.forEach(widget.pivot.keysObject[keysListName], subKeysList => {
8788
+ lodash.forEach(subKeysList, (key, index) => {
8789
+ if (widgetFields[index].type === 'Date') {
8790
+ subKeysList[index] = invertedDateStringMap[key] || key;
8791
+ }
8792
+ });
8793
+ if (!lodash.find(uniqueFormattedKeysList, formattedSubKeys => lodash.isEqual(formattedSubKeys, subKeysList))) {
8794
+ uniqueFormattedKeysList.push(subKeysList);
8795
+ }
8796
+ });
8797
+ widget.pivot.keysObject[keysListName] = uniqueFormattedKeysList;
8798
+ });
8799
+ }
8770
8800
  }
8771
- lodash.forEach(datesFields, function (row) {
8772
- row.values = lodash.uniq(row.values);
8773
8801
 
8774
- const isTimestampDateField = row.type === 'Date' && lodash.some(row.values, value => typeof value ==='number');
8775
- if (isTimestampDateField) {
8776
- const nullValueIndex = row.values.indexOf(NULL_VALUE);
8777
- if (~nullValueIndex) {
8778
- row.values.splice(nullValueIndex, 1);
8802
+ if (!highchartsRenderer.isSortingOnBackendEnabled()) {
8803
+
8804
+ lodash.forEach(datesFields, function (row) {
8805
+ row.values = lodash.uniq(row.values);
8806
+
8807
+ const isTimestampDateField = row.type === 'Date' && lodash.some(row.values, value => typeof value ==='number');
8808
+ if (isTimestampDateField) {
8809
+ const nullValueIndex = row.values.indexOf(NULL_VALUE);
8810
+ if (~nullValueIndex) {
8811
+ row.values.splice(nullValueIndex, 1);
8812
+ }
8813
+ row.values = row.values.sort((a, b) => a - b);
8814
+ if (~nullValueIndex) {
8815
+ row.values.push(NULL_VALUE);
8816
+ }
8817
+ } else {
8818
+ row.values = row.values.sort();
8779
8819
  }
8780
- row.values = row.values.sort((a, b) => a - b);
8781
- if (~nullValueIndex) {
8782
- row.values.push(NULL_VALUE);
8820
+
8821
+ if (row.sorting && row.sorting.type == "largestToSmallest") {
8822
+ row.values = lodash.reverse(row.values);
8783
8823
  }
8784
- } else {
8785
- row.values = row.values.sort();
8786
- }
8824
+ delete row.sorting;
8825
+ row.values = lodash.map(row.values, function (val) {
8826
+ return highchartsRenderer.returnRawDataValue(row.type, val, row.format, row.name, row.val_not_convert) + "";
8827
+ })
8828
+
8829
+ });
8787
8830
 
8788
- if (row.sorting && row.sorting.type == "largestToSmallest") {
8789
- row.values = lodash.reverse(row.values);
8831
+ /* date string */
8832
+ var rowsAndCols = [];
8833
+ rowsAndCols = widget.rows.concat(widget.cols);
8834
+
8835
+ if (widget.chart_type === highchartsRenderer.CHART_TYPES.WATERFALL_BREAKDOWN) {
8836
+
8837
+ // if it is breakdown widget - redefine sorting according to breakdown_options
8838
+ // TODO: remove this when BE sort will be implemented
8839
+ lodash.forEach(rowsAndCols, function (field) {
8840
+ const waterfallFieldType = field.id === widget.cols[0].id ? 'totals' : 'breakdown';
8841
+ field.sorting = {
8842
+ type: 'CustomOrder',
8843
+ values: lodash.map(
8844
+ widget.options.breakdown_options.values[waterfallFieldType],
8845
+ value => value.key
8846
+ ),
8847
+ };
8848
+ });
8849
+ } else if (isCustomSorting) {
8850
+ lodash.forEach(rowsAndCols, function (field) {
8851
+ const fieldToSort = lodash.find(
8852
+ widget.options.sortingFields, element => element.id === field.id && lodash.get(element, 'sorting.sort_by') === 'field_items'
8853
+ );
8854
+ field.sorting = fieldToSort ? fieldToSort.sorting : field.sorting;
8855
+ });
8790
8856
  }
8791
- delete row.sorting;
8792
- row.values = lodash.map(row.values, function (val) {
8793
- return highchartsRenderer.returnRawDataValue(row.type, val, row.format, row.name, row.val_not_convert) + "";
8794
- })
8795
-
8796
- });
8797
8857
 
8798
- /* date string */
8799
- var rowsAndCols = [];
8800
- rowsAndCols = widget.rows.concat(widget.cols);
8801
-
8802
- if (widget.chart_type === highchartsRenderer.CHART_TYPES.WATERFALL_BREAKDOWN) {
8803
-
8804
- // if it is breakdown widget - redefine sorting according to breakdown_options
8805
- // TODO: remove this when BE sort will be implemented
8806
8858
  lodash.forEach(rowsAndCols, function (field) {
8807
- const waterfallFieldType = field.id === widget.cols[0].id ? 'totals' : 'breakdown';
8808
- field.sorting = {
8809
- type: 'CustomOrder',
8810
- values: lodash.map(
8811
- widget.options.breakdown_options.values[waterfallFieldType],
8812
- value => value.key
8813
- ),
8814
- };
8815
- });
8816
- } else if (isCustomSorting) {
8817
- lodash.forEach(rowsAndCols, function (field) {
8818
- const fieldToSort = lodash.find(
8819
- widget.options.sortingFields, element => element.id === field.id && lodash.get(element, 'sorting.sort_by') === 'field_items'
8820
- );
8821
- field.sorting = fieldToSort ? fieldToSort.sorting : field.sorting;
8822
- });
8823
- }
8824
-
8825
- lodash.forEach(rowsAndCols, function (field) {
8826
- if (field.sorting && (field.sorting.type == "DateString" || field.sorting.type == "largestToSmallest")) {
8827
- var find_field = lodash.find(datesFields, {name: field.name});
8828
- if (find_field) {
8829
- if (find_field.type != 'Date')
8830
- find_field.sorting = field.sorting;
8831
- } else {
8859
+ if (field.sorting && (field.sorting.type == "DateString" || field.sorting.type == "largestToSmallest")) {
8860
+ var find_field = lodash.find(datesFields, {name: field.name});
8861
+ if (find_field) {
8862
+ if (find_field.type != 'Date')
8863
+ find_field.sorting = field.sorting;
8864
+ } else {
8865
+ datesFields.push({
8866
+ "format": field.format,
8867
+ "name": field.name,
8868
+ "type": field.type,
8869
+ "values": [],
8870
+ "sorting": field.sorting,
8871
+ });
8872
+ }
8873
+ } else if (field.sorting && field.sorting.type == "CustomOrder" && field.sorting.values) {
8832
8874
  datesFields.push({
8833
8875
  "format": field.format,
8834
8876
  "name": field.name,
8835
8877
  "type": field.type,
8836
- "values": [],
8837
- "sorting": field.sorting,
8878
+ "values": field.sorting.values
8838
8879
  });
8839
8880
  }
8840
- } else if (field.sorting && field.sorting.type == "CustomOrder" && field.sorting.values) {
8841
- datesFields.push({
8842
- "format": field.format,
8843
- "name": field.name,
8844
- "type": field.type,
8845
- "values": field.sorting.values
8846
- });
8847
- }
8848
- });
8849
-
8850
- if (widget.vals && widget.vals.length > 1) {
8851
- datesFields.push({name: "DR_Values", values: lodash.map(widget.vals, 'name')});
8852
- }
8881
+ });
8853
8882
 
8854
- /****** END *******/
8855
-
8856
- // TODO: Remove. sortingValues looks like lagacy which is not in use neither in webclient nor in renderer
8857
- if (widget.options && widget.options.sortingValues) {
8858
- var field = lodash.find(datesFields, {name: widget.options.sortingValues.field});
8859
- if (field) {
8860
- field.values = widget.options.sortingValues.values;
8861
- field.sorting = null;
8862
- } else {
8863
- datesFields.push({
8864
- name: widget.options.sortingValues.field,
8865
- values: widget.options.sortingValues.values
8866
- });
8883
+ if (widget.vals && widget.vals.length > 1) {
8884
+ datesFields.push({name: "DR_Values", values: lodash.map(widget.vals, 'name')});
8867
8885
  }
8868
- }
8869
-
8870
- let sorters = function (attr) {
8871
- var field = lodash.find(datesFields, {name: attr});
8872
- if (field)
8873
- if (field.sorting && field.sorting.type == "DateString") {
8874
- return $.pivotUtilities.sortDateStrings(field.sorting.month_order);
8875
- } else if (field.sorting && field.sorting.type == "largestToSmallest") {
8876
- if (field.sorting.is_absolute)
8877
- return $.pivotUtilities.largeToSmallSortByAbsolute;
8878
-
8879
- return $.pivotUtilities.largeToSmallSort;
8886
+
8887
+ /****** END *******/
8888
+
8889
+ // TODO: Remove. sortingValues looks like lagacy which is not in use neither in webclient nor in renderer
8890
+ if (widget.options && widget.options.sortingValues) {
8891
+ var field = lodash.find(datesFields, {name: widget.options.sortingValues.field});
8892
+ if (field) {
8893
+ field.values = widget.options.sortingValues.values;
8894
+ field.sorting = null;
8880
8895
  } else {
8881
- return $.pivotUtilities.sortAs(field.values);
8896
+ datesFields.push({
8897
+ name: widget.options.sortingValues.field,
8898
+ values: widget.options.sortingValues.values
8899
+ });
8882
8900
  }
8883
- };
8901
+ }
8902
+ sorters = function (attr) {
8903
+ var field = lodash.find(datesFields, {name: attr});
8904
+ if (field)
8905
+ if (field.sorting && field.sorting.type == "DateString") {
8906
+ return $.pivotUtilities.sortDateStrings(field.sorting.month_order);
8907
+ } else if (field.sorting && field.sorting.type == "largestToSmallest") {
8908
+ if (field.sorting.is_absolute)
8909
+ return $.pivotUtilities.largeToSmallSortByAbsolute;
8910
+
8911
+ return $.pivotUtilities.largeToSmallSort;
8912
+ } else {
8913
+ return $.pivotUtilities.sortAs(field.values);
8914
+ }
8915
+ };
8916
+ }
8884
8917
 
8885
8918
  return sorters;
8886
8919
  };
@@ -8900,6 +8933,12 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8900
8933
 
8901
8934
  //highchartsRenderer.getGraphOptions(scope.data, override_values, res, scope.dataModel.templatesWithOutData, scope.openDrillDownList, drillDownFunction)
8902
8935
  highchartsRenderer.getGraphOptions = function (widget_obj, override_values, row_data, templates, openDrillDownListFunction, drillDownFunction) {
8936
+
8937
+ let keysObject;
8938
+ if (highchartsRenderer.isSortingOnBackendEnabled()) {
8939
+ keysObject = row_data.pop();
8940
+ }
8941
+
8903
8942
  let res = highchartsRenderer.updateSelectedOverrideValues(widget_obj, override_values, row_data);
8904
8943
  res = highchartsRenderer.convertUniqueDateValues(widget_obj, templates, res);
8905
8944
 
@@ -8911,6 +8950,10 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8911
8950
 
8912
8951
  let pivot = {};
8913
8952
 
8953
+ if (highchartsRenderer.isSortingOnBackendEnabled()) {
8954
+ pivot.keysObject = keysObject;
8955
+ }
8956
+
8914
8957
  let templateNoData = lodash.find(templates, {id: widget_obj.template_id});
8915
8958
  if (templateNoData) {
8916
8959
 
@@ -8947,6 +8990,8 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
8947
8990
  subopts.onlyOptions = true;
8948
8991
  }
8949
8992
 
8993
+ subopts.keysObject = keysObject;
8994
+
8950
8995
  let hc_options = highchartsRenderer.rhPivotView(res, subopts, is_table, widget_obj);
8951
8996
 
8952
8997
  return hc_options;
@@ -9301,6 +9346,10 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
9301
9346
  return highchartsRenderer.enabledNewWidgetValueFormatting && (isCustomFormat || !isSecondaryAxis);
9302
9347
  }
9303
9348
 
9349
+ highchartsRenderer.isSortingOnBackendEnabled = function() {
9350
+ return lodash.includes(lodash.get(document, 'ReportHippo.user.features'), 'enable_server_widget_data_sorting');
9351
+ }
9352
+
9304
9353
  return highchartsRenderer;
9305
9354
  };
9306
9355
 
package/src/pivottable.js CHANGED
@@ -686,8 +686,17 @@ let initPivotTable = function($, window, document) {
686
686
  });
687
687
  this.tree = {};
688
688
  this.insights = [];
689
- this.rowKeys = [];
690
- this.colKeys = [];
689
+
690
+ this.isKeysSortingDoneOnBackendSide = opts.keysObject && typeof opts.keysObject === 'object';
691
+ if (this.isKeysSortingDoneOnBackendSide) {
692
+ this.rowKeys = opts.keysObject.row_keys;
693
+ this.colKeys = opts.keysObject.col_keys;
694
+ // TODO: add also for breakdown sort object when BE story is ready.
695
+ } else {
696
+ this.rowKeys = [];
697
+ this.colKeys = [];
698
+ }
699
+
691
700
  this.rowTotals = {};
692
701
  this.colTotals = {};
693
702
  this.allTotal = this.aggregator(this, [], []);
@@ -853,12 +862,16 @@ let initPivotTable = function($, window, document) {
853
862
  };
854
863
 
855
864
  PivotData.prototype.getColKeys = function() {
856
- this.sortKeys();
865
+ if (!this.isKeysSortingDoneOnBackendSide) {
866
+ this.sortKeys();
867
+ }
857
868
  return this.colKeys;
858
869
  };
859
870
 
860
871
  PivotData.prototype.getRowKeys = function() {
861
- this.sortKeys();
872
+ if (!this.isKeysSortingDoneOnBackendSide) {
873
+ this.sortKeys();
874
+ }
862
875
  return this.rowKeys;
863
876
  };
864
877