@gooddata/sdk-ui-dashboard 11.35.0-alpha.5 → 11.35.0-alpha.6
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/NOTICE +8 -8
- package/esm/__version.d.ts +1 -1
- package/esm/__version.js +1 -1
- package/esm/index.d.ts +3 -4
- package/esm/index.js +1 -2
- package/esm/model/commandHandlers/dashboard/common/parameterHydration.d.ts +11 -2
- package/esm/model/commandHandlers/dashboard/common/parameterHydration.js +21 -0
- package/esm/model/commandHandlers/dashboard/common/stateInitializers.js +44 -79
- package/esm/model/commandHandlers/dashboard/saveAsDashboardHandler.js +10 -5
- package/esm/model/commandHandlers/dashboard/saveDashboardHandler.js +11 -7
- package/esm/model/store/dashboardStore.d.ts +0 -2
- package/esm/model/store/dashboardStore.js +0 -2
- package/esm/model/store/meta/metaSelectors.js +4 -3
- package/esm/model/store/tabs/index.d.ts +12 -0
- package/esm/model/store/tabs/index.js +2 -0
- package/esm/model/store/{parameters → tabs/parameters}/parametersReducers.d.ts +3 -7
- package/esm/model/store/tabs/parameters/parametersReducers.js +46 -0
- package/esm/model/store/tabs/parameters/parametersSelectors.d.ts +72 -0
- package/esm/model/store/tabs/parameters/parametersSelectors.js +228 -0
- package/esm/model/store/{parameters → tabs/parameters}/parametersState.d.ts +11 -1
- package/esm/model/store/tabs/parameters/parametersState.js +20 -0
- package/esm/model/store/tabs/tabsState.d.ts +2 -0
- package/esm/model/store/types.d.ts +0 -6
- package/esm/presentation/dragAndDrop/draggableParameterFilter/DefaultParameterDraggingComponent.js +1 -1
- package/esm/presentation/dragAndDrop/useFilterDeleteDrop.js +2 -2
- package/esm/presentation/filterBar/filterBar/DefaultFilterBar.js +1 -1
- package/esm/presentation/filterBar/parameterFilter/DashboardParameterFilter.js +3 -3
- package/esm/presentation/filterBar/parameterFilter/DashboardParameterPicker.js +3 -3
- package/esm/presentation/widget/insight/ViewModeDashboardInsight/Insight/DashboardInsight.js +1 -1
- package/esm/sdk-ui-dashboard.d.ts +27 -26
- package/package.json +21 -21
- package/esm/model/store/parameters/index.d.ts +0 -12
- package/esm/model/store/parameters/index.js +0 -14
- package/esm/model/store/parameters/parametersReducers.js +0 -36
- package/esm/model/store/parameters/parametersSelectors.d.ts +0 -67
- package/esm/model/store/parameters/parametersSelectors.js +0 -122
- package/esm/model/store/parameters/parametersState.js +0 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { areObjRefsEqual } from "@gooddata/sdk-model";
|
|
3
|
+
import { getActiveTab } from "../tabsState.js";
|
|
4
|
+
import { parametersInitialState } from "./parametersState.js";
|
|
5
|
+
const addParameter = (state, action) => {
|
|
6
|
+
const activeTab = getActiveTab(state);
|
|
7
|
+
if (!activeTab) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const { parameter, workspaceDefault } = action.payload;
|
|
11
|
+
const tabParameters = activeTab.parameters ?? parametersInitialState;
|
|
12
|
+
if (tabParameters.parameters.some((entry) => areObjRefsEqual(entry.parameter.ref, parameter.ref))) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
activeTab.parameters = {
|
|
16
|
+
parameters: [
|
|
17
|
+
...tabParameters.parameters,
|
|
18
|
+
{ parameter, runtimeOverride: parameter.value ?? workspaceDefault },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const setParameterRuntimeValue = (state, action) => {
|
|
23
|
+
const activeTab = getActiveTab(state);
|
|
24
|
+
if (!activeTab?.parameters) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const { ref, value } = action.payload;
|
|
28
|
+
const entry = activeTab.parameters.parameters.find((item) => areObjRefsEqual(item.parameter.ref, ref));
|
|
29
|
+
if (entry) {
|
|
30
|
+
entry.runtimeOverride = value;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const removeParameter = (state, action) => {
|
|
34
|
+
const activeTab = getActiveTab(state);
|
|
35
|
+
if (!activeTab?.parameters) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
activeTab.parameters = {
|
|
39
|
+
parameters: activeTab.parameters.parameters.filter((entry) => !areObjRefsEqual(entry.parameter.ref, action.payload.ref)),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const parametersReducers = {
|
|
43
|
+
addParameter,
|
|
44
|
+
setParameterRuntimeValue,
|
|
45
|
+
removeParameter,
|
|
46
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type IDashboardParameter, type IInsightParameterValue, type ObjRef } from "@gooddata/sdk-model";
|
|
2
|
+
import { type DashboardSelector } from "../../types.js";
|
|
3
|
+
import { type IDashboardParameterEntry } from "./parametersState.js";
|
|
4
|
+
/**
|
|
5
|
+
* Returns the persisted-shape parameter entries currently held by the active tab.
|
|
6
|
+
*
|
|
7
|
+
* @alpha
|
|
8
|
+
*/
|
|
9
|
+
export declare const selectDashboardParameters: DashboardSelector<IDashboardParameter[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Returns currently active parameter references on the active tab.
|
|
12
|
+
*
|
|
13
|
+
* @alpha
|
|
14
|
+
*/
|
|
15
|
+
export declare const selectActiveParameterRefKeys: DashboardSelector<ReadonlySet<string>>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the full per-parameter entries (persisted shape + ephemeral `runtimeOverride`) for the
|
|
18
|
+
* active tab.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export declare const selectDashboardParameterEntries: DashboardSelector<IDashboardParameterEntry[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Returns a selector that yields the entry held by the active tab for a given parameter ref,
|
|
25
|
+
* or `undefined` if no such entry exists.
|
|
26
|
+
*
|
|
27
|
+
* @alpha
|
|
28
|
+
*/
|
|
29
|
+
export declare const selectDashboardParameterEntryByRef: (ref: ObjRef) => DashboardSelector<IDashboardParameterEntry | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Returns a selector that yields the current `runtimeOverride` for a given parameter ref on the
|
|
32
|
+
* active tab, or `undefined` if the active tab does not hold an entry for that ref.
|
|
33
|
+
*
|
|
34
|
+
* @alpha
|
|
35
|
+
*/
|
|
36
|
+
export declare const selectParameterRuntimeOverrideByRef: (ref: ObjRef) => DashboardSelector<number | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Computes the dashboard parameters keyed by tab `localIdentifier` in the shape that would be
|
|
39
|
+
* persisted on save right now.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Smart persistence applies independently per tab: `value` is dropped when equal to the
|
|
43
|
+
* workspace default, `label` is dropped when equal to the parameter title, all per tab.
|
|
44
|
+
* Non-resolved entries (catalog not loaded, gated off, ref missing) are emitted verbatim from
|
|
45
|
+
* the previously persisted entry on the same tab when available. V1 fallback applies when no
|
|
46
|
+
* tab in the persisted dashboard carries `parameters` — the persisted root array is used as the
|
|
47
|
+
* persistence source for every tab.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare const selectSmartPersistedTabsParameters: DashboardSelector<Record<string, IDashboardParameter[]>>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if the dashboard parameters that would be persisted differ from the persisted
|
|
54
|
+
* version on any tab.
|
|
55
|
+
*
|
|
56
|
+
* @alpha
|
|
57
|
+
*/
|
|
58
|
+
export declare const selectIsParametersChanged: DashboardSelector<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Returns the parameter values to inject into the widget's `IExecutionConfig.parameterValues`.
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* The widget's owning tab is resolved from layout, then the result is the intersection of that
|
|
64
|
+
* tab's parameter entries and the parameters referenced by the widget's insight (per
|
|
65
|
+
* `insightParameters`). Dashboard parameters not referenced by the widget's insight are excluded
|
|
66
|
+
* so that adding/removing unrelated parameters does not invalidate the widget's `defFingerprint`.
|
|
67
|
+
* Returns an empty array when `enableParameters` is off so persisted parameter values cannot
|
|
68
|
+
* silently affect execution while the UI is hidden.
|
|
69
|
+
*
|
|
70
|
+
* @alpha
|
|
71
|
+
*/
|
|
72
|
+
export declare const selectEffectiveParameterValuesForWidget: (ref: ObjRef | undefined) => DashboardSelector<IInsightParameterValue[]>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { createSelector } from "@reduxjs/toolkit";
|
|
3
|
+
import { isEqual } from "lodash-es";
|
|
4
|
+
import { areObjRefsEqual, insightParameters, isDashboardLayout, isInsightWidget, isNumberParameterDefinition, isVisualizationSwitcherWidget, objRefToString, } from "@gooddata/sdk-model";
|
|
5
|
+
import { createMemoizedSelector } from "../../_infra/selectors.js";
|
|
6
|
+
import { selectCatalogParameters, selectCatalogParametersIsLoaded } from "../../catalog/catalogSelectors.js";
|
|
7
|
+
import { selectEnableParameters } from "../../config/configSelectors.js";
|
|
8
|
+
import { selectInsightsMap } from "../../insights/insightsSelectors.js";
|
|
9
|
+
import { selectActiveTab, selectTabs } from "../tabsSelectors.js";
|
|
10
|
+
import { DEFAULT_TAB_ID } from "../tabsState.js";
|
|
11
|
+
import { parametersInitialState, pickTabParametersSource, } from "./parametersState.js";
|
|
12
|
+
const EMPTY_PARAMETERS = [];
|
|
13
|
+
const EMPTY_TABS = [];
|
|
14
|
+
const selectParametersState = createSelector(selectActiveTab, (activeTab) => activeTab?.parameters ?? parametersInitialState);
|
|
15
|
+
const selectPersistedParametersFromMeta = (state) => state.meta?.persistedDashboard?.parameters ?? EMPTY_PARAMETERS;
|
|
16
|
+
const selectPersistedDashboardTabsRaw = (state) => state.meta?.persistedDashboard?.tabs ?? EMPTY_TABS;
|
|
17
|
+
/**
|
|
18
|
+
* Returns the persisted-shape parameter entries currently held by the active tab.
|
|
19
|
+
*
|
|
20
|
+
* @alpha
|
|
21
|
+
*/
|
|
22
|
+
export const selectDashboardParameters = createSelector(selectParametersState, (state) => state.parameters.map((entry) => entry.parameter));
|
|
23
|
+
/**
|
|
24
|
+
* Returns currently active parameter references on the active tab.
|
|
25
|
+
*
|
|
26
|
+
* @alpha
|
|
27
|
+
*/
|
|
28
|
+
export const selectActiveParameterRefKeys = createSelector(selectDashboardParameters, (parameters) => new Set(parameters.map((parameter) => objRefToString(parameter.ref))));
|
|
29
|
+
/**
|
|
30
|
+
* Returns the full per-parameter entries (persisted shape + ephemeral `runtimeOverride`) for the
|
|
31
|
+
* active tab.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export const selectDashboardParameterEntries = createSelector(selectParametersState, (state) => state.parameters);
|
|
36
|
+
/**
|
|
37
|
+
* Returns a selector that yields the entry held by the active tab for a given parameter ref,
|
|
38
|
+
* or `undefined` if no such entry exists.
|
|
39
|
+
*
|
|
40
|
+
* @alpha
|
|
41
|
+
*/
|
|
42
|
+
export const selectDashboardParameterEntryByRef = createMemoizedSelector((ref) => createSelector(selectParametersState, (state) => state.parameters.find((item) => areObjRefsEqual(item.parameter.ref, ref))));
|
|
43
|
+
/**
|
|
44
|
+
* Returns a selector that yields the current `runtimeOverride` for a given parameter ref on the
|
|
45
|
+
* active tab, or `undefined` if the active tab does not hold an entry for that ref.
|
|
46
|
+
*
|
|
47
|
+
* @alpha
|
|
48
|
+
*/
|
|
49
|
+
export const selectParameterRuntimeOverrideByRef = createMemoizedSelector((ref) => createSelector(selectDashboardParameterEntryByRef(ref), (entry) => entry?.runtimeOverride));
|
|
50
|
+
/**
|
|
51
|
+
* Computes the dashboard parameters keyed by tab `localIdentifier` in the shape that would be
|
|
52
|
+
* persisted on save right now.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Smart persistence applies independently per tab: `value` is dropped when equal to the
|
|
56
|
+
* workspace default, `label` is dropped when equal to the parameter title, all per tab.
|
|
57
|
+
* Non-resolved entries (catalog not loaded, gated off, ref missing) are emitted verbatim from
|
|
58
|
+
* the previously persisted entry on the same tab when available. V1 fallback applies when no
|
|
59
|
+
* tab in the persisted dashboard carries `parameters` — the persisted root array is used as the
|
|
60
|
+
* persistence source for every tab.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export const selectSmartPersistedTabsParameters = createSelector(selectTabs, selectCatalogParameters, selectCatalogParametersIsLoaded, selectPersistedDashboardTabsRaw, selectPersistedParametersFromMeta, (tabs, workspaceParameters, isCatalogLoaded, persistedTabs, rootPersistedParameters) => {
|
|
65
|
+
const result = {};
|
|
66
|
+
if (!tabs) {
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
const workspaceByRef = new Map(workspaceParameters.map((wp) => [objRefToString(wp.ref), wp]));
|
|
70
|
+
const persistedByTabAndRef = buildPersistedByTabAndRef(persistedTabs, rootPersistedParameters);
|
|
71
|
+
for (const tab of tabs) {
|
|
72
|
+
const entries = tab.parameters?.parameters ?? [];
|
|
73
|
+
const persistedForTab = persistedByTabAndRef.get(tab.localIdentifier) ?? new Map();
|
|
74
|
+
result[tab.localIdentifier] = entries.map((entry) => {
|
|
75
|
+
const refKey = objRefToString(entry.parameter.ref);
|
|
76
|
+
const workspaceParameter = isCatalogLoaded ? workspaceByRef.get(refKey) : undefined;
|
|
77
|
+
if (!workspaceParameter) {
|
|
78
|
+
return persistedForTab.get(refKey) ?? entry.parameter;
|
|
79
|
+
}
|
|
80
|
+
return smartPersistResolvedEntry(entry, workspaceParameter);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Returns persisted parameters from `meta.persistedDashboard`, keyed by tab `localIdentifier`.
|
|
87
|
+
* Honors V1 → per-tab migration: when no tab in the persisted dashboard carries `parameters`,
|
|
88
|
+
* the persisted root `parameters` is used as fallback for every tab. For legacy single-tab
|
|
89
|
+
* dashboards (no `tabs[]` in persistedDashboard), the synthetic `DEFAULT_TAB_ID` tab carries
|
|
90
|
+
* the persisted root parameters.
|
|
91
|
+
*
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
const selectPersistedTabsParametersFromMeta = createSelector(selectPersistedDashboardTabsRaw, selectPersistedParametersFromMeta, (persistedTabs, rootPersistedParameters) => {
|
|
95
|
+
const result = {};
|
|
96
|
+
if (persistedTabs.length === 0) {
|
|
97
|
+
result[DEFAULT_TAB_ID] = rootPersistedParameters;
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
for (const tab of persistedTabs) {
|
|
101
|
+
result[tab.localIdentifier] =
|
|
102
|
+
pickTabParametersSource(tab, persistedTabs, rootPersistedParameters) ?? EMPTY_PARAMETERS;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* Returns true if the dashboard parameters that would be persisted differ from the persisted
|
|
108
|
+
* version on any tab.
|
|
109
|
+
*
|
|
110
|
+
* @alpha
|
|
111
|
+
*/
|
|
112
|
+
export const selectIsParametersChanged = createSelector(selectSmartPersistedTabsParameters, selectPersistedTabsParametersFromMeta, (smartPersistedByTab, persistedByTab) => {
|
|
113
|
+
const allTabIds = new Set([...Object.keys(smartPersistedByTab), ...Object.keys(persistedByTab)]);
|
|
114
|
+
for (const tabId of allTabIds) {
|
|
115
|
+
const smart = smartPersistedByTab[tabId] ?? EMPTY_PARAMETERS;
|
|
116
|
+
const persisted = persistedByTab[tabId] ?? EMPTY_PARAMETERS;
|
|
117
|
+
if (!isEqual(smart, persisted)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* Returns the parameter values to inject into the widget's `IExecutionConfig.parameterValues`.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* The widget's owning tab is resolved from layout, then the result is the intersection of that
|
|
128
|
+
* tab's parameter entries and the parameters referenced by the widget's insight (per
|
|
129
|
+
* `insightParameters`). Dashboard parameters not referenced by the widget's insight are excluded
|
|
130
|
+
* so that adding/removing unrelated parameters does not invalidate the widget's `defFingerprint`.
|
|
131
|
+
* Returns an empty array when `enableParameters` is off so persisted parameter values cannot
|
|
132
|
+
* silently affect execution while the UI is hidden.
|
|
133
|
+
*
|
|
134
|
+
* @alpha
|
|
135
|
+
*/
|
|
136
|
+
export const selectEffectiveParameterValuesForWidget = createMemoizedSelector((ref) => createSelector(selectParameterExecutionContextByWidgetRef(ref), selectInsightsMap, selectEnableParameters, (context, insights, isEnabled) => {
|
|
137
|
+
if (!isEnabled || !context) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const insight = insights.get(context.widget.insight);
|
|
141
|
+
if (!insight) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const entries = context.tab.parameters?.parameters ?? parametersInitialState.parameters;
|
|
145
|
+
const referencedRefs = new Set(insightParameters(insight).map((parameter) => objRefToString(parameter.ref)));
|
|
146
|
+
const result = [];
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (entry.runtimeOverride === undefined) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (referencedRefs.has(objRefToString(entry.parameter.ref))) {
|
|
152
|
+
result.push({ ref: entry.parameter.ref, value: entry.runtimeOverride });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}));
|
|
157
|
+
const selectParameterExecutionContextByWidgetRef = createMemoizedSelector((ref) => createSelector(selectTabs, (tabs) => findParameterExecutionContext(tabs, ref)));
|
|
158
|
+
function findParameterExecutionContext(tabs, ref) {
|
|
159
|
+
if (!ref || !tabs) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
for (const tab of tabs) {
|
|
163
|
+
const layout = tab.layout?.layout;
|
|
164
|
+
if (!layout) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const widget = findInsightWidgetInLayout(layout, ref);
|
|
168
|
+
if (widget) {
|
|
169
|
+
return { tab, widget };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
function findInsightWidgetInLayout(layout, ref) {
|
|
175
|
+
for (const section of layout.sections) {
|
|
176
|
+
for (const item of section.items) {
|
|
177
|
+
const widget = item.widget;
|
|
178
|
+
if (!widget) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (isInsightWidget(widget) && areObjRefsEqual(widget.ref, ref)) {
|
|
182
|
+
return widget;
|
|
183
|
+
}
|
|
184
|
+
if (isDashboardLayout(widget)) {
|
|
185
|
+
const nestedWidget = findInsightWidgetInLayout(widget, ref);
|
|
186
|
+
if (nestedWidget) {
|
|
187
|
+
return nestedWidget;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (isVisualizationSwitcherWidget(widget)) {
|
|
191
|
+
const visualization = widget.visualizations.find((visualization) => areObjRefsEqual(visualization.ref, ref));
|
|
192
|
+
if (visualization) {
|
|
193
|
+
return visualization;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
function buildPersistedByTabAndRef(persistedTabs, rootPersistedParameters) {
|
|
201
|
+
const result = new Map();
|
|
202
|
+
for (const tab of persistedTabs) {
|
|
203
|
+
const sourceParameters = pickTabParametersSource(tab, persistedTabs, rootPersistedParameters) ?? EMPTY_PARAMETERS;
|
|
204
|
+
result.set(tab.localIdentifier, new Map(sourceParameters.map((parameter) => [objRefToString(parameter.ref), parameter])));
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
function smartPersistResolvedEntry(entry, workspaceParameter) {
|
|
209
|
+
const workspaceDefault = isNumberParameterDefinition(workspaceParameter.definition)
|
|
210
|
+
? workspaceParameter.definition.defaultValue
|
|
211
|
+
: undefined;
|
|
212
|
+
const result = {
|
|
213
|
+
ref: entry.parameter.ref,
|
|
214
|
+
parameterType: entry.parameter.parameterType,
|
|
215
|
+
mode: entry.parameter.mode,
|
|
216
|
+
...labelOverride(entry, workspaceParameter),
|
|
217
|
+
};
|
|
218
|
+
if (entry.runtimeOverride === undefined || entry.runtimeOverride === workspaceDefault) {
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
return { ...result, value: entry.runtimeOverride };
|
|
222
|
+
}
|
|
223
|
+
function labelOverride(entry, workspaceParameter) {
|
|
224
|
+
if (entry.parameter.label && entry.parameter.label !== workspaceParameter.title) {
|
|
225
|
+
return { label: entry.parameter.label };
|
|
226
|
+
}
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type IDashboardParameter } from "@gooddata/sdk-model";
|
|
1
|
+
import { type IDashboardParameter, type IDashboardTab } from "@gooddata/sdk-model";
|
|
2
2
|
/**
|
|
3
3
|
* Per-parameter state tracked by the dashboard store.
|
|
4
4
|
*
|
|
@@ -26,3 +26,13 @@ export interface IParametersState {
|
|
|
26
26
|
parameters: IDashboardParameterEntry[];
|
|
27
27
|
}
|
|
28
28
|
export declare const parametersInitialState: IParametersState;
|
|
29
|
+
/**
|
|
30
|
+
* Picks a tab's persisted parameter source under the V1 → per-tab migration rule.
|
|
31
|
+
*
|
|
32
|
+
* - If the tab has its own `parameters` (including `[]`) → return it.
|
|
33
|
+
* - Else if every tab's `parameters === undefined` AND root `parameters` is defined → root array.
|
|
34
|
+
* - Else → `undefined`.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickTabParametersSource(tab: IDashboardTab, allTabs: ReadonlyArray<IDashboardTab>, rootParameters: IDashboardParameter[] | undefined): IDashboardParameter[] | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
export const parametersInitialState = {
|
|
3
|
+
parameters: [],
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Picks a tab's persisted parameter source under the V1 → per-tab migration rule.
|
|
7
|
+
*
|
|
8
|
+
* - If the tab has its own `parameters` (including `[]`) → return it.
|
|
9
|
+
* - Else if every tab's `parameters === undefined` AND root `parameters` is defined → root array.
|
|
10
|
+
* - Else → `undefined`.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export function pickTabParametersSource(tab, allTabs, rootParameters) {
|
|
15
|
+
if (tab.parameters !== undefined) {
|
|
16
|
+
return tab.parameters;
|
|
17
|
+
}
|
|
18
|
+
const everyTabUndefined = allTabs.length > 0 && allTabs.every((other) => other.parameters === undefined);
|
|
19
|
+
return everyTabUndefined ? rootParameters : undefined;
|
|
20
|
+
}
|
|
@@ -6,6 +6,7 @@ import { type IDateFilterConfigsState } from "./dateFilterConfigs/dateFilterConf
|
|
|
6
6
|
import type { FilterContextState } from "./filterContext/filterContextState.js";
|
|
7
7
|
import type { ILayoutState } from "./layout/layoutState.js";
|
|
8
8
|
import { type IMeasureValueFilterConfigsState } from "./measureValueFilterConfigs/measureValueFilterConfigsState.js";
|
|
9
|
+
import { type IParametersState } from "./parameters/parametersState.js";
|
|
9
10
|
/**
|
|
10
11
|
* Identifier used for dashboards without explicit tabs support and the first tab created automatically.
|
|
11
12
|
*
|
|
@@ -36,6 +37,7 @@ export interface ITabState {
|
|
|
36
37
|
filterContext?: FilterContextState;
|
|
37
38
|
filterGroupsConfig?: IDashboardFilterGroupsConfig;
|
|
38
39
|
layout?: ILayoutState;
|
|
40
|
+
parameters?: IParametersState;
|
|
39
41
|
/**
|
|
40
42
|
* UI-only flag indicating the tab is currently being renamed.
|
|
41
43
|
* @internal
|
|
@@ -17,7 +17,6 @@ import { type IFilterViewsState } from "./filterViews/filterViewsState.js";
|
|
|
17
17
|
import { type ILoadingState } from "./loading/loadingState.js";
|
|
18
18
|
import { type IDashboardMetaState } from "./meta/metaState.js";
|
|
19
19
|
import { type INotificationChannelsState } from "./notificationChannels/notificationChannelsState.js";
|
|
20
|
-
import { type IParametersState } from "./parameters/parametersState.js";
|
|
21
20
|
import { type PermissionsState } from "./permissions/permissionsState.js";
|
|
22
21
|
import { type IRenderModeState } from "./renderMode/renderModeState.js";
|
|
23
22
|
import { type SavingState } from "./saving/savingState.js";
|
|
@@ -75,11 +74,6 @@ export type DashboardState = {
|
|
|
75
74
|
users: IUsersState;
|
|
76
75
|
/** @alpha */
|
|
77
76
|
notificationChannels: INotificationChannelsState;
|
|
78
|
-
/**
|
|
79
|
-
* Dashboard-level parameter overrides slice.
|
|
80
|
-
* @alpha
|
|
81
|
-
*/
|
|
82
|
-
parameters: IParametersState;
|
|
83
77
|
/**
|
|
84
78
|
* Internal state for dashboard summary AI workflow.
|
|
85
79
|
*
|
package/esm/presentation/dragAndDrop/draggableParameterFilter/DefaultParameterDraggingComponent.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { ParameterControlButton } from "@gooddata/sdk-ui-kit";
|
|
4
4
|
import { useDashboardSelector } from "../../../model/react/DashboardStoreProvider.js";
|
|
5
5
|
import { selectCatalogParameterByRef } from "../../../model/store/catalog/catalogSelectors.js";
|
|
6
|
-
import { selectDashboardParameterEntryByRef } from "../../../model/store/parameters/parametersSelectors.js";
|
|
6
|
+
import { selectDashboardParameterEntryByRef } from "../../../model/store/tabs/parameters/parametersSelectors.js";
|
|
7
7
|
/**
|
|
8
8
|
* Renders the floating preview of a parameter chip while it is being dragged.
|
|
9
9
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { dashboardAttributeFilterItemLocalIdentifier, dashboardFilterLocalIdentifier, } from "@gooddata/sdk-model";
|
|
3
3
|
import { removeAttributeFilter, removeDateFilter, removeMeasureValueFilter, } from "../../model/commands/filters.js";
|
|
4
4
|
import { useDashboardDispatch } from "../../model/react/DashboardStoreProvider.js";
|
|
5
|
-
import {
|
|
5
|
+
import { tabsActions } from "../../model/store/tabs/index.js";
|
|
6
6
|
import { isAttributeFilterDraggableItem, isDateFilterDraggableItem, isMeasureValueFilterDraggableItem, isParameterDraggableItem, } from "./types.js";
|
|
7
7
|
import { useDashboardDrop } from "./useDashboardDrop.js";
|
|
8
8
|
/**
|
|
@@ -27,7 +27,7 @@ export function useFilterDeleteDrop() {
|
|
|
27
27
|
dispatch(removeMeasureValueFilter(identifier));
|
|
28
28
|
}
|
|
29
29
|
else if (isParameterDraggableItem(item)) {
|
|
30
|
-
dispatch(
|
|
30
|
+
dispatch(tabsActions.removeParameter({ ref: item.ref }));
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
}, [dispatch]);
|
|
@@ -10,7 +10,6 @@ import { changeAttributeFilterSelection, changeDateFilterSelection, changeMeasur
|
|
|
10
10
|
import { useDashboardDispatch, useDashboardSelector } from "../../../model/react/DashboardStoreProvider.js";
|
|
11
11
|
import { selectSupportsElementUris } from "../../../model/store/backendCapabilities/backendCapabilitiesSelectors.js";
|
|
12
12
|
import { selectEnableDashboardFilterGroups, selectEnableDateFilterIdentifiers, selectEnableMeasureValueFilterKD, selectEnableParameters, selectIsApplyFiltersAllAtOnceEnabledAndSet, selectIsExport, } from "../../../model/store/config/configSelectors.js";
|
|
13
|
-
import { selectDashboardParameters } from "../../../model/store/parameters/parametersSelectors.js";
|
|
14
13
|
import { selectIsInEditMode } from "../../../model/store/renderMode/renderModeSelectors.js";
|
|
15
14
|
import { selectEffectiveAttributeFiltersModeMap } from "../../../model/store/tabs/attributeFilterConfigs/attributeFilterConfigsSelectors.js";
|
|
16
15
|
import { selectEffectiveDateFilterAvailableGranularities, selectEffectiveDateFilterMode, selectEffectiveDateFilterOptions, } from "../../../model/store/tabs/dateFilterConfig/dateFilterConfigSelectors.js";
|
|
@@ -18,6 +17,7 @@ import { selectEffectiveDateFiltersModeMap } from "../../../model/store/tabs/dat
|
|
|
18
17
|
import { selectCanAddMoreFilters, selectFilterContextFilters, selectWorkingFilterContextFilters, } from "../../../model/store/tabs/filterContext/filterContextSelectors.js";
|
|
19
18
|
import { selectFilterGroupsConfig } from "../../../model/store/tabs/filterGroups/filterGroupsSelectors.js";
|
|
20
19
|
import { selectEffectiveMeasureValueFiltersModeMap } from "../../../model/store/tabs/measureValueFilterConfigs/measureValueFilterConfigsSelectors.js";
|
|
20
|
+
import { selectDashboardParameters } from "../../../model/store/tabs/parameters/parametersSelectors.js";
|
|
21
21
|
import { useDashboardComponentsContext } from "../../dashboardContexts/DashboardComponentsContext.js";
|
|
22
22
|
import { DraggableFilterDropZone } from "../../dragAndDrop/draggableFilterDropZone/DraggableFilterDropZone.js";
|
|
23
23
|
import { DraggableFilterDropZoneHint } from "../../dragAndDrop/draggableFilterDropZone/DraggableFilterDropZoneHint.js";
|
|
@@ -3,9 +3,9 @@ import { DashboardParameterModeValues, isNumberParameterDefinition, objRefToStri
|
|
|
3
3
|
import { Dropdown, ParameterControlButton, ParameterControlDropdown } from "@gooddata/sdk-ui-kit";
|
|
4
4
|
import { useDashboardDispatch, useDashboardSelector } from "../../../model/react/DashboardStoreProvider.js";
|
|
5
5
|
import { selectCatalogParameterByRef } from "../../../model/store/catalog/catalogSelectors.js";
|
|
6
|
-
import { parametersActions } from "../../../model/store/parameters/index.js";
|
|
7
|
-
import { selectParameterRuntimeOverrideByRef } from "../../../model/store/parameters/parametersSelectors.js";
|
|
8
6
|
import { selectIsInEditMode } from "../../../model/store/renderMode/renderModeSelectors.js";
|
|
7
|
+
import { tabsActions } from "../../../model/store/tabs/index.js";
|
|
8
|
+
import { selectParameterRuntimeOverrideByRef } from "../../../model/store/tabs/parameters/parametersSelectors.js";
|
|
9
9
|
import { DraggableChipSource } from "../../dragAndDrop/DraggableChipSource.js";
|
|
10
10
|
/**
|
|
11
11
|
* Renders a chip for a single dashboard parameter.
|
|
@@ -30,7 +30,7 @@ export function DashboardParameterFilter({ parameter }) {
|
|
|
30
30
|
return (_jsx(DraggableChipSource, { dragItem: dragItem, canDrag: isInEditMode, children: _jsx(ParameterControlButton, { name: name, value: runtimeOverride, isActive: false, isDraggable: isInEditMode, "data-testid": `dashboard-parameter-${objRefToString(parameter.ref)}` }) }));
|
|
31
31
|
}
|
|
32
32
|
return (_jsx(DraggableChipSource, { dragItem: dragItem, canDrag: isInEditMode, children: _jsx(Dropdown, { renderButton: ({ isOpen, toggleDropdown }) => (_jsx(ParameterControlButton, { name: name, value: runtimeOverride, isActive: isOpen, isDraggable: isInEditMode, onClick: () => toggleDropdown(), "data-testid": `dashboard-parameter-${objRefToString(parameter.ref)}` })), renderBody: ({ closeDropdown }) => (_jsx(ParameterControlDropdown, { name: name, value: runtimeOverride, constraints: constraints, onApply: (value) => {
|
|
33
|
-
dispatch(
|
|
33
|
+
dispatch(tabsActions.setParameterRuntimeValue({ ref: parameter.ref, value }));
|
|
34
34
|
closeDropdown();
|
|
35
35
|
}, onCancel: closeDropdown })) }) }));
|
|
36
36
|
}
|
|
@@ -3,8 +3,8 @@ import { DashboardParameterModeValues, isIdentifierRef } from "@gooddata/sdk-mod
|
|
|
3
3
|
import { ParameterPicker } from "@gooddata/sdk-ui-kit";
|
|
4
4
|
import { useDashboardDispatch, useDashboardSelector } from "../../../model/react/DashboardStoreProvider.js";
|
|
5
5
|
import { selectCatalogParameters, selectCatalogParametersStatus, } from "../../../model/store/catalog/catalogSelectors.js";
|
|
6
|
-
import {
|
|
7
|
-
import { selectActiveParameterRefKeys } from "../../../model/store/parameters/parametersSelectors.js";
|
|
6
|
+
import { tabsActions } from "../../../model/store/tabs/index.js";
|
|
7
|
+
import { selectActiveParameterRefKeys } from "../../../model/store/tabs/parameters/parametersSelectors.js";
|
|
8
8
|
const PICKER_MAX_LIST_HEIGHT = 320;
|
|
9
9
|
/**
|
|
10
10
|
* Connected wrapper around sdk-ui-kit's `ParameterPicker`.
|
|
@@ -22,7 +22,7 @@ export function DashboardParameterPicker({ onAdd, onCancel }) {
|
|
|
22
22
|
if (!isIdentifierRef(ref)) {
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
|
-
dispatch(
|
|
25
|
+
dispatch(tabsActions.addParameter({
|
|
26
26
|
parameter: {
|
|
27
27
|
ref,
|
|
28
28
|
parameterType: "NUMBER",
|
package/esm/presentation/widget/insight/ViewModeDashboardInsight/Insight/DashboardInsight.js
CHANGED
|
@@ -14,9 +14,9 @@ import { useWidgetExecutionsHandler } from "../../../../../model/react/useWidget
|
|
|
14
14
|
import { useWidgetFilters } from "../../../../../model/react/useWidgetFilters.js";
|
|
15
15
|
import { selectAgGridToken, selectColorPalette, selectEnableExecutionCancelling, selectEnableSnapshotExportAccessibility, selectIsExport, selectLocale, selectMapboxToken, selectMaxZoomLevel, selectSeparators, selectSettings, } from "../../../../../model/store/config/configSelectors.js";
|
|
16
16
|
import { selectCrossFilteringSelectedPointsByWidgetRef, selectDrillableItems, } from "../../../../../model/store/drill/drillSelectors.js";
|
|
17
|
-
import { selectEffectiveParameterValuesForWidget } from "../../../../../model/store/parameters/parametersSelectors.js";
|
|
18
17
|
import { selectPermissions } from "../../../../../model/store/permissions/permissionsSelectors.js";
|
|
19
18
|
import { selectIsInEditMode, selectIsInExportMode, } from "../../../../../model/store/renderMode/renderModeSelectors.js";
|
|
19
|
+
import { selectEffectiveParameterValuesForWidget } from "../../../../../model/store/tabs/parameters/parametersSelectors.js";
|
|
20
20
|
import { selectExecutionTimestamp } from "../../../../../model/store/ui/uiSelectors.js";
|
|
21
21
|
import { useDashboardComponentsContext } from "../../../../dashboardContexts/DashboardComponentsContext.js";
|
|
22
22
|
import { useMinimalSizeValidation } from "../../../../export/hooks/useMinimalSizeValidation.js";
|
|
@@ -3215,11 +3215,6 @@ export declare type DashboardState = {
|
|
|
3215
3215
|
users: IUsersState;
|
|
3216
3216
|
/** @alpha */
|
|
3217
3217
|
notificationChannels: INotificationChannelsState;
|
|
3218
|
-
/**
|
|
3219
|
-
* Dashboard-level parameter overrides slice.
|
|
3220
|
-
* @alpha
|
|
3221
|
-
*/
|
|
3222
|
-
parameters: IParametersState;
|
|
3223
3218
|
/**
|
|
3224
3219
|
* Internal state for dashboard summary AI workflow.
|
|
3225
3220
|
*
|
|
@@ -5352,7 +5347,7 @@ export declare interface IAddMeasureValueFilterPayload {
|
|
|
5352
5347
|
}
|
|
5353
5348
|
|
|
5354
5349
|
/**
|
|
5355
|
-
* Add a parameter to the
|
|
5350
|
+
* Add a parameter to the active tab. Initial `runtimeOverride` is `parameter.value`
|
|
5356
5351
|
* (when pinned) otherwise the workspace default supplied by the caller.
|
|
5357
5352
|
*
|
|
5358
5353
|
* @alpha
|
|
@@ -18847,6 +18842,7 @@ export declare interface ITabState {
|
|
|
18847
18842
|
filterContext?: FilterContextState;
|
|
18848
18843
|
filterGroupsConfig?: IDashboardFilterGroupsConfig;
|
|
18849
18844
|
layout?: ILayoutState;
|
|
18845
|
+
parameters?: IParametersState;
|
|
18850
18846
|
/**
|
|
18851
18847
|
* UI-only flag indicating the tab is currently being renamed.
|
|
18852
18848
|
* @internal
|
|
@@ -20875,16 +20871,6 @@ export declare type ParameterDraggableItem = {
|
|
|
20875
20871
|
ref: ObjRef;
|
|
20876
20872
|
};
|
|
20877
20873
|
|
|
20878
|
-
/**
|
|
20879
|
-
* @internal
|
|
20880
|
-
*/
|
|
20881
|
-
export declare const parametersActions: {
|
|
20882
|
-
addParameter: ActionCreatorWithPayload<IAddParameterPayload, "parameters/addParameter">;
|
|
20883
|
-
setParameterRuntimeValue: ActionCreatorWithPayload<ISetParameterRuntimeValuePayload, "parameters/setParameterRuntimeValue">;
|
|
20884
|
-
removeParameter: ActionCreatorWithPayload<IRemoveParameterPayload, "parameters/removeParameter">;
|
|
20885
|
-
setParameterEntries: ActionCreatorWithPayload<IDashboardParameterEntry[], "parameters/setParameterEntries">;
|
|
20886
|
-
};
|
|
20887
|
-
|
|
20888
20874
|
/**
|
|
20889
20875
|
* @beta
|
|
20890
20876
|
*/
|
|
@@ -23146,14 +23132,15 @@ export declare const selectDashboardLockedFilters: DashboardSelector<FilterConte
|
|
|
23146
23132
|
export declare const selectDashboardLockStatus: DashboardSelector<boolean>;
|
|
23147
23133
|
|
|
23148
23134
|
/**
|
|
23149
|
-
* Returns the full per-parameter entries (persisted shape + ephemeral `runtimeOverride`)
|
|
23135
|
+
* Returns the full per-parameter entries (persisted shape + ephemeral `runtimeOverride`) for the
|
|
23136
|
+
* active tab.
|
|
23150
23137
|
*
|
|
23151
23138
|
* @internal
|
|
23152
23139
|
*/
|
|
23153
23140
|
export declare const selectDashboardParameterEntries: DashboardSelector<IDashboardParameterEntry[]>;
|
|
23154
23141
|
|
|
23155
23142
|
/**
|
|
23156
|
-
* Returns the persisted-shape parameter entries currently held by the
|
|
23143
|
+
* Returns the persisted-shape parameter entries currently held by the active tab.
|
|
23157
23144
|
*
|
|
23158
23145
|
* @alpha
|
|
23159
23146
|
*/
|
|
@@ -23624,11 +23611,12 @@ export declare const selectEffectiveDateFilterTitle: DashboardSelector<string |
|
|
|
23624
23611
|
* Returns the parameter values to inject into the widget's `IExecutionConfig.parameterValues`.
|
|
23625
23612
|
*
|
|
23626
23613
|
* @remarks
|
|
23627
|
-
* The
|
|
23628
|
-
*
|
|
23629
|
-
*
|
|
23630
|
-
*
|
|
23631
|
-
*
|
|
23614
|
+
* The widget's owning tab is resolved from layout, then the result is the intersection of that
|
|
23615
|
+
* tab's parameter entries and the parameters referenced by the widget's insight (per
|
|
23616
|
+
* `insightParameters`). Dashboard parameters not referenced by the widget's insight are excluded
|
|
23617
|
+
* so that adding/removing unrelated parameters does not invalidate the widget's `defFingerprint`.
|
|
23618
|
+
* Returns an empty array when `enableParameters` is off so persisted parameter values cannot
|
|
23619
|
+
* silently affect execution while the UI is hidden.
|
|
23632
23620
|
*
|
|
23633
23621
|
* @alpha
|
|
23634
23622
|
*/
|
|
@@ -25008,7 +24996,8 @@ export declare const selectIsNewDashboard: DashboardSelector<boolean>;
|
|
|
25008
24996
|
export declare const selectIsNewDashboardWithContent: DashboardSelector<boolean>;
|
|
25009
24997
|
|
|
25010
24998
|
/**
|
|
25011
|
-
* Returns true if the dashboard parameters that would be persisted differ from the persisted
|
|
24999
|
+
* Returns true if the dashboard parameters that would be persisted differ from the persisted
|
|
25000
|
+
* version on any tab.
|
|
25012
25001
|
*
|
|
25013
25002
|
* @alpha
|
|
25014
25003
|
*/
|
|
@@ -25399,8 +25388,8 @@ export declare const selectOtherContextAttributeFilterItems: (ref?: ObjRef) => D
|
|
|
25399
25388
|
export declare const selectOtherContextAttributeFilters: (ref?: ObjRef) => DashboardSelector<IDashboardAttributeFilter[]>;
|
|
25400
25389
|
|
|
25401
25390
|
/**
|
|
25402
|
-
* Returns a selector that yields the current `runtimeOverride` for a given parameter ref
|
|
25403
|
-
* or `undefined` if the
|
|
25391
|
+
* Returns a selector that yields the current `runtimeOverride` for a given parameter ref on the
|
|
25392
|
+
* active tab, or `undefined` if the active tab does not hold an entry for that ref.
|
|
25404
25393
|
*
|
|
25405
25394
|
* @alpha
|
|
25406
25395
|
*/
|
|
@@ -27004,6 +26993,18 @@ readonly removeMeasureValueFilterConfig: (state: WritableDraft<ITabsState>, acti
|
|
|
27004
26993
|
payload: string;
|
|
27005
26994
|
type: string;
|
|
27006
26995
|
}) => void | ITabsState | WritableDraft<ITabsState>;
|
|
26996
|
+
readonly addParameter: (state: WritableDraft<ITabsState>, action: {
|
|
26997
|
+
payload: IAddParameterPayload;
|
|
26998
|
+
type: string;
|
|
26999
|
+
}) => void | ITabsState | WritableDraft<ITabsState>;
|
|
27000
|
+
readonly setParameterRuntimeValue: (state: WritableDraft<ITabsState>, action: {
|
|
27001
|
+
payload: ISetParameterRuntimeValuePayload;
|
|
27002
|
+
type: string;
|
|
27003
|
+
}) => void | ITabsState | WritableDraft<ITabsState>;
|
|
27004
|
+
readonly removeParameter: (state: WritableDraft<ITabsState>, action: {
|
|
27005
|
+
payload: IRemoveParameterPayload;
|
|
27006
|
+
type: string;
|
|
27007
|
+
}) => void | ITabsState | WritableDraft<ITabsState>;
|
|
27007
27008
|
}, "tabs">;
|
|
27008
27009
|
|
|
27009
27010
|
/**
|