@adaptabletools/adaptable 24.0.0-canary.5 → 24.0.0-canary.7

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.
Files changed (138) hide show
  1. package/devtools.d.ts +182 -1
  2. package/index.css +300 -275
  3. package/package.json +2 -1
  4. package/src/AdaptableInterfaces/IAdaptable.d.ts +4 -4
  5. package/src/AdaptableOptions/DefaultAdaptableOptions.js +5 -5
  6. package/src/AdaptableOptions/FlashingCellOptions.d.ts +3 -3
  7. package/src/AdaptableOptions/SettingsPanelOptions.d.ts +6 -4
  8. package/src/AdaptableOptions/StateOptions.d.ts +37 -13
  9. package/src/AdaptableOptions/UserInterfaceOptions.d.ts +4 -6
  10. package/src/AdaptableState/AdaptableState.d.ts +2 -0
  11. package/src/AdaptableState/QuickSearchState.d.ts +4 -0
  12. package/src/AdaptableState/StyledColumns/GradientStyle.d.ts +4 -0
  13. package/src/Api/Implementation/ColumnScopeApiImpl.js +3 -3
  14. package/src/Api/Implementation/QuickSearchApiImpl.js +2 -2
  15. package/src/Api/Implementation/StateApiImpl.d.ts +1 -1
  16. package/src/Api/Implementation/StateApiImpl.js +4 -3
  17. package/src/Api/Implementation/UserInterfaceApiImpl.d.ts +0 -1
  18. package/src/Api/Implementation/UserInterfaceApiImpl.js +9 -15
  19. package/src/Api/Internal/AdaptableInternalApi.js +5 -3
  20. package/src/Api/Internal/CalculatedColumnInternalApi.js +1 -1
  21. package/src/Api/Internal/StateInternalApi.d.ts +1 -1
  22. package/src/Api/Internal/StateInternalApi.js +6 -5
  23. package/src/Api/StateApi.d.ts +2 -2
  24. package/src/Api/UserInterfaceApi.d.ts +0 -4
  25. package/src/Redux/ActionsReducers/QuickSearchRedux.d.ts +9 -4
  26. package/src/Redux/ActionsReducers/QuickSearchRedux.js +12 -7
  27. package/src/Redux/Store/AdaptableReduxLocalStorageEngine.d.ts +8 -0
  28. package/src/Redux/Store/AdaptableReduxLocalStorageEngine.js +12 -12
  29. package/src/Redux/Store/AdaptableStore.d.ts +8 -5
  30. package/src/Redux/Store/AdaptableStore.js +51 -21
  31. package/src/Redux/Store/Interface/IAdaptableStore.d.ts +5 -5
  32. package/src/Redux/Store/Interface/IStorageEngine.d.ts +4 -4
  33. package/src/Redux/Store/StateOptionsGuards.d.ts +58 -0
  34. package/src/Redux/Store/StateOptionsGuards.js +112 -0
  35. package/src/Redux/Store/isTransientReduxAction.d.ts +5 -0
  36. package/src/Redux/Store/isTransientReduxAction.js +19 -0
  37. package/src/Utilities/Constants/DocumentationLinkConstants.d.ts +1 -0
  38. package/src/Utilities/Constants/DocumentationLinkConstants.js +1 -0
  39. package/src/Utilities/Constants/ReduxConstants.d.ts +2 -2
  40. package/src/Utilities/Constants/ReduxConstants.js +5 -5
  41. package/src/Utilities/Helpers/AlertTypeHelper.d.ts +1 -1
  42. package/src/Utilities/Helpers/SettingsPanelHelper.d.ts +14 -0
  43. package/src/Utilities/Helpers/SettingsPanelHelper.js +37 -1
  44. package/src/Utilities/Helpers/UIHelper.d.ts +12 -14
  45. package/src/Utilities/Helpers/UIHelper.js +12 -35
  46. package/src/Utilities/ObjectFactory.js +3 -3
  47. package/src/View/Components/Popups/AdaptablePopup/Navigation.js +9 -11
  48. package/src/View/Components/StyleComponent.js +2 -2
  49. package/src/View/Dashboard/Dashboard.d.ts +1 -1
  50. package/src/View/Dashboard/Dashboard.js +1 -1
  51. package/src/View/Dashboard/DashboardPopupSections.js +1 -1
  52. package/src/View/GridInfo/GridInfoPopup.js +62 -25
  53. package/src/View/QuickSearch/QuickSearchInput.js +1 -1
  54. package/src/View/QuickSearch/QuickSearchPopup.d.ts +1 -1
  55. package/src/View/QuickSearch/QuickSearchPopup.js +1 -1
  56. package/src/View/QuickSearch/Utilities/useQuickSearchDebounced.d.ts +2 -2
  57. package/src/View/StyledColumn/RangesComponent.js +13 -16
  58. package/src/View/StyledColumn/Wizard/BadgePillStyleEditor.d.ts +0 -2
  59. package/src/View/StyledColumn/Wizard/BadgePillStyleEditor.js +2 -2
  60. package/src/View/StyledColumn/Wizard/StyledColumnBadgeSection.js +2 -3
  61. package/src/View/StyledColumn/Wizard/StyledColumnSliceStyleEditors.d.ts +0 -3
  62. package/src/View/StyledColumn/Wizard/StyledColumnSliceStyleEditors.js +3 -3
  63. package/src/View/StyledColumn/Wizard/StyledColumnSparklineSettingsSection.js +7 -11
  64. package/src/View/StyledColumn/Wizard/StyledColumnWizardBulletSection.js +6 -6
  65. package/src/View/StyledColumn/Wizard/StyledColumnWizardGradientSection.js +2 -2
  66. package/src/View/StyledColumn/Wizard/StyledColumnWizardIconSection.js +2 -2
  67. package/src/View/StyledColumn/Wizard/StyledColumnWizardRangeBarSection.js +8 -8
  68. package/src/View/StyledColumn/Wizard/StyledColumnWizardRatingSection.js +4 -4
  69. package/src/View/StyledColumn/Wizard/StyledColumnWizardStyleSection/StyledColumnWizardRangesSection.js +2 -2
  70. package/src/View/StyledColumn/Wizard/StyledColumnWizardStyleSection/StyledColumnWizardStyleSection.js +4 -4
  71. package/src/View/Wizard/CollapsibleWizardCard.d.ts +3 -1
  72. package/src/View/Wizard/CollapsibleWizardCard.js +17 -9
  73. package/src/agGrid/AdaptableAgGrid.d.ts +4 -4
  74. package/src/agGrid/AdaptableAgGrid.js +2 -0
  75. package/src/agGrid/AgGridColumnAdapter.d.ts +1 -1
  76. package/src/agGrid/AgGridColumnAdapter.js +4 -4
  77. package/src/components/ColorField/index.d.ts +14 -0
  78. package/src/components/ColorField/index.js +6 -0
  79. package/src/components/aql/QueryBuilder/QueryBuilderInputs.js +1 -1
  80. package/src/env.js +2 -2
  81. package/src/layout-manager/src/normalizeLayoutModel.d.ts +1 -1
  82. package/src/layout-manager/src/simplifyLayoutModel.d.ts +1 -1
  83. package/src/migration/AdaptableUpgradeHelper.d.ts +7 -2
  84. package/src/migration/VersionUpgrade17.js +13 -8
  85. package/src/migration/VersionUpgrade24.d.ts +9 -0
  86. package/src/migration/VersionUpgrade24.js +27 -0
  87. package/src/setupDevTools.js +605 -112
  88. package/src/types.d.ts +1 -1
  89. package/tsconfig.esm.tsbuildinfo +1 -1
  90. package/vendor/components/Button/index.d.ts +1 -1
  91. package/vendor/components/Button/index.d.ts.map +1 -1
  92. package/vendor/components/Button/index.js.map +1 -1
  93. package/vendor/components/CheckBox/index.d.ts +1 -1
  94. package/vendor/components/CheckBox/index.d.ts.map +1 -1
  95. package/vendor/components/CheckBox/index.js.map +1 -1
  96. package/vendor/components/ColorField/index.d.ts +43 -0
  97. package/vendor/components/ColorField/index.d.ts.map +1 -0
  98. package/vendor/components/ColorField/index.js +81 -0
  99. package/vendor/components/ColorField/index.js.map +1 -0
  100. package/vendor/components/ColorPicker/colorPickerTab.d.ts +4 -0
  101. package/vendor/components/ColorPicker/colorPickerTab.d.ts.map +1 -0
  102. package/vendor/components/ColorPicker/colorPickerTab.js +28 -0
  103. package/vendor/components/ColorPicker/colorPickerTab.js.map +1 -0
  104. package/vendor/components/ColorPicker/index.d.ts +59 -0
  105. package/vendor/components/ColorPicker/index.d.ts.map +1 -0
  106. package/vendor/components/ColorPicker/index.js +204 -0
  107. package/vendor/components/ColorPicker/index.js.map +1 -0
  108. package/vendor/components/ColorPicker/themeColors.d.ts +53 -0
  109. package/vendor/components/ColorPicker/themeColors.d.ts.map +1 -0
  110. package/vendor/components/ColorPicker/themeColors.js +110 -0
  111. package/vendor/components/ColorPicker/themeColors.js.map +1 -0
  112. package/vendor/components/Combobox/VirtualizedList.d.ts +1 -1
  113. package/vendor/components/Combobox/VirtualizedList.d.ts.map +1 -1
  114. package/vendor/components/Combobox/VirtualizedList.js.map +1 -1
  115. package/vendor/components/Combobox/index.d.ts +1 -1
  116. package/vendor/components/Combobox/index.d.ts.map +1 -1
  117. package/vendor/components/Combobox/index.js.map +1 -1
  118. package/vendor/components/NumberInput/index.d.ts +51 -0
  119. package/vendor/components/NumberInput/index.d.ts.map +1 -0
  120. package/vendor/components/NumberInput/index.js +82 -0
  121. package/vendor/components/NumberInput/index.js.map +1 -0
  122. package/vendor/components/index.css +280 -24
  123. package/vendor/components/index.d.ts +6 -0
  124. package/vendor/components/index.d.ts.map +1 -1
  125. package/vendor/components/index.js +3 -0
  126. package/vendor/components/index.js.map +1 -1
  127. package/vendor/components/ui/combobox.d.ts.map +1 -1
  128. package/vendor/components/ui/combobox.js +1 -1
  129. package/vendor/components/ui/combobox.js.map +1 -1
  130. package/vendor/components/ui/input-group.d.ts.map +1 -1
  131. package/vendor/components/ui/input-group.js +5 -4
  132. package/vendor/components/ui/input-group.js.map +1 -1
  133. package/src/components/ColorPicker/ColorPicker.d.ts +0 -12
  134. package/src/components/ColorPicker/ColorPicker.js +0 -48
  135. package/src/components/ColorPicker/OptionalColorPicker.d.ts +0 -12
  136. package/src/components/ColorPicker/OptionalColorPicker.js +0 -13
  137. package/src/components/ColorPicker/index.d.ts +0 -2
  138. package/src/components/ColorPicker/index.js +0 -2
@@ -1,6 +1,14 @@
1
1
  import IStorageEngine from './Interface/IStorageEngine';
2
2
  import { InitialState } from '../../AdaptableState/InitialState';
3
3
  import { AdaptableLoadStateFunction, AdaptablePersistStateFunction } from '../../AdaptableOptions/StateOptions';
4
+ /**
5
+ * Default `persistState` implementation - writes the state to localStorage under `adaptableStateKey`
6
+ */
7
+ export declare const persistState: AdaptablePersistStateFunction;
8
+ /**
9
+ * Default `loadState` implementation - reads the state from localStorage under `adaptableStateKey`
10
+ */
11
+ export declare const loadState: AdaptableLoadStateFunction;
4
12
  export declare function createEngine(config: {
5
13
  adaptableId: string;
6
14
  adaptableStateKey: string;
@@ -1,6 +1,7 @@
1
1
  import { debounce } from '../../Utilities/Helpers/TimingHelper';
2
2
  import { MergeStateFunction } from './AdaptableReduxMerger';
3
3
  import { AdaptableLogger } from '../../agGrid/AdaptableLogger';
4
+ import { runAsyncStateFunction, runSyncStateFunction } from './StateOptionsGuards';
4
5
  const checkStatus = (response) => {
5
6
  const error = new Error(response.statusText);
6
7
  if (response.status >= 200 && response.status < 300) {
@@ -8,7 +9,7 @@ const checkStatus = (response) => {
8
9
  }
9
10
  throw error;
10
11
  };
11
- const persistState = (state, config) => {
12
+ export const persistState = (state, config) => {
12
13
  return new Promise((resolve, reject) => {
13
14
  try {
14
15
  localStorage.setItem(config.adaptableStateKey, JSON.stringify(state));
@@ -19,7 +20,7 @@ const persistState = (state, config) => {
19
20
  }
20
21
  });
21
22
  };
22
- const loadState = ({ adaptableId, adaptableStateKey }) => {
23
+ export const loadState = ({ adaptableStateKey }) => {
23
24
  const jsonState = localStorage.getItem(adaptableStateKey);
24
25
  const parsedJsonState = JSON.parse(jsonState) || {};
25
26
  return Promise.resolve(parsedJsonState);
@@ -49,7 +50,7 @@ class AdaptableReduxLocalStorageEngine {
49
50
  this.adaptableStateKey = adaptableStateKey;
50
51
  }
51
52
  load(adaptableInstance, initialState = this.initialState) {
52
- return (this.loadState || loadState)(adaptableInstance.api.stateApi.internalApi.buildAdaptableStateFunctionConfig()).then((parsedJsonState) => {
53
+ return runAsyncStateFunction('loadState', this.loadState || loadState, [adaptableInstance.api.stateApi.internalApi.buildAdaptableStateFunctionConfig()], adaptableInstance.logger).then((parsedJsonState) => {
53
54
  return Promise.resolve(initialState)
54
55
  .then((parsedInitialState) => {
55
56
  return MergeStateFunction(parsedInitialState, parsedJsonState);
@@ -67,20 +68,19 @@ class AdaptableReduxLocalStorageEngine {
67
68
  const config = adaptableInstance.api.stateApi.internalApi.buildAdaptableStateFunctionConfig();
68
69
  config.actionName = actionName;
69
70
  config.previousState = adaptableInstance.adaptableStore.getPreviousStorageState() || null;
70
- let stateObject = structuredClone(state);
71
- const getTransformedState = adaptableInstance.adaptableOptions.stateOptions.saveState;
72
- if (getTransformedState) {
73
- stateObject = getTransformedState(stateObject, config);
71
+ let stateObject;
72
+ try {
73
+ stateObject = runSyncStateFunction('saveState', adaptableInstance.adaptableOptions.stateOptions.saveState, structuredClone(state), config, adaptableInstance.logger);
74
74
  }
75
- const promise = (this.persistState || persistState)(stateObject, config)?.catch(rejectWithMessage);
76
- if (!(promise instanceof Promise)) {
77
- AdaptableLogger.consoleWarnBase(`AdaptableId: ${this.adaptableId}`, 'stateOptions.persistState should return a promise, it returned', 'Error', promise);
75
+ catch (error) {
76
+ adaptableInstance.logger.consoleError('Adaptable State was NOT persisted:', error instanceof Error ? error.message : error);
77
+ return Promise.reject(error);
78
78
  }
79
- return promise;
79
+ return runAsyncStateFunction('persistState', this.persistState || persistState, [stateObject, config], adaptableInstance.logger).catch(rejectWithMessage);
80
80
  }
81
81
  }
82
82
  function rejectWithMessage(error) {
83
- return Promise.reject(error.message);
83
+ return Promise.reject(error?.message ?? error);
84
84
  }
85
85
  export function createEngine(config) {
86
86
  return new AdaptableReduxLocalStorageEngine(config);
@@ -1,11 +1,14 @@
1
1
  import * as Redux from 'redux';
2
2
  import { IAdaptable } from '../../AdaptableInterfaces/IAdaptable';
3
- import { AdaptableState } from '../../AdaptableState/AdaptableState';
3
+ import { AdaptablePersistentState, AdaptableState } from '../../AdaptableState/AdaptableState';
4
4
  import { BaseState } from '../../AdaptableState/BaseState';
5
5
  import { IAdaptableStore, LoadStoreConfig } from './Interface/IAdaptableStore';
6
6
  type EmitterCallback = (data?: any) => any;
7
7
  type EmitterAnyCallback = (eventName: string, data?: any) => any;
8
- export declare const getPersistableState: (state: AdaptableState) => AdaptableState;
8
+ /**
9
+ * Strips the transient (non-persisted) sections from the given state object - mutates and returns the same object.
10
+ */
11
+ export declare const getPersistableState: (state: AdaptableState) => AdaptablePersistentState;
9
12
  export interface ResetUserDataAction extends Redux.Action {
10
13
  }
11
14
  export interface InitStateAction extends Redux.Action {
@@ -40,9 +43,9 @@ export declare class AdaptableStore implements IAdaptableStore {
40
43
  */
41
44
  constructor(adaptable: IAdaptable);
42
45
  destroy(): void;
43
- getCurrentStorageState(): AdaptableState;
44
- getPreviousStorageState(): AdaptableState;
45
- saveStateNow(adaptable: IAdaptable, state?: AdaptableState): Promise<any>;
46
+ getCurrentStorageState(): AdaptablePersistentState;
47
+ getPreviousStorageState(): AdaptablePersistentState;
48
+ saveStateNow(adaptable: IAdaptable, state?: AdaptablePersistentState): Promise<any>;
46
49
  loadStore: (config: LoadStoreConfig) => Promise<any>;
47
50
  }
48
51
  export {};
@@ -4,6 +4,7 @@ import * as PopupRedux from '../ActionsReducers/PopupRedux';
4
4
  import { PROGRESS_INDICATOR_HIDE, PROGRESS_INDICATOR_SHOW } from '../ActionsReducers/PopupRedux';
5
5
  import { createEngine as createEngineLocal } from './AdaptableReduxLocalStorageEngine';
6
6
  import { mergeReducer } from './AdaptableReduxMerger';
7
+ import { runSyncStateFunction } from './StateOptionsGuards';
7
8
  import { isRowChangeAlertDefinition } from '../../AdaptableState/AlertState';
8
9
  import { getAlertCellDataChangedInfos, isAdaptableRowChangedAlert, } from '../../AdaptableState/Common/AdaptableAlert';
9
10
  import { closeAgChartDefinitionIfOpen, closeExternalChartDefinitionIfOpen, } from '../../Utilities/Helpers/ChartHelper';
@@ -185,13 +186,9 @@ export class AdaptableStore {
185
186
  !init &&
186
187
  !this.loadStorageInProgress;
187
188
  if (shouldPersist) {
188
- let storageState = { ...finalState };
189
- storageState = getPersistableState(storageState);
189
+ const storageState = getPersistableState({ ...finalState });
190
190
  this.currentStorageState = storageState;
191
- this.previousStorageState = { ...state };
192
- NON_PERSISTENT_STORE_KEYS.forEach((key) => {
193
- delete this.previousStorageState[key];
194
- });
191
+ this.previousStorageState = getPersistableState({ ...state });
195
192
  storageEngine.save(adaptable, storageState, action.type);
196
193
  }
197
194
  return finalState;
@@ -245,7 +242,9 @@ export class AdaptableStore {
245
242
  return this.previousStorageState;
246
243
  }
247
244
  saveStateNow(adaptable, state) {
248
- const newState = state ?? this.getCurrentStorageState();
245
+ const newState = state
246
+ ? getPersistableState({ ...state })
247
+ : this.getCurrentStorageState();
249
248
  if (newState) {
250
249
  return this.storageEngine.saveNow(adaptable, newState, 'API_CALL_SAVE_STATE');
251
250
  }
@@ -260,7 +259,8 @@ export class AdaptableStore {
260
259
  .load(adaptable, initialState ?? adaptable.adaptableOptions.initialState)
261
260
  .then((storedState) => {
262
261
  if (storedState && this.loadStateOnStartup) {
263
- this.TheStore.dispatch(LoadState(postProcessState(adaptable.adaptableOptions.stateOptions.applyState(storedState, adaptable.api.stateApi.internalApi.buildAdaptableStateFunctionConfig()))));
262
+ const appliedState = runSyncStateFunction('applyState', adaptable.adaptableOptions.stateOptions.applyState, storedState, adaptable.api.stateApi.internalApi.buildAdaptableStateFunctionConfig(), adaptable.logger);
263
+ this.TheStore.dispatch(LoadState(postProcessState(appliedState)));
264
264
  }
265
265
  })
266
266
  .then(() => {
@@ -402,10 +402,26 @@ function refreshFormulaTraceCells(adaptable, formulaTraceInfo) {
402
402
  });
403
403
  }
404
404
  const LAYOUT_EXTENSION_OBJECT_ACTIONS = {
405
- [AlertRedux.ALERT_DEFINITION_EDIT]: { module: 'Alert', payloadKey: 'alertDefinition', kind: 'edit' },
406
- [AlertRedux.ALERT_DEFINITION_DELETE]: { module: 'Alert', payloadKey: 'alertDefinition', kind: 'delete' },
407
- [CustomSortRedux.CUSTOM_SORT_EDIT]: { module: 'CustomSort', payloadKey: 'customSort', kind: 'edit' },
408
- [CustomSortRedux.CUSTOM_SORT_DELETE]: { module: 'CustomSort', payloadKey: 'customSort', kind: 'delete' },
405
+ [AlertRedux.ALERT_DEFINITION_EDIT]: {
406
+ module: 'Alert',
407
+ payloadKey: 'alertDefinition',
408
+ kind: 'edit',
409
+ },
410
+ [AlertRedux.ALERT_DEFINITION_DELETE]: {
411
+ module: 'Alert',
412
+ payloadKey: 'alertDefinition',
413
+ kind: 'delete',
414
+ },
415
+ [CustomSortRedux.CUSTOM_SORT_EDIT]: {
416
+ module: 'CustomSort',
417
+ payloadKey: 'customSort',
418
+ kind: 'edit',
419
+ },
420
+ [CustomSortRedux.CUSTOM_SORT_DELETE]: {
421
+ module: 'CustomSort',
422
+ payloadKey: 'customSort',
423
+ kind: 'delete',
424
+ },
409
425
  [FlashingCellRedux.FLASHING_CELL_DEFINITION_EDIT]: {
410
426
  module: 'FlashingCell',
411
427
  payloadKey: 'flashingCellDefinition',
@@ -416,13 +432,21 @@ const LAYOUT_EXTENSION_OBJECT_ACTIONS = {
416
432
  payloadKey: 'flashingCellDefinition',
417
433
  kind: 'delete',
418
434
  },
419
- [FormatColumnRedux.FORMAT_COLUMN_EDIT]: { module: 'FormatColumn', payloadKey: 'formatColumn', kind: 'edit' },
435
+ [FormatColumnRedux.FORMAT_COLUMN_EDIT]: {
436
+ module: 'FormatColumn',
437
+ payloadKey: 'formatColumn',
438
+ kind: 'edit',
439
+ },
420
440
  [FormatColumnRedux.FORMAT_COLUMN_DELETE]: {
421
441
  module: 'FormatColumn',
422
442
  payloadKey: 'formatColumn',
423
443
  kind: 'delete',
424
444
  },
425
- [PlusMinusRedux.PLUS_MINUS_RULE_EDIT]: { module: 'PlusMinus', payloadKey: 'plusMinusNudge', kind: 'edit' },
445
+ [PlusMinusRedux.PLUS_MINUS_RULE_EDIT]: {
446
+ module: 'PlusMinus',
447
+ payloadKey: 'plusMinusNudge',
448
+ kind: 'edit',
449
+ },
426
450
  [PlusMinusRedux.PLUS_MINUS_RULE_DELETE]: {
427
451
  module: 'PlusMinus',
428
452
  payloadKey: 'plusMinusNudge',
@@ -430,7 +454,11 @@ const LAYOUT_EXTENSION_OBJECT_ACTIONS = {
430
454
  },
431
455
  [ShortcutRedux.SHORTCUT_EDIT]: { module: 'Shortcut', payloadKey: 'shortcut', kind: 'edit' },
432
456
  [ShortcutRedux.SHORTCUT_DELETE]: { module: 'Shortcut', payloadKey: 'shortcut', kind: 'delete' },
433
- [StyledColumnRedux.STYLED_COLUMN_EDIT]: { module: 'StyledColumn', payloadKey: 'StyledColumn', kind: 'edit' },
457
+ [StyledColumnRedux.STYLED_COLUMN_EDIT]: {
458
+ module: 'StyledColumn',
459
+ payloadKey: 'StyledColumn',
460
+ kind: 'edit',
461
+ },
434
462
  [StyledColumnRedux.STYLED_COLUMN_DELETE]: {
435
463
  module: 'StyledColumn',
436
464
  payloadKey: 'StyledColumn',
@@ -585,8 +613,8 @@ const adaptableMiddleware = (adaptable) => (function(middlewareAPI) {
585
613
  if (!adaptable.api.freeTextColumnApi.internalApi.validateDeletedFreeTextColumn(actionTyped.freeTextColumn)) {
586
614
  return;
587
615
  }
588
- if (adaptable.api.optionsApi.getLayoutOptions()
589
- ?.autoRemoveDeletedColumnsFromLayouts !== false) {
616
+ if (adaptable.api.optionsApi.getLayoutOptions()?.autoRemoveDeletedColumnsFromLayouts !==
617
+ false) {
590
618
  adaptable.api.layoutApi.internalApi.removeColumnIdFromAllLayouts(actionTyped.freeTextColumn.ColumnId);
591
619
  }
592
620
  }
@@ -607,8 +635,8 @@ const adaptableMiddleware = (adaptable) => (function(middlewareAPI) {
607
635
  if (!adaptable.api.calculatedColumnApi.internalApi.validateDeletedCalculatedColumn(calculatedColumn)) {
608
636
  return;
609
637
  }
610
- if (adaptable.api.optionsApi.getLayoutOptions()
611
- ?.autoRemoveDeletedColumnsFromLayouts !== false) {
638
+ if (adaptable.api.optionsApi.getLayoutOptions()?.autoRemoveDeletedColumnsFromLayouts !==
639
+ false) {
612
640
  adaptable.api.layoutApi.internalApi.removeColumnIdFromAllLayouts(calculatedColumn.ColumnId);
613
641
  }
614
642
  }
@@ -692,10 +720,12 @@ const adaptableMiddleware = (adaptable) => (function(middlewareAPI) {
692
720
  adaptable.refreshSortIfColumnsSorted();
693
721
  return returnAction;
694
722
  }
723
+ case QuickSearchRedux.QUICK_SEARCH_CLEAR:
695
724
  case QuickSearchRedux.QUICK_SEARCH_RUN: {
696
725
  let returnAction = next(action);
697
- const actionTyped = action;
698
- const searchText = actionTyped.quickSearchText;
726
+ const searchText = action.type === QuickSearchRedux.QUICK_SEARCH_CLEAR
727
+ ? ''
728
+ : action.quickSearchText;
699
729
  adaptable.setAgGridFindSearchValue(searchText);
700
730
  adaptable.api.gridApi.refreshAllCells(true);
701
731
  if (adaptable.adaptableOptions.quickSearchOptions.filterGridAfterQuickSearch) {
@@ -1,20 +1,20 @@
1
1
  import * as Redux from 'redux';
2
- import { AdaptableState } from '../../../AdaptableState/AdaptableState';
2
+ import { AdaptablePersistentState, AdaptableState } from '../../../AdaptableState/AdaptableState';
3
3
  import { InitialState } from '../../../types';
4
4
  import { IAdaptable } from '../../../AdaptableInterfaces/IAdaptable';
5
5
  export interface LoadStoreConfig {
6
6
  adaptable: IAdaptable;
7
7
  adaptableStateKey: string;
8
8
  initialState?: InitialState;
9
- postLoadHook?: (state: AdaptableState) => AdaptableState;
9
+ postLoadHook?: (state: Partial<AdaptablePersistentState>) => Partial<AdaptablePersistentState>;
10
10
  }
11
11
  export interface IAdaptableStore {
12
12
  TheStore: Redux.Store<AdaptableState>;
13
13
  Load: Promise<any>;
14
14
  loadStore: (config: LoadStoreConfig) => Promise<any>;
15
- getCurrentStorageState: () => AdaptableState | void;
16
- getPreviousStorageState: () => AdaptableState | void;
17
- saveStateNow: (adaptable: IAdaptable, state?: AdaptableState) => Promise<any>;
15
+ getCurrentStorageState: () => AdaptablePersistentState | void;
16
+ getPreviousStorageState: () => AdaptablePersistentState | void;
17
+ saveStateNow: (adaptable: IAdaptable, state?: AdaptablePersistentState) => Promise<any>;
18
18
  on: (eventName: string, callback: (data?: any) => any) => () => void;
19
19
  onAny: (callback: (eventName: string, data?: any) => any) => () => void;
20
20
  onBeforeAny: (callback: (eventName: string, data?: any) => any) => () => void;
@@ -1,9 +1,9 @@
1
- import { AdaptableState } from '../../../AdaptableState/AdaptableState';
1
+ import { AdaptablePersistentState } from '../../../AdaptableState/AdaptableState';
2
2
  import { InitialState } from '../../../AdaptableState/InitialState';
3
3
  import { IAdaptable } from '../../../AdaptableInterfaces/IAdaptable';
4
4
  export default interface IStorageEngine {
5
- load(adaptable: IAdaptable, initialState?: string | InitialState): Promise<any>;
6
- save(adaptable: IAdaptable, state: AdaptableState, actionName: string): Promise<any>;
7
- saveNow(adaptable: IAdaptable, state: AdaptableState, actionName: string): Promise<any>;
5
+ load(adaptable: IAdaptable, initialState?: string | InitialState): Promise<Partial<AdaptablePersistentState> | void>;
6
+ save(adaptable: IAdaptable, state: AdaptablePersistentState, actionName: string): Promise<any>;
7
+ saveNow(adaptable: IAdaptable, state: AdaptablePersistentState, actionName: string): Promise<any>;
8
8
  setStateKey(stateKey: string): void;
9
9
  }
@@ -0,0 +1,58 @@
1
+ import { AdaptablePersistentState } from '../../AdaptableState/AdaptableState';
2
+ import { AdaptableStateFunctionConfig, StateOptions } from '../../AdaptableOptions/StateOptions';
3
+ /**
4
+ * Runtime guards for the `StateOptions` functions.
5
+ *
6
+ * The most frequent mistakes when implementing these functions are:
7
+ * - providing an `async` function (or otherwise returning a Promise) for `saveState` / `applyState`, which are synchronous
8
+ * - mutating the state in `saveState` / `applyState` but forgetting to return it
9
+ * - returning a plain value (not a Promise) from `loadState` / `persistState`
10
+ *
11
+ * The helpers below detect these cases - as early as possible - and either recover (when there is only one sensible
12
+ * interpretation) or fail with an actionable error message instead of an obscure one further down the line.
13
+ */
14
+ export type StateOptionsLogger = {
15
+ consoleError(message: string, ...optionalParams: any[]): void;
16
+ consoleWarn(message: string, ...optionalParams: any[]): void;
17
+ };
18
+ export type SyncStateFunctionName = 'saveState' | 'applyState';
19
+ export type AsyncStateFunctionName = 'loadState' | 'persistState' | 'clearState';
20
+ export type StateFunctionName = SyncStateFunctionName | AsyncStateFunctionName;
21
+ /**
22
+ * Error thrown when a `StateOptions` function breaks its contract
23
+ */
24
+ export declare class AdaptableStateOptionsError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ export declare const isThenable: (value: unknown) => value is PromiseLike<unknown>;
28
+ /**
29
+ * Detects functions declared with the `async` keyword (not transpiled to generators / promises)
30
+ */
31
+ export declare const isAsyncFunction: (fn: unknown) => boolean;
32
+ /**
33
+ * Validates the shape of `stateOptions` once, at initialisation - before any state is loaded or persisted.
34
+ *
35
+ * Reports (does not throw):
36
+ * - non-function values for any state function
37
+ * - `async` functions for `saveState` / `applyState`
38
+ * - `loadState` without `persistState` (or vice versa) - the localStorage fallback is used for the missing one,
39
+ * which makes the remote and local copies of the state diverge
40
+ */
41
+ export declare function validateStateOptions(stateOptions: StateOptions | undefined, logger: StateOptionsLogger): void;
42
+ /**
43
+ * Invokes a synchronous state transform (`saveState` / `applyState`) and validates its result.
44
+ *
45
+ * - returns the result when it is a state object
46
+ * - **recovers** when the function returned nothing: the input state is used (the function most likely mutated it in place
47
+ * and forgot to `return` it) and an error is logged
48
+ * - **throws** `AdaptableStateOptionsError` when the result is a Promise (async function) or any other non-object value;
49
+ * there is no safe way to continue in these cases
50
+ */
51
+ export declare function runSyncStateFunction<T extends Partial<AdaptablePersistentState>>(functionName: SyncStateFunctionName, fn: ((state: T, config: AdaptableStateFunctionConfig) => T) | undefined, state: T, config: AdaptableStateFunctionConfig, logger: StateOptionsLogger): T;
52
+ /**
53
+ * Invokes an asynchronous state function (`loadState` / `persistState` / `clearState`) and normalises its result to a Promise.
54
+ *
55
+ * A non-Promise result is logged (these functions are expected to be async) and wrapped in a resolved Promise, so a
56
+ * synchronous implementation degrades gracefully instead of failing with `.then is not a function`.
57
+ */
58
+ export declare function runAsyncStateFunction<TResult, TArgs extends unknown[]>(functionName: AsyncStateFunctionName, fn: (...args: TArgs) => Promise<TResult>, args: TArgs, logger: StateOptionsLogger): Promise<TResult>;
@@ -0,0 +1,112 @@
1
+ import { StatePersistenceDocsLink } from '../../Utilities/Constants/DocumentationLinkConstants';
2
+ const STATE_FUNCTION_NAMES = [
3
+ 'loadState',
4
+ 'persistState',
5
+ 'applyState',
6
+ 'saveState',
7
+ 'clearState',
8
+ ];
9
+ export class AdaptableStateOptionsError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'AdaptableStateOptionsError';
13
+ }
14
+ }
15
+ export const isThenable = (value) => {
16
+ return value != null && typeof value.then === 'function';
17
+ };
18
+ export const isAsyncFunction = (fn) => {
19
+ return typeof fn === 'function' && fn.constructor?.name === 'AsyncFunction';
20
+ };
21
+ const isStateObject = (value) => {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ };
24
+ const docs = `See ${StatePersistenceDocsLink}`;
25
+ const syncFunctionMustReturnMessage = (functionName) => `stateOptions.${functionName} must be synchronous and return the (modified) state object.`;
26
+ const asyncFunctionMessage = (functionName, reason) => `${syncFunctionMustReturnMessage(functionName)} ${reason} ` +
27
+ `Asynchronous work belongs in stateOptions.${functionName === 'saveState' ? 'persistState' : 'loadState'}. ${docs}`;
28
+ export function validateStateOptions(stateOptions, logger) {
29
+ if (!stateOptions) {
30
+ return;
31
+ }
32
+ for (const functionName of STATE_FUNCTION_NAMES) {
33
+ const fn = stateOptions[functionName];
34
+ if (fn != null && typeof fn !== 'function') {
35
+ logger.consoleError(`stateOptions.${functionName} must be a function, but received ${describe(fn)}. ${docs}`);
36
+ }
37
+ }
38
+ for (const functionName of ['saveState', 'applyState']) {
39
+ if (isAsyncFunction(stateOptions[functionName])) {
40
+ logger.consoleError(asyncFunctionMessage(functionName, 'It is declared as an async function.'));
41
+ }
42
+ }
43
+ const hasLoadState = typeof stateOptions.loadState === 'function';
44
+ const hasPersistState = typeof stateOptions.persistState === 'function';
45
+ if (hasLoadState !== hasPersistState) {
46
+ const provided = hasLoadState ? 'loadState' : 'persistState';
47
+ const missing = hasLoadState ? 'persistState' : 'loadState';
48
+ logger.consoleWarn(`stateOptions.${provided} is provided without stateOptions.${missing}; ` +
49
+ `the default localStorage implementation is used for ${missing}, so the loaded and the persisted state may diverge. ` +
50
+ `Provide both functions (or neither). ${docs}`);
51
+ }
52
+ }
53
+ export function runSyncStateFunction(functionName, fn, state, config, logger) {
54
+ if (fn == null) {
55
+ return state;
56
+ }
57
+ if (typeof fn !== 'function') {
58
+ throw new AdaptableStateOptionsError(`stateOptions.${functionName} must be a function, but received ${describe(fn)}. ${docs}`);
59
+ }
60
+ const result = fn(state, config);
61
+ if (result === undefined) {
62
+ logger.consoleError(`${syncFunctionMustReturnMessage(functionName)} It returned nothing - ` +
63
+ `the state object passed to it is used instead (add 'return state;' to fix this). ${docs}`);
64
+ return state;
65
+ }
66
+ if (isThenable(result)) {
67
+ throw new AdaptableStateOptionsError(asyncFunctionMessage(functionName, 'It returned a Promise (is it an async function?).'));
68
+ }
69
+ if (!isStateObject(result)) {
70
+ throw new AdaptableStateOptionsError(`${syncFunctionMustReturnMessage(functionName)} It returned ${describe(result)}. ${docs}`);
71
+ }
72
+ return result;
73
+ }
74
+ export function runAsyncStateFunction(functionName, fn, args, logger) {
75
+ if (typeof fn !== 'function') {
76
+ return Promise.reject(new AdaptableStateOptionsError(`stateOptions.${functionName} must be a function, but received ${describe(fn)}. ${docs}`));
77
+ }
78
+ let result;
79
+ try {
80
+ result = fn(...args);
81
+ }
82
+ catch (error) {
83
+ return Promise.reject(error);
84
+ }
85
+ if (!isThenable(result)) {
86
+ logger.consoleWarn(`stateOptions.${functionName} should be asynchronous and return a Promise, but it returned ${describe(result)}. The value is used as if it had been resolved. ${docs}`);
87
+ return Promise.resolve(result);
88
+ }
89
+ return Promise.resolve(result);
90
+ }
91
+ function describe(value) {
92
+ if (value === null) {
93
+ return 'null';
94
+ }
95
+ if (value === undefined) {
96
+ return 'undefined';
97
+ }
98
+ if (Array.isArray(value)) {
99
+ return 'an array';
100
+ }
101
+ if (isThenable(value)) {
102
+ return 'a Promise';
103
+ }
104
+ const type = typeof value;
105
+ if (type === 'object') {
106
+ return 'an object';
107
+ }
108
+ if (type === 'string') {
109
+ return `a string ('${String(value).slice(0, 40)}${String(value).length > 40 ? '…' : ''}')`;
110
+ }
111
+ return `${type} (${String(value)})`;
112
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Whether a Redux action only affects transient (non-persisted) Adaptable state,
3
+ * e.g. showing a popup, highlighting cells or setting the selected rows.
4
+ */
5
+ export declare function isTransientReduxAction(actionType: string | undefined): boolean;
@@ -0,0 +1,19 @@
1
+ import * as CommentsRedux from '../ActionsReducers/CommentsRedux';
2
+ import * as InternalRedux from '../ActionsReducers/InternalRedux';
3
+ import * as PluginsRedux from '../ActionsReducers/PluginsRedux';
4
+ import * as PopupRedux from '../ActionsReducers/PopupRedux';
5
+ const collectActionTypes = (reduxModule) => {
6
+ return Object.values(reduxModule).filter((value) => typeof value === 'string');
7
+ };
8
+ const TRANSIENT_REDUX_ACTION_TYPES = new Set([
9
+ ...collectActionTypes(InternalRedux),
10
+ ...collectActionTypes(PopupRedux),
11
+ ...collectActionTypes(PluginsRedux),
12
+ ...collectActionTypes(CommentsRedux),
13
+ ]);
14
+ export function isTransientReduxAction(actionType) {
15
+ if (!actionType) {
16
+ return false;
17
+ }
18
+ return TRANSIENT_REDUX_ACTION_TYPES.has(actionType);
19
+ }
@@ -16,3 +16,4 @@ export declare const AgGridModulesDocsLink = "https://www.adaptabletools.com/doc
16
16
  export declare const AlertMessageDocsLink = "https://www.adaptabletools.com/docs/handbook-alerting-message";
17
17
  export declare const FormatColumnPlaceholderDocsLink = "https://www.adaptabletools.com/docs/handbook-column-formatting-display-format-template";
18
18
  export declare const AgGridRequiredModulesDocsLink = "https://www.adaptabletools.com/docs/getting-started-aggrid-modules#mandatory-modules";
19
+ export declare const StatePersistenceDocsLink = "https://www.adaptabletools.com/docs/dev-guide-adaptable-state-persistence";
@@ -16,3 +16,4 @@ export const AgGridModulesDocsLink = `${HOST_URL_DOCS}/getting-started-aggrid-mo
16
16
  export const AlertMessageDocsLink = `${HOST_URL_DOCS}/handbook-alerting-message`;
17
17
  export const FormatColumnPlaceholderDocsLink = `${HOST_URL_DOCS}/handbook-column-formatting-display-format-template`;
18
18
  export const AgGridRequiredModulesDocsLink = `${HOST_URL_DOCS}/getting-started-aggrid-modules#mandatory-modules`;
19
+ export const StatePersistenceDocsLink = `${HOST_URL_DOCS}/dev-guide-adaptable-state-persistence`;
@@ -2,8 +2,8 @@ import { SummaryOperation } from '../../AdaptableState/Common/Enums';
2
2
  import { SystemThemeEntry } from '../../AdaptableState/ThemeState';
3
3
  export declare const CELL_SUMMARY_DEFAULT_OPERATION: SummaryOperation | string;
4
4
  export declare const QUICK_SEARCH_DEFAULT_TEXT_BACK_COLOR: string;
5
- export declare const QUICK_SEARCH_DEFAULT_CELL_BACK_COLOR: string;
6
- export declare const QUICK_SEARCH_DEFAULT_FORE_COLOR: string;
5
+ export declare const QUICK_SEARCH_DEFAULT_TEXT_FORE_COLOR: string;
7
6
  export declare const QUICK_SEARCH_DEFAULT_CURRENT_BACK_COLOR: string;
7
+ export declare const QUICK_SEARCH_DEFAULT_CURRENT_FORE_COLOR: string;
8
8
  export declare const SYSTEM_THEMES: SystemThemeEntry[];
9
9
  export declare const THEME_DEFAULT_CURRENT_THEME: string;
@@ -1,10 +1,10 @@
1
1
  import { SummaryOperation } from '../../AdaptableState/Common/Enums';
2
- import { getSwatchColorVar } from '../../Utilities/Helpers/UIHelper';
2
+ import { themeColorVar } from '../../Utilities/Helpers/UIHelper';
3
3
  import { LIGHT_THEME, DARK_THEME, OS_THEME } from './GeneralConstants';
4
4
  export const CELL_SUMMARY_DEFAULT_OPERATION = SummaryOperation.Sum;
5
- export const QUICK_SEARCH_DEFAULT_TEXT_BACK_COLOR = getSwatchColorVar(9);
6
- export const QUICK_SEARCH_DEFAULT_CELL_BACK_COLOR = getSwatchColorVar(10);
7
- export const QUICK_SEARCH_DEFAULT_FORE_COLOR = getSwatchColorVar(1);
8
- export const QUICK_SEARCH_DEFAULT_CURRENT_BACK_COLOR = getSwatchColorVar(20);
5
+ export const QUICK_SEARCH_DEFAULT_TEXT_BACK_COLOR = themeColorVar('quick-search-match');
6
+ export const QUICK_SEARCH_DEFAULT_TEXT_FORE_COLOR = themeColorVar('quick-search-match-foreground');
7
+ export const QUICK_SEARCH_DEFAULT_CURRENT_BACK_COLOR = themeColorVar('quick-search-current-match');
8
+ export const QUICK_SEARCH_DEFAULT_CURRENT_FORE_COLOR = themeColorVar('quick-search-current-match-foreground');
9
9
  export const SYSTEM_THEMES = [LIGHT_THEME, DARK_THEME, OS_THEME];
10
10
  export const THEME_DEFAULT_CURRENT_THEME = LIGHT_THEME;
@@ -31,4 +31,4 @@ export declare const inferAlertTypeFromDefinition: (alert: AlertDefinition, kind
31
31
  * otherwise inferred from Rule / Schedule shape.
32
32
  */
33
33
  export declare const getAlertType: (alert: AlertDefinition, kind?: ExpressionKind) => AlertType | null;
34
- export declare const getAlertTypeText: (alertType: AlertType) => "" | "Aggregation" | "Observable" | "Scheduled" | "Data Change" | "Relative Change" | "Data Validation" | "Row Change";
34
+ export declare const getAlertTypeText: (alertType: AlertType) => "" | "Observable" | "Aggregation" | "Scheduled" | "Data Change" | "Relative Change" | "Data Validation" | "Row Change";
@@ -1,6 +1,15 @@
1
1
  import { AdaptableApi } from '../../Api/AdaptableApi';
2
2
  import { SettingsPanelDisplayOptions, SettingsPanelNavigation, SettingsPanelNavigationGroup, SettingsPanelOptions } from '../../AdaptableOptions/SettingsPanelOptions';
3
3
  import { AdaptableIcon } from '../../types';
4
+ /** One Settings Panel sidebar module (visible for the current grid / entitlements). */
5
+ export type VisibleSettingsPanelNavigationItem = {
6
+ moduleName: string;
7
+ label: string;
8
+ };
9
+ export type VisibleSettingsPanelNavigationGroup = {
10
+ label: string;
11
+ items: VisibleSettingsPanelNavigationItem[];
12
+ };
4
13
  /**
5
14
  * True when `adaptableId` is the framework placeholder (`adaptable_id` or `adaptable_id_<n>`),
6
15
  * not a developer-supplied human name.
@@ -41,3 +50,8 @@ export declare const getSettingsPanelOpenIcon: (resolvedIcon: SettingsPanelOptio
41
50
  export declare const DEFAULT_SETTINGS_PANEL_NAVIGATION_GROUPS: SettingsPanelNavigationGroup[];
42
51
  export declare const buildDefaultSettingsPanelNavigation: (customPanelNames?: string[]) => SettingsPanelNavigation;
43
52
  export declare const resolveSettingsPanelNavigation: (api: AdaptableApi, settingsPanelOptions: SettingsPanelOptions) => SettingsPanelNavigation;
53
+ /**
54
+ * Settings Panel navigation groups containing only modules that appear in the
55
+ * panel sidebar (same rules as {@link useMenuItems} in AdaptablePopup).
56
+ */
57
+ export declare function resolveVisibleSettingsPanelNavigationGroups(api: AdaptableApi): VisibleSettingsPanelNavigationGroup[];
@@ -1,4 +1,4 @@
1
- import { ADAPTABLE_ID } from '../Constants/GeneralConstants';
1
+ import { ADAPTABLE_ID, STANDALONE_MODULE_POPUPS } from '../Constants/GeneralConstants';
2
2
  import { SettingsPanelFriendlyName } from '../Constants/ModuleConstants';
3
3
  import StringExtensions from '../Extensions/StringExtensions';
4
4
  export const isGenericAdaptableId = (adaptableId) => {
@@ -143,3 +143,39 @@ export const resolveSettingsPanelNavigation = (api, settingsPanelOptions) => {
143
143
  warnLegacyNavigationOnce(api);
144
144
  return convertLegacyFlatNavigationToGrouped(navigation);
145
145
  };
146
+ export function resolveVisibleSettingsPanelNavigationGroups(api) {
147
+ const settingsPanelOptions = api.optionsApi.getAdaptableOptions().settingsPanelOptions;
148
+ const navigation = resolveSettingsPanelNavigation(api, settingsPanelOptions ?? {});
149
+ const allMenuItems = api.internalApi.getInternalState().SettingsPanelModuleEntries ?? [];
150
+ const visibleItems = allMenuItems.filter((item) => item.category !== 'General' &&
151
+ item.category !== 'CustomSettingsPanel' &&
152
+ item.category !== 'Group' &&
153
+ !STANDALONE_MODULE_POPUPS.includes(item.category) &&
154
+ item.isVisible);
155
+ const moduleService = api.internalApi.getModuleService();
156
+ const groups = [];
157
+ for (const group of navigation.groups) {
158
+ const items = [];
159
+ for (const moduleName of group.items) {
160
+ const moduleId = String(moduleName);
161
+ const customSettingsPanel = settingsPanelOptions?.customSettingsPanels?.find((panel) => panel.name === moduleId);
162
+ if (customSettingsPanel) {
163
+ items.push({ moduleName: moduleId, label: customSettingsPanel.name });
164
+ continue;
165
+ }
166
+ const menuItem = visibleItems.find((item) => item.category === moduleId);
167
+ if (!menuItem) {
168
+ continue;
169
+ }
170
+ items.push({
171
+ moduleName: moduleId,
172
+ label: menuItem.label ??
173
+ moduleService.getModuleFriendlyName(moduleId),
174
+ });
175
+ }
176
+ if (items.length) {
177
+ groups.push({ label: group.label ?? '', items });
178
+ }
179
+ }
180
+ return groups;
181
+ }