@adaptabletools/adaptable 22.0.7 → 22.0.9
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/index.css +19 -1
- package/index.css.map +1 -1
- package/package.json +1 -1
- package/src/AdaptableOptions/FilterOptions.d.ts +23 -5
- package/src/AdaptableState/Common/CellDataChangedInfo.d.ts +4 -0
- package/src/AdaptableState/Common/ColumnScope.d.ts +4 -0
- package/src/AdaptableState/Common/ColumnScope.js +12 -1
- package/src/Api/Implementation/StateApiImpl.d.ts +1 -1
- package/src/Api/Implementation/StateApiImpl.js +12 -15
- package/src/Api/StateApi.d.ts +1 -1
- package/src/Redux/Store/AdaptableReduxMerger.js +4 -1
- package/src/Redux/Store/AdaptableStore.d.ts +2 -1
- package/src/Redux/Store/AdaptableStore.js +12 -8
- package/src/Redux/Store/Interface/IAdaptableStore.d.ts +1 -1
- package/src/Strategy/CalculatedColumnModule.js +1 -1
- package/src/Strategy/ColumnFilterModule.js +4 -2
- package/src/Strategy/DataChangeHistoryModule.js +3 -1
- package/src/Strategy/FlashingCellModule.js +1 -1
- package/src/Strategy/FreeTextColumnModule.js +1 -1
- package/src/Utilities/Services/DataService.js +6 -0
- package/src/View/Alert/Wizard/AlertScopeWizardSection.js +10 -3
- package/src/View/CalculatedColumn/Wizard/CalculatedColumnDefinitionWizardSection.js +8 -8
- package/src/View/CalculatedColumn/Wizard/CalculatedColumnTypeSection.js +1 -1
- package/src/View/CalculatedColumn/Wizard/CalculatedColumnWizard.js +4 -2
- package/src/View/Components/ColumnFilter/components/FloatingFilterValues.js +1 -0
- package/src/View/Components/ColumnGroupTag/index.d.ts +5 -0
- package/src/View/Components/ColumnGroupTag/index.js +9 -0
- package/src/View/Components/ColumnSelector/index.js +4 -1
- package/src/View/Components/FilterForm/ListBoxFilterForm.js +9 -2
- package/src/View/Components/NewScopeComponent.js +19 -37
- package/src/View/Components/ReorderDraggable/index.d.ts +1 -0
- package/src/View/Components/ReorderDraggable/index.js +2 -1
- package/src/View/CustomSort/Wizard/CustomSortColumnWizardSection.js +7 -1
- package/src/View/CustomSort/Wizard/CustomSortWizard.js +1 -1
- package/src/View/FlashingCell/Wizard/FlashingCellScopeWizardSection.js +10 -3
- package/src/View/FormatColumn/Wizard/FormatColumnColumnScopeWizardSection.js +14 -7
- package/src/View/FormatColumn/Wizard/FormatColumnRuleWizardSection.js +1 -3
- package/src/View/FormatColumn/Wizard/FormatColumnWizard.js +1 -3
- package/src/View/FreeTextColumn/Wizard/FreeTextColumnSettingsWizardSection.d.ts +1 -1
- package/src/View/FreeTextColumn/Wizard/FreeTextColumnSettingsWizardSection.js +7 -7
- package/src/View/Layout/Wizard/LayoutWizard.js +2 -2
- package/src/View/Layout/Wizard/sections/AggregationsSection.js +2 -0
- package/src/View/Layout/Wizard/sections/ColumnsSection.js +149 -140
- package/src/View/Layout/Wizard/sections/FilterSection.js +8 -1
- package/src/View/Layout/Wizard/sections/PivotAggregationsSection.js +2 -0
- package/src/View/Layout/Wizard/sections/PivotRowGroupingSection.js +5 -2
- package/src/View/Layout/Wizard/sections/RowGroupingSection.js +4 -1
- package/src/View/Layout/Wizard/sections/RowSummarySection.js +8 -3
- package/src/View/Layout/Wizard/sections/SortSection.js +4 -2
- package/src/View/StyledColumn/Wizard/StyledColumnWizardColumnSection.js +8 -2
- package/src/View/Wizard/OnePageAdaptableWizard.d.ts +0 -1
- package/src/View/Wizard/OnePageAdaptableWizard.js +1 -1
- package/src/View/Wizard/OnePageWizards.d.ts +1 -0
- package/src/View/Wizard/OnePageWizards.js +11 -4
- package/src/agGrid/AdaptableAgGrid.js +18 -30
- package/src/agGrid/AgGridAdapter.d.ts +1 -0
- package/src/agGrid/AgGridAdapter.js +4 -0
- package/src/agGrid/AgGridColumnAdapter.js +3 -3
- package/src/agGrid/AgGridExportAdapter.js +1 -3
- package/src/components/Tree/TreeDropdown/index.d.ts +9 -0
- package/src/components/Tree/TreeDropdown/index.js +20 -1
- package/src/components/Tree/TreeList/index.d.ts +1 -1
- package/src/env.js +2 -2
- package/src/metamodel/adaptable.metamodel.js +1 -1
- package/tsconfig.esm.tsbuildinfo +1 -1
|
@@ -59,6 +59,12 @@ const NON_PERSISTENT_STORE_KEYS = [
|
|
|
59
59
|
'Comment',
|
|
60
60
|
'Plugins',
|
|
61
61
|
];
|
|
62
|
+
export const getPersistableState = (state) => {
|
|
63
|
+
NON_PERSISTENT_STORE_KEYS.forEach((key) => {
|
|
64
|
+
delete state[key];
|
|
65
|
+
});
|
|
66
|
+
return state;
|
|
67
|
+
};
|
|
62
68
|
export const InitState = () => ({
|
|
63
69
|
type: INIT_STATE,
|
|
64
70
|
});
|
|
@@ -198,10 +204,8 @@ export class AdaptableStore {
|
|
|
198
204
|
!init &&
|
|
199
205
|
!this.loadStorageInProgress;
|
|
200
206
|
if (shouldPersist) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
delete storageState[key];
|
|
204
|
-
});
|
|
207
|
+
let storageState = { ...finalState };
|
|
208
|
+
storageState = getPersistableState(storageState);
|
|
205
209
|
this.currentStorageState = storageState;
|
|
206
210
|
this.previousStorageState = { ...state };
|
|
207
211
|
NON_PERSISTENT_STORE_KEYS.forEach((key) => {
|
|
@@ -264,10 +268,10 @@ export class AdaptableStore {
|
|
|
264
268
|
getPreviousStorageState() {
|
|
265
269
|
return this.previousStorageState;
|
|
266
270
|
}
|
|
267
|
-
saveStateNow(adaptable) {
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
return this.storageEngine.saveNow(adaptable,
|
|
271
|
+
saveStateNow(adaptable, state) {
|
|
272
|
+
const newState = state ?? this.getCurrentStorageState();
|
|
273
|
+
if (newState) {
|
|
274
|
+
return this.storageEngine.saveNow(adaptable, newState, 'API_CALL_SAVE_STATE');
|
|
271
275
|
}
|
|
272
276
|
return Promise.resolve(true);
|
|
273
277
|
}
|
|
@@ -14,7 +14,7 @@ export interface IAdaptableStore {
|
|
|
14
14
|
loadStore: (config: LoadStoreConfig) => Promise<any>;
|
|
15
15
|
getCurrentStorageState: () => AdaptableState | void;
|
|
16
16
|
getPreviousStorageState: () => AdaptableState | void;
|
|
17
|
-
saveStateNow: (adaptable: IAdaptable) => Promise<any>;
|
|
17
|
+
saveStateNow: (adaptable: IAdaptable, state?: AdaptableState) => 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;
|
|
@@ -32,7 +32,7 @@ export class CalculatedColumnModule extends AdaptableModuleBase {
|
|
|
32
32
|
this.api.internalApi
|
|
33
33
|
.getDataService()
|
|
34
34
|
.on('CellDataChanged', (cellDataChangedInfo) => {
|
|
35
|
-
if (cellDataChangedInfo.trigger !== 'aggChange') {
|
|
35
|
+
if (cellDataChangedInfo.trigger !== 'aggChange' && !cellDataChangedInfo.preventEdit) {
|
|
36
36
|
this.api.internalApi
|
|
37
37
|
.getCalculatedColumnExpressionService()
|
|
38
38
|
.listentoCellDataChange(cellDataChangedInfo);
|
|
@@ -24,8 +24,10 @@ export class ColumnFilterModule extends AdaptableModuleBase {
|
|
|
24
24
|
this.api.internalApi
|
|
25
25
|
.getDataService()
|
|
26
26
|
.on('CellDataChanged', (cellDataChangedInfo) => {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (!cellDataChangedInfo.preventEdit) {
|
|
28
|
+
const changedColumnId = cellDataChangedInfo.column.columnId;
|
|
29
|
+
this.api.filterApi.columnFilterApi.resetFilterValues(changedColumnId);
|
|
30
|
+
}
|
|
29
31
|
});
|
|
30
32
|
// we reset the filter cache on sort change, because we might have columns
|
|
31
33
|
// with "In" filters, with custom values, which use `context.orderedValues`
|
|
@@ -29,7 +29,9 @@ export class DataChangeHistoryModule extends AdaptableModuleBase {
|
|
|
29
29
|
this.api.internalApi
|
|
30
30
|
.getDataService()
|
|
31
31
|
.on('CellDataChanged', (cellDataChangedInfo) => {
|
|
32
|
-
if (this.isListeningToCellDataChanges &&
|
|
32
|
+
if (this.isListeningToCellDataChanges &&
|
|
33
|
+
this.isDataChangeLoggable(cellDataChangedInfo) &&
|
|
34
|
+
!cellDataChangedInfo.preventEdit) {
|
|
33
35
|
this.api.dataChangeHistoryApi.addDataChangeHistoryEntry(cellDataChangedInfo);
|
|
34
36
|
}
|
|
35
37
|
});
|
|
@@ -35,7 +35,7 @@ export class FlashingCellModule extends AdaptableModuleBase {
|
|
|
35
35
|
this.api.internalApi
|
|
36
36
|
.getDataService()
|
|
37
37
|
.on('CellDataChanged', (cellDataChangedInfo) => {
|
|
38
|
-
if (cellDataChangedInfo.trigger === 'undo') {
|
|
38
|
+
if (cellDataChangedInfo.trigger === 'undo' || cellDataChangedInfo.preventEdit) {
|
|
39
39
|
// do NOT handle reverted changes
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
@@ -30,7 +30,7 @@ export class FreeTextColumnModule extends AdaptableModuleBase {
|
|
|
30
30
|
this.api.internalApi
|
|
31
31
|
.getDataService()
|
|
32
32
|
.on('CellDataChanged', (cellDataChangedInfo) => {
|
|
33
|
-
if (cellDataChangedInfo.trigger !== 'aggChange') {
|
|
33
|
+
if (cellDataChangedInfo.trigger !== 'aggChange' && !cellDataChangedInfo.preventEdit) {
|
|
34
34
|
this.api.freeTextColumnApi.internalApi.handleFreeTextColumnDataChange(cellDataChangedInfo);
|
|
35
35
|
}
|
|
36
36
|
});
|
|
@@ -41,6 +41,12 @@ export class DataService {
|
|
|
41
41
|
emit = (eventName, data) => this.emitter.emit(eventName, data);
|
|
42
42
|
CreateCellDataChangedEvent(cellDataChangedInfo) {
|
|
43
43
|
if (cellDataChangedInfo.newValue != cellDataChangedInfo.oldValue) {
|
|
44
|
+
const validationRules = this.adaptable.api.internalApi
|
|
45
|
+
.getValidationService()
|
|
46
|
+
.getValidationRulesForDataChange(cellDataChangedInfo);
|
|
47
|
+
if (validationRules.length) {
|
|
48
|
+
cellDataChangedInfo.preventEdit = true;
|
|
49
|
+
}
|
|
44
50
|
this.emitter.emitSync('CellDataChanged', cellDataChangedInfo);
|
|
45
51
|
this.adaptable.api.eventApi.internalApi.fireCellChangedEvent(cellDataChangedInfo);
|
|
46
52
|
const dataChangeLogEntry = this.extractDataChangeLogEntry(cellDataChangedInfo);
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { NewScopeComponent } from '../../Components/NewScopeComponent';
|
|
3
3
|
import { useOnePageAdaptableWizardContext } from '../../Wizard/OnePageAdaptableWizard';
|
|
4
4
|
import { Flex } from '../../../components/Flex';
|
|
5
|
+
import { isScopeColumnIds } from '../../../AdaptableState/Common/ColumnScope';
|
|
5
6
|
export const AlertScopeWizardSection = (props) => {
|
|
6
7
|
const { data, api } = useOnePageAdaptableWizardContext();
|
|
7
8
|
let disableDataTypes = true;
|
|
@@ -18,10 +19,16 @@ export const AlertScopeWizardSection = (props) => {
|
|
|
18
19
|
}, scope: data.Scope, updateScope: (Scope) => {
|
|
19
20
|
const newData = { ...data, Scope };
|
|
20
21
|
if (newData.Rule.Predicates) {
|
|
21
|
-
|
|
22
|
-
// if it was set to a predicate before
|
|
22
|
+
const validPredicateIds = new Set(api.alertApi.internalApi.getAlertPredicateDefsForScope(Scope).map((def) => def.id));
|
|
23
23
|
newData.Rule = {
|
|
24
|
-
Predicates:
|
|
24
|
+
Predicates: newData.Rule.Predicates.filter((p) => validPredicateIds.has(p.PredicateId)).filter((predicate) => {
|
|
25
|
+
// if there are more than 1 column, then we must eliminate the IN/NotIn predicates
|
|
26
|
+
// TODO: this should NOT be required, but the ColumnValueSelector does NOT support creatable values right now
|
|
27
|
+
if (isScopeColumnIds(Scope) && Scope.ColumnIds.length > 1) {
|
|
28
|
+
return predicate.PredicateId !== 'In' && predicate.PredicateId !== 'NotIn';
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}),
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
if (newData.Rule.ObservableExpression !== undefined &&
|
|
@@ -21,11 +21,11 @@ export const renderCalculatedColumnDefinitionSummary = (data) => {
|
|
|
21
21
|
export const isValidCalculatedColumnDefinition = (data, api) => {
|
|
22
22
|
const columns = api.columnApi.getColumns();
|
|
23
23
|
if (!data.ColumnId) {
|
|
24
|
-
return 'A Column
|
|
24
|
+
return 'A Column Name is required';
|
|
25
25
|
}
|
|
26
26
|
const columnsWithSameIdCount = columns.filter((c) => c.columnId === data.ColumnId).length;
|
|
27
27
|
const hasAlreadyExistingId = data.Uuid ? columnsWithSameIdCount > 1 : columnsWithSameIdCount > 0;
|
|
28
|
-
return hasAlreadyExistingId ? 'A column with this
|
|
28
|
+
return hasAlreadyExistingId ? 'A column with this Name already exists' : true;
|
|
29
29
|
};
|
|
30
30
|
export const CalculatedColumnDefinitionWizardSection = (props) => {
|
|
31
31
|
const { data, api } = useOnePageAdaptableWizardContext();
|
|
@@ -65,16 +65,16 @@ export const CalculatedColumnDefinitionWizardSection = (props) => {
|
|
|
65
65
|
React.createElement(Tabs.Content, null,
|
|
66
66
|
React.createElement(Flex, { flexDirection: "row" },
|
|
67
67
|
React.createElement(FormLayout, null,
|
|
68
|
-
React.createElement(FormRow, { label: "Column
|
|
69
|
-
React.createElement(Input, { "data-name": "column-id", value: data.ColumnId || '', width:
|
|
70
|
-
React.createElement(FormRow, { label: "
|
|
68
|
+
React.createElement(FormRow, { label: "Column Name" },
|
|
69
|
+
React.createElement(Input, { "data-name": "column-id", value: data.ColumnId || '', style: { width: '100%', maxWidth: 500 }, autoFocus: !inEdit, disabled: inEdit, type: "text", placeholder: "Enter a Column Name", onChange: handleColumnIdChange })),
|
|
70
|
+
React.createElement(FormRow, { label: "Column Header" },
|
|
71
71
|
React.createElement(Input, { "data-name": "column-name", autoFocus: inEdit, onFocus: () => {
|
|
72
72
|
setColumnNameFocused(true);
|
|
73
73
|
}, onBlur: () => {
|
|
74
74
|
setColumnNameFocused(false);
|
|
75
|
-
}, value: ColumnNameFocused ? ColumnName || '' : ColumnName || ColumnId || '', width:
|
|
76
|
-
React.createElement(FormRow, { label: "Header Tooltip" },
|
|
77
|
-
React.createElement(Input, { "data-name": "header-tooltip", type: "text", width:
|
|
75
|
+
}, value: ColumnNameFocused ? ColumnName || '' : ColumnName || ColumnId || '', style: { width: '100%', maxWidth: 500 }, type: "text", placeholder: "Enter a Column Header (optional)", onChange: handleColumnNameChange })),
|
|
76
|
+
React.createElement(FormRow, { label: "Column Header Tooltip" },
|
|
77
|
+
React.createElement(Input, { "data-name": "header-tooltip", type: "text", style: { width: '100%', maxWidth: 500 }, value: HeaderToolTip, onChange: (e) => handleSpecialColumnSettingsChange({
|
|
78
78
|
HeaderToolTip: e.target.value,
|
|
79
79
|
}) })),
|
|
80
80
|
React.createElement(FormRow, { label: "" },
|
|
@@ -4,7 +4,7 @@ import { TypeRadio } from '../../Wizard/TypeRadio';
|
|
|
4
4
|
import { Flex } from '../../../components/Flex';
|
|
5
5
|
export const CalculatedColumnTypeWizardSection = (props) => {
|
|
6
6
|
return (React.createElement(Tabs, null,
|
|
7
|
-
React.createElement(Tabs.Tab, null, "Calculated Column
|
|
7
|
+
React.createElement(Tabs.Tab, null, "Calculated Column Type"),
|
|
8
8
|
React.createElement(Tabs.Content, null,
|
|
9
9
|
React.createElement(Flex, { flexDirection: "column" },
|
|
10
10
|
React.createElement(TypeRadio, { text: 'Standard', description: "The calculated value is derived from other cells in the row", checked: props.type === 'ScalarExpression', onClick: () => props.onTypeChange('ScalarExpression') }),
|
|
@@ -67,6 +67,7 @@ export const CalculatedColumnWizard = (props) => {
|
|
|
67
67
|
return (React.createElement(OnePageAdaptableWizard, { defaultCurrentSectionName: props.defaultCurrentSectionName, moduleInfo: props.moduleInfo, data: calculatedColumn, onHide: props.onCloseWizard, onFinish: handleFinish, sections: [
|
|
68
68
|
{
|
|
69
69
|
title: 'Type',
|
|
70
|
+
details: 'Select Type of Calculated Column',
|
|
70
71
|
renderSummary: () => {
|
|
71
72
|
return (React.createElement(Box, { className: "twa:text-2" },
|
|
72
73
|
"Expression Type: ",
|
|
@@ -79,7 +80,7 @@ export const CalculatedColumnWizard = (props) => {
|
|
|
79
80
|
},
|
|
80
81
|
{
|
|
81
82
|
title: 'Details',
|
|
82
|
-
details: '
|
|
83
|
+
details: 'Provide Calculated Column Details',
|
|
83
84
|
isValid: isValidCalculatedColumnDefinition,
|
|
84
85
|
renderSummary: renderCalculatedColumnDefinitionSummary,
|
|
85
86
|
render: () => {
|
|
@@ -99,7 +100,7 @@ export const CalculatedColumnWizard = (props) => {
|
|
|
99
100
|
},
|
|
100
101
|
{
|
|
101
102
|
title: 'Settings',
|
|
102
|
-
details: 'Specify Column
|
|
103
|
+
details: 'Specify Calculated Column Properties',
|
|
103
104
|
isValid: isValidCalculatedColumnSettings,
|
|
104
105
|
renderSummary: renderCalculatedColumnSettingsSummary,
|
|
105
106
|
render: () => {
|
|
@@ -117,6 +118,7 @@ export const CalculatedColumnWizard = (props) => {
|
|
|
117
118
|
},
|
|
118
119
|
'-',
|
|
119
120
|
{
|
|
121
|
+
details: 'Review the Calculated Column',
|
|
120
122
|
render: () => {
|
|
121
123
|
return (React.createElement(Box, { className: "twa:p-2" },
|
|
122
124
|
React.createElement(OnePageWizardSummary, null)));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box } from '../../../components/Flex';
|
|
3
|
+
export const ColumnGroupTag = ({ column }) => {
|
|
4
|
+
if (!column.columnGroup || column.columnGroup.groupCount <= 1)
|
|
5
|
+
return null;
|
|
6
|
+
return (React.createElement(Box, { className: "ab-Layout-Wizard__ColumnRow__Title twa:mx-2 twa:p-1 twa:flex twa:items-center" },
|
|
7
|
+
"Column Group: ",
|
|
8
|
+
column.columnGroup.friendlyName));
|
|
9
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { ValueSelector } from '../ValueSelector';
|
|
4
|
+
import { ColumnGroupTag } from '../ColumnGroupTag';
|
|
4
5
|
export const NewColumnSelector = ({ allowReorder = true, ...props }) => {
|
|
5
6
|
const { columnFilterText, availableColumns, selected, onChange, singleSelect } = props;
|
|
6
7
|
const filterFn = useMemo(() => {
|
|
@@ -15,5 +16,7 @@ export const NewColumnSelector = ({ allowReorder = true, ...props }) => {
|
|
|
15
16
|
else {
|
|
16
17
|
onChange(colIds);
|
|
17
18
|
}
|
|
18
|
-
}, toIdentifier: (c) => c.columnId, toLabel: (c) => c.friendlyName }
|
|
19
|
+
}, toIdentifier: (c) => c.columnId, toLabel: (c) => c.friendlyName, toListLabel: (c) => (React.createElement("div", { className: "twa:flex twa:flex-row twa:items-center" },
|
|
20
|
+
c.friendlyName,
|
|
21
|
+
React.createElement(ColumnGroupTag, { column: c }))) }));
|
|
19
22
|
};
|
|
@@ -4,7 +4,7 @@ import { isEqual } from 'date-fns';
|
|
|
4
4
|
import { parseDateValue } from '../../../Utilities/Helpers/DateHelper';
|
|
5
5
|
import join from '../../../components/utils/join';
|
|
6
6
|
import { Select } from '../../../components/Select';
|
|
7
|
-
import { toDisplayValueDefault, TreeDropdown } from '../../../components/Tree/TreeDropdown';
|
|
7
|
+
import { toDisplayValueDefault, toDisplayValueFromOptionTree, TreeDropdown, } from '../../../components/Tree/TreeDropdown';
|
|
8
8
|
import { AG_GRID_GROUPED_COLUMN } from '../../../Utilities/Constants/GeneralConstants';
|
|
9
9
|
import { useAdaptable } from '../../AdaptableContext';
|
|
10
10
|
import { useMemo } from 'react';
|
|
@@ -93,13 +93,20 @@ export const ColumnValuesSelect = (props) => {
|
|
|
93
93
|
minWidth: `var(--ab-cmp-select-column-menu-${column.columnId}__min-width, var(--ab-cmp-select-column-menu__min-width, 160px))`,
|
|
94
94
|
};
|
|
95
95
|
}, [column.columnId]);
|
|
96
|
+
const treeDropdownToDisplayValue = React.useMemo(() => {
|
|
97
|
+
if (column.dataType === 'date') {
|
|
98
|
+
return toDateDisplayValue;
|
|
99
|
+
}
|
|
100
|
+
return (paths) => toDisplayValueFromOptionTree(paths, optionsFromProps);
|
|
101
|
+
}, [column.dataType, optionsFromProps]);
|
|
96
102
|
const component = column.dataType === 'date' || column.columnId === AG_GRID_GROUPED_COLUMN ? (React.createElement(TreeDropdown, { style: {
|
|
97
103
|
width: '100%',
|
|
98
104
|
height: '100%',
|
|
99
105
|
...props.selectProps?.styles?.container,
|
|
100
|
-
}, toDisplayValue:
|
|
106
|
+
}, toDisplayValue: treeDropdownToDisplayValue, fieldStyle: {
|
|
101
107
|
boxShadow: 'none',
|
|
102
108
|
...props.selectProps?.styles?.control,
|
|
109
|
+
...props.selectProps?.styles?.valueContainer,
|
|
103
110
|
}, resizable: true, onChange: column.dataType === 'date'
|
|
104
111
|
? (value) => props.onChange(dateOnChangeParser(value))
|
|
105
112
|
: props.onChange, options: options,
|
|
@@ -71,47 +71,30 @@ export const NewScopeComponent = (props) => {
|
|
|
71
71
|
}
|
|
72
72
|
return allColumns;
|
|
73
73
|
}, []);
|
|
74
|
-
const
|
|
74
|
+
const getTabFromScope = (scope) => {
|
|
75
75
|
if (!scope) {
|
|
76
76
|
return undefined;
|
|
77
77
|
}
|
|
78
78
|
if (scopeApi.scopeIsAll(scope)) {
|
|
79
|
-
return '
|
|
79
|
+
return 'Row';
|
|
80
80
|
}
|
|
81
|
-
if (scopeApi.scopeHasColumns(
|
|
81
|
+
if (scopeApi.scopeHasColumns(scope)) {
|
|
82
82
|
return 'Column';
|
|
83
83
|
}
|
|
84
|
-
if (scopeApi.scopeHasDataType(
|
|
84
|
+
if (scopeApi.scopeHasDataType(scope)) {
|
|
85
85
|
return 'DataType';
|
|
86
86
|
}
|
|
87
|
-
if (scopeApi.scopeHasColumnType(
|
|
87
|
+
if (scopeApi.scopeHasColumnType(scope)) {
|
|
88
88
|
return 'ColumnType';
|
|
89
89
|
}
|
|
90
90
|
return undefined;
|
|
91
91
|
};
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
};
|
|
92
|
+
const [activeTab, setActiveTab] = useState(() => getTabFromScope(props.scope));
|
|
93
|
+
const onTabChanged = (value) => {
|
|
94
|
+
setActiveTab(value);
|
|
95
|
+
if (value === 'Row') {
|
|
96
|
+
props.updateScope({ All: true });
|
|
98
97
|
}
|
|
99
|
-
else if (value == 'DataType') {
|
|
100
|
-
newScope = {
|
|
101
|
-
DataTypes: [],
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
else if (value == 'ColumnType') {
|
|
105
|
-
newScope = {
|
|
106
|
-
ColumnTypes: [],
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
newScope = {
|
|
111
|
-
All: true,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
props.updateScope(newScope);
|
|
115
98
|
};
|
|
116
99
|
const onColumnsSelectedChanged = (cols) => {
|
|
117
100
|
const newScope = {
|
|
@@ -120,7 +103,7 @@ export const NewScopeComponent = (props) => {
|
|
|
120
103
|
props.updateScope(newScope);
|
|
121
104
|
};
|
|
122
105
|
const onCheckBoxDataTypeChecked = (checked, item) => {
|
|
123
|
-
let dataTypes = [].concat(scopeApi.getDataTypesInScope(props.scope));
|
|
106
|
+
let dataTypes = [].concat(scopeApi.getDataTypesInScope(props.scope) ?? []);
|
|
124
107
|
if (checked) {
|
|
125
108
|
dataTypes.push(item);
|
|
126
109
|
}
|
|
@@ -135,7 +118,6 @@ export const NewScopeComponent = (props) => {
|
|
|
135
118
|
};
|
|
136
119
|
props.updateScope(newScope);
|
|
137
120
|
};
|
|
138
|
-
const scopeChoice = getScopeChoice(props.scope);
|
|
139
121
|
const dataTypesInScope = props.scope && 'DataTypes' in props.scope ? scopeApi.getDataTypesInScope(props.scope) : null;
|
|
140
122
|
let dataTypeOptions = DATA_TYPES_OPTIONS;
|
|
141
123
|
if (Array.isArray(props.availableDataTypes)) {
|
|
@@ -144,15 +126,15 @@ export const NewScopeComponent = (props) => {
|
|
|
144
126
|
const hasColumnTypes = React.useMemo(() => {
|
|
145
127
|
return api.columnApi.getColumnTypes()?.length > 0;
|
|
146
128
|
}, []);
|
|
147
|
-
return (React.createElement(Tabs, { "data-name": 'scope-component', className: "ab-ScopeComponent", value:
|
|
129
|
+
return (React.createElement(Tabs, { "data-name": 'scope-component', className: "ab-ScopeComponent", value: activeTab, style: { height: '100%', ...props.style }, onValueChange: onTabChanged },
|
|
148
130
|
props.hideWholeRow ? null : (React.createElement(Tabs.Tab, { value: "Row" },
|
|
149
|
-
React.createElement(Radio, { className: "twa:m-0", checked:
|
|
131
|
+
React.createElement(Radio, { className: "twa:m-0", checked: activeTab == 'Row', tabIndex: -1 }, "All Columns"))),
|
|
150
132
|
!props.disableColumns && (React.createElement(Tabs.Tab, { value: "Column" },
|
|
151
|
-
React.createElement(Radio, { className: "twa:m-0", value: "Column", checked:
|
|
133
|
+
React.createElement(Radio, { className: "twa:m-0", value: "Column", checked: activeTab == 'Column', tabIndex: -1 }, "Selected Columns"))),
|
|
152
134
|
!props.disableDataTypes && (React.createElement(Tabs.Tab, { value: "DataType" },
|
|
153
|
-
React.createElement(Radio, { className: "twa:m-0", value: "DataType", checked:
|
|
135
|
+
React.createElement(Radio, { className: "twa:m-0", value: "DataType", checked: activeTab == 'DataType', tabIndex: -1 }, "Data Types"))),
|
|
154
136
|
hasColumnTypes && (React.createElement(Tabs.Tab, { value: "ColumnType" },
|
|
155
|
-
React.createElement(Radio, { className: "twa:m-0", value: "ColumnType", checked:
|
|
137
|
+
React.createElement(Radio, { className: "twa:m-0", value: "ColumnType", checked: activeTab == 'ColumnType', tabIndex: -1 }, "Column Types"))),
|
|
156
138
|
props.hideWholeRow ? null : (React.createElement(Tabs.Content, { value: "Row", style: { flex: 'none' }, "data-name": "row-scope" },
|
|
157
139
|
React.createElement(Box, { className: "twa:p-2 twa:pl-0 twa:text-2" }, props.descriptions.rowScope))),
|
|
158
140
|
!props.disableColumns && (React.createElement(Tabs.Content, { value: "Column", "data-name": "column-scope", className: "twa:p-0 twa:flex-1 twa:overflow-auto" },
|
|
@@ -161,15 +143,15 @@ export const NewScopeComponent = (props) => {
|
|
|
161
143
|
React.createElement(Box, { className: "twa:flex-3" }),
|
|
162
144
|
React.createElement(AdaptableFormControlTextClear, { value: columnsSearchText, OnTextChange: setColumnsSearchText, placeholder: "Type to search columns", style: { flex: 1 } })),
|
|
163
145
|
React.createElement(Flex, { className: "twa:overflow-hidden twa:pl-2 twa:flex-1" },
|
|
164
|
-
React.createElement(NewColumnSelector, { columnFilterText: columnsSearchText, allowReorder: false, availableColumns: scopeColumns, selected: scopeApi.getColumnIdsInScope(props.scope), onChange: onColumnsSelectedChanged })))),
|
|
146
|
+
React.createElement(NewColumnSelector, { columnFilterText: columnsSearchText, allowReorder: false, availableColumns: scopeColumns, selected: scopeApi.getColumnIdsInScope(props.scope) ?? [], onChange: onColumnsSelectedChanged })))),
|
|
165
147
|
!props.disableDataTypes && (React.createElement(Tabs.Content, { value: "DataType", style: { flex: 'none' }, "data-name": "datatype-scope" },
|
|
166
148
|
React.createElement(Box, null,
|
|
167
149
|
props.descriptions.dataTypeScope && (React.createElement(Box, { className: "twa:p-2 twa:pl-0 twa:mb-2 twa:text-2" }, props.descriptions.dataTypeScope)),
|
|
168
|
-
React.createElement(Flex, { flexDirection: "column" }, dataTypeOptions.map((dataTypeOption) => (React.createElement(CheckBox, { "data-name": "scope", "data-value": dataTypeOption.value, key: dataTypeOption.value, checked: dataTypesInScope && dataTypesInScope.includes(dataTypeOption.value), onChange: (checked) => onCheckBoxDataTypeChecked(checked, dataTypeOption.value) }, dataTypeOption.label))))))),
|
|
150
|
+
React.createElement(Flex, { flexDirection: "column" }, dataTypeOptions.map((dataTypeOption) => (React.createElement(CheckBox, { "data-name": "scope", "data-value": dataTypeOption.value, key: dataTypeOption.value, checked: !!dataTypesInScope && dataTypesInScope.includes(dataTypeOption.value), onChange: (checked) => onCheckBoxDataTypeChecked(checked, dataTypeOption.value) }, dataTypeOption.label))))))),
|
|
169
151
|
hasColumnTypes && (React.createElement(Tabs.Content, { value: "ColumnType", className: "twa:flex-none", "data-name": "column-type-scope" },
|
|
170
152
|
React.createElement(Box, null,
|
|
171
153
|
React.createElement(Flex, { flexDirection: "column" }, api.columnApi.getColumnTypes()?.map?.((columnType) => (React.createElement(CheckBox, { "data-name": "scope", "data-value": columnType, key: columnType, checked: 'ColumnTypes' in props.scope && props.scope.ColumnTypes?.includes(columnType), onChange: (checked) => {
|
|
172
|
-
let columnTypes = [].concat(props.scope.ColumnTypes);
|
|
154
|
+
let columnTypes = [].concat('ColumnTypes' in props.scope ? props.scope.ColumnTypes : []);
|
|
173
155
|
if (checked) {
|
|
174
156
|
columnTypes.push(columnType);
|
|
175
157
|
}
|
|
@@ -10,5 +10,6 @@ export type ReorderDraggableProps<OPTION_TYPE, ID_TYPE extends number | string>
|
|
|
10
10
|
onOptionClick?: (option: OPTION_TYPE, event: React.MouseEvent<HTMLDivElement>) => void;
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
style?: React.CSSProperties;
|
|
13
|
+
className?: string;
|
|
13
14
|
};
|
|
14
15
|
export declare function ReorderDraggable<OPTION_TYPE, ID_TYPE extends number | string>(props: ReorderDraggableProps<OPTION_TYPE, ID_TYPE>): React.JSX.Element;
|
|
@@ -4,6 +4,7 @@ import { Icon } from '../../../components/icons';
|
|
|
4
4
|
import ArrayExtensions from '../../../Utilities/Extensions/ArrayExtensions';
|
|
5
5
|
import { Box, Flex } from '../../../components/Flex';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
|
+
import { twMerge } from '../../../twMerge';
|
|
7
8
|
export function ReorderDraggable(props) {
|
|
8
9
|
const { onChange, order, toIdentifier, isOptionDraggable, disabled, } = props;
|
|
9
10
|
const baseClassName = 'ab-ReorderDraggable';
|
|
@@ -27,7 +28,7 @@ export function ReorderDraggable(props) {
|
|
|
27
28
|
}, draggableProvided.dragHandleProps);
|
|
28
29
|
}));
|
|
29
30
|
};
|
|
30
|
-
return (React.createElement(Flex, { style: props.style, className: `${baseClassName} twa:flex-1`, flexDirection: "column" },
|
|
31
|
+
return (React.createElement(Flex, { style: props.style, className: twMerge(`${baseClassName} twa:flex-1`, props.className), flexDirection: "column" },
|
|
31
32
|
React.createElement(DragDropContext, { onDragEnd: (result) => {
|
|
32
33
|
const { source, destination } = result;
|
|
33
34
|
const newOrder = ArrayExtensions.reorderArray(props.order, source.index, destination.index);
|
|
@@ -6,6 +6,7 @@ import { useOnePageAdaptableWizardContext } from '../../Wizard/OnePageAdaptableW
|
|
|
6
6
|
import { Box, Flex } from '../../../components/Flex';
|
|
7
7
|
import FormLayout, { FormRow } from '../../../components/FormLayout';
|
|
8
8
|
import Input from '../../../components/Input';
|
|
9
|
+
import { AdaptableFormControlTextClear } from '../../Components/Forms/AdaptableFormControlTextClear';
|
|
9
10
|
export const renderCustomSortColumn = (data) => {
|
|
10
11
|
const { api } = useOnePageAdaptableWizardContext();
|
|
11
12
|
return (React.createElement(Box, { className: "twa:text-2 twa:py-2 twa:pr-2" },
|
|
@@ -54,6 +55,7 @@ export const CustomSortColumnWizardSection = (props) => {
|
|
|
54
55
|
});
|
|
55
56
|
});
|
|
56
57
|
}, []);
|
|
58
|
+
const [columnsSearchText, setColumnsSearchText] = React.useState('');
|
|
57
59
|
const onNameChange = (event) => {
|
|
58
60
|
props.onChange({
|
|
59
61
|
...data,
|
|
@@ -71,7 +73,11 @@ export const CustomSortColumnWizardSection = (props) => {
|
|
|
71
73
|
React.createElement(Tabs, { style: { flex: 1, minHeight: 0 } },
|
|
72
74
|
React.createElement(Tabs.Tab, null, "Column"),
|
|
73
75
|
React.createElement(Tabs.Content, null,
|
|
74
|
-
React.createElement(
|
|
76
|
+
React.createElement(Flex, { flexDirection: "row", alignItems: "center", className: "twa:p-2" },
|
|
77
|
+
React.createElement(Box, { className: "twa:text-2" }, "Columns"),
|
|
78
|
+
React.createElement(Box, { className: "twa:flex-3" }),
|
|
79
|
+
React.createElement(AdaptableFormControlTextClear, { value: columnsSearchText, OnTextChange: setColumnsSearchText, placeholder: "Type to search columns", style: { flex: 1 } })),
|
|
80
|
+
React.createElement(NewColumnSelector, { columnFilterText: columnsSearchText, availableColumns: sortableCols, selected: data.ColumnId ? [data.ColumnId] : [], singleSelect: true, onChange: (ids) => {
|
|
75
81
|
props.onChange({
|
|
76
82
|
...data,
|
|
77
83
|
SortedValues: [],
|
|
@@ -39,7 +39,7 @@ export const CustomSortWizard = (props) => {
|
|
|
39
39
|
{
|
|
40
40
|
isValid: (data) => isValidCustomSortColumn(data, allCustomSorts),
|
|
41
41
|
renderSummary: renderCustomSortColumn,
|
|
42
|
-
details: 'Enter Name and select a Column
|
|
42
|
+
details: 'Enter Name and select a Column to Sort',
|
|
43
43
|
render: () => {
|
|
44
44
|
return (React.createElement(Box, { className: "twa:p-2 twa:h-full" },
|
|
45
45
|
React.createElement(CustomSortColumnWizardSection, { isNew: props.isNew, onChange: setCustomSort, allCustomSorts: allCustomSorts })));
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { NewScopeComponent } from '../../Components/NewScopeComponent';
|
|
3
3
|
import { useOnePageAdaptableWizardContext } from '../../Wizard/OnePageAdaptableWizard';
|
|
4
4
|
import { Flex } from '../../../components/Flex';
|
|
5
|
+
import { isScopeColumnIds } from '../../../AdaptableState/Common/ColumnScope';
|
|
5
6
|
export const FlashingAlertScopeWizardSection = (props) => {
|
|
6
7
|
const { data, api } = useOnePageAdaptableWizardContext();
|
|
7
8
|
const availableColumns = React.useMemo(() => api.columnApi.getNonSpecialColumns(), []);
|
|
@@ -12,10 +13,16 @@ export const FlashingAlertScopeWizardSection = (props) => {
|
|
|
12
13
|
}, scopeColumns: availableColumns, scope: data.Scope, updateScope: (Scope) => {
|
|
13
14
|
const newData = { ...data, Scope };
|
|
14
15
|
if (newData.Rule.Predicates) {
|
|
15
|
-
|
|
16
|
-
// if it was set to a predicate before
|
|
16
|
+
const validPredicateIds = new Set(api.flashingCellApi.getFlashingCellPredicateDefsForScope(Scope).map((def) => def.id));
|
|
17
17
|
newData.Rule = {
|
|
18
|
-
Predicates:
|
|
18
|
+
Predicates: newData.Rule.Predicates.filter((p) => validPredicateIds.has(p.PredicateId)).filter((predicate) => {
|
|
19
|
+
// if there are more than 1 column, then we must eliminate the IN/NotIn predicates
|
|
20
|
+
// TODO: this should NOT be required, but the ColumnValueSelector does NOT support creatable values right now
|
|
21
|
+
if (isScopeColumnIds(Scope) && Scope.ColumnIds.length > 1) {
|
|
22
|
+
return predicate.PredicateId !== 'In' && predicate.PredicateId !== 'NotIn';
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}),
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
props.onChange(newData);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { NewScopeComponent, renderScopeSummary } from '../../Components/NewScopeComponent';
|
|
3
3
|
import { useOnePageAdaptableWizardContext } from '../../Wizard/OnePageAdaptableWizard';
|
|
4
|
-
import {
|
|
4
|
+
import { isScopeColumnIds } from '../../../AdaptableState/Common/ColumnScope';
|
|
5
5
|
export const renderFormatColumnScopeSummary = (data) => {
|
|
6
6
|
return renderScopeSummary(data.Scope, {
|
|
7
7
|
scopeWholeRow: 'Matching rows will be formatted',
|
|
@@ -26,12 +26,19 @@ export const FormatColumnScopeWizardSection = (props) => {
|
|
|
26
26
|
newData.Rule.BooleanExpression = '';
|
|
27
27
|
}
|
|
28
28
|
else {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const validPredicateIds = new Set(api.formatColumnApi.internalApi
|
|
30
|
+
.getFormatColumnDefsForScope(Scope)
|
|
31
|
+
.map((def) => def.id));
|
|
32
|
+
newData.Rule = {
|
|
33
|
+
Predicates: newData.Rule.Predicates.filter((p) => validPredicateIds.has(p.PredicateId)).filter((predicate) => {
|
|
34
|
+
// if there are more than 1 column, then we must eliminate the IN/NotIn predicates
|
|
35
|
+
// TODO: this should NOT be required, but the ColumnValueSelector does NOT support creatable values right now
|
|
36
|
+
if (isScopeColumnIds(Scope) && Scope.ColumnIds.length > 1) {
|
|
37
|
+
return predicate.PredicateId !== 'In' && predicate.PredicateId !== 'NotIn';
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
props.onChange(newData);
|
|
@@ -7,9 +7,7 @@ export function FormatColumnRuleWizardSection(props) {
|
|
|
7
7
|
if (data.Target && data.Target === 'columnHeader') {
|
|
8
8
|
return (React.createElement(HelpBlock, { className: "twa:mt-3" }, "Conditions cannot be applied if the Target of the Format Column is Column Header"));
|
|
9
9
|
}
|
|
10
|
-
return (React.createElement(EntityRulesEditor, { module: moduleInfo.ModuleName, defaultPredicateId: props.defaultPredicateId,
|
|
11
|
-
// TODO see what is this
|
|
12
|
-
predicateDefs: api.formatColumnApi.internalApi.getFormatColumnDefsForScope(data.Scope), getPredicateDefsForColId: (colId) => api.formatColumnApi.internalApi.getFormatColumnDefsForScope({ ColumnIds: [colId] }), showNoRule: true, showBoolean: true, showAggregation: false, showObservable: false, showQueryBuilder: true, showPredicate: !api.columnScopeApi.scopeIsAll(data.Scope), data: data, onChange: (formatColumn) => props.onChange(formatColumn), descriptions: {
|
|
10
|
+
return (React.createElement(EntityRulesEditor, { module: moduleInfo.ModuleName, defaultPredicateId: props.defaultPredicateId, predicateDefs: api.formatColumnApi.internalApi.getFormatColumnDefsForScope(data.Scope), getPredicateDefsForColId: (colId) => api.formatColumnApi.internalApi.getFormatColumnDefsForScope({ ColumnIds: [colId] }), showNoRule: true, showBoolean: true, showAggregation: false, showObservable: false, showQueryBuilder: true, showPredicate: !api.columnScopeApi.scopeIsAll(data.Scope), data: data, onChange: (formatColumn) => props.onChange(formatColumn), descriptions: {
|
|
13
11
|
selectPredicate: 'Create a Format Column Rule - to be applied when data changes',
|
|
14
12
|
useBooleanQuery: (React.createElement(React.Fragment, null,
|
|
15
13
|
"Use an BooleanQuery if ",
|
|
@@ -3,9 +3,7 @@ import { useState } from 'react';
|
|
|
3
3
|
import { OnePageAdaptableWizard, OnePageWizardSummary } from '../../Wizard/OnePageAdaptableWizard';
|
|
4
4
|
import { cloneObject } from '../../../Utilities/Helpers/Helper';
|
|
5
5
|
import { FormatColumnScopeWizardSection, renderFormatColumnScopeSummary, } from './FormatColumnColumnScopeWizardSection';
|
|
6
|
-
import { FormatColumnRowScopeWizardSection, renderFormatColumnRowScopeSummary,
|
|
7
|
-
// renderFormatColumnRowScopeSummary,
|
|
8
|
-
} from './FormatColumnRowScopeWizardSection';
|
|
6
|
+
import { FormatColumnRowScopeWizardSection, renderFormatColumnRowScopeSummary, } from './FormatColumnRowScopeWizardSection';
|
|
9
7
|
import { FormatColumnStyleWizardSection, isFormatColumnStyleValid, renderFormatColumnStyleWizardSummary, } from './FormatColumnStyleWizardSection';
|
|
10
8
|
import { FormatColumnFormatWizardSection, getFormatDisplayTypeForScope, renderFormatColumnFormatSummary, } from './FormatColumnFormatWizardSection';
|
|
11
9
|
import { useAdaptable } from '../../AdaptableContext';
|
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { FreeTextColumn } from '../../../AdaptableState/FreeTextColumnState';
|
|
3
3
|
import { AdaptableApi } from '../../../Api/AdaptableApi';
|
|
4
4
|
export declare const renderFreeTextColumnSummary: (data: FreeTextColumn) => React.JSX.Element;
|
|
5
|
-
export declare const isValidFreeTextColumn: (data: FreeTextColumn, api: AdaptableApi) => true | "A Column
|
|
5
|
+
export declare const isValidFreeTextColumn: (data: FreeTextColumn, api: AdaptableApi) => true | "A Column Name is required" | "A column with this Name already exists" | "A data type is required for the column";
|
|
6
6
|
export type FreeTextColumnSettingsWizardSectionProps = {
|
|
7
7
|
onChange: (data: FreeTextColumn) => void;
|
|
8
8
|
isEdit: boolean;
|