@gooddata/sdk-ui-filters 11.41.0-alpha.2 → 11.41.0-alpha.4
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/esm/AttributeFilter/hooks/types.d.ts +8 -1
- package/esm/AttributeFilter/hooks/useAttributeFilterController.js +40 -3
- package/esm/AttributeFilter/hooks/useAttributeFilterHandlerMethods.d.ts +1 -1
- package/esm/AttributeFilter/hooks/useElementsFilterController.js +48 -10
- package/esm/AttributeFilterHandler/internal/bridge.d.ts +1 -1
- package/esm/AttributeFilterHandler/internal/bridge.js +2 -1
- package/esm/AttributeFilterHandler/internal/loader.d.ts +1 -1
- package/esm/AttributeFilterHandler/internal/loader.js +2 -2
- package/esm/AttributeFilterHandler/internal/redux/init/initReducers.d.ts +1 -0
- package/esm/AttributeFilterHandler/internal/redux/init/initSaga.js +9 -2
- package/esm/AttributeFilterHandler/internal/redux/store/rootReducers.d.ts +1 -0
- package/esm/AttributeFilterHandler/internal/redux/store/slice.d.ts +1 -0
- package/esm/AttributeFilterHandler/types/attributeFilterLoader.d.ts +3 -1
- package/esm/FilterGroup/FilterGroup.js +27 -11
- package/esm/MeasureValueFilter/ConditionInputSection.d.ts +0 -1
- package/esm/MeasureValueFilter/ConditionInputSection.js +4 -4
- package/esm/MeasureValueFilter/Dropdown.js +1 -1
- package/esm/MeasureValueFilter/DropdownBody.d.ts +0 -1
- package/esm/MeasureValueFilter/DropdownBody.js +60 -15
- package/esm/MeasureValueFilter/MeasureValueFilterDropdownActions.js +10 -3
- package/esm/MeasureValueFilter/OperatorDropdownBody.js +4 -1
- package/esm/MeasureValueFilter/RangeInput.js +2 -2
- package/esm/sdk-ui-filters.d.ts +10 -1
- package/esm/tsdoc-metadata.json +1 -1
- package/package.json +13 -13
- package/styles/css/attributeFilter.css +57 -0
- package/styles/css/attributeFilter.css.map +1 -1
- package/styles/css/attributeFilterNext/attributeFilterDropdownButton.css +57 -0
- package/styles/css/attributeFilterNext/attributeFilterDropdownButton.css.map +1 -1
- package/styles/css/attributeFilterNext.css +57 -0
- package/styles/css/attributeFilterNext.css.map +1 -1
- package/styles/css/filterGroup.css +12 -2
- package/styles/css/filterGroup.css.map +1 -1
- package/styles/css/main.css +69 -2
- package/styles/css/main.css.map +1 -1
- package/styles/scss/filterGroup.scss +20 -2
|
@@ -375,6 +375,13 @@ export type SelectionTypeControllerCallbacks = {
|
|
|
375
375
|
* @internal
|
|
376
376
|
*/
|
|
377
377
|
resetForModeSwitch?: (newFilter: IAttributeFilter, newDisplayAsLabel?: ObjRef) => void;
|
|
378
|
+
/**
|
|
379
|
+
* Imperatively read the current working (not yet applied) elements selection as a filter.
|
|
380
|
+
* Used to snapshot the List selection before a mode switch so it can be restored later.
|
|
381
|
+
*
|
|
382
|
+
* @internal
|
|
383
|
+
*/
|
|
384
|
+
getWorkingElementsFilter?: () => IAttributeFilter | undefined;
|
|
378
385
|
};
|
|
379
386
|
/**
|
|
380
387
|
* AttributeFilter controller callbacks.
|
|
@@ -393,7 +400,7 @@ export type AttributeFilterController = AttributeFilterControllerData & Attribut
|
|
|
393
400
|
*
|
|
394
401
|
* @internal
|
|
395
402
|
*/
|
|
396
|
-
export type ElementsFilterController = CommonFilterControllerData & ElementsFilterControllerData & CommonFilterControllerCallbacks & ElementsFilterControllerCallbacks & Pick<SelectionTypeControllerCallbacks, "setDisplayForm" | "resetForModeSwitch">;
|
|
403
|
+
export type ElementsFilterController = CommonFilterControllerData & ElementsFilterControllerData & CommonFilterControllerCallbacks & ElementsFilterControllerCallbacks & Pick<SelectionTypeControllerCallbacks, "setDisplayForm" | "resetForModeSwitch" | "getWorkingElementsFilter">;
|
|
397
404
|
/**
|
|
398
405
|
* Text mode controller return type.
|
|
399
406
|
* Only text-specific data and callbacks; root controller merges with common data and elements stubs.
|
|
@@ -40,10 +40,28 @@ export const useAttributeFilterController = (props) => {
|
|
|
40
40
|
const supportsSingleSelectDependentFilters = backend.capabilities.supportsSingleSelectDependentFilters;
|
|
41
41
|
const { filter: resolvedFilter, setConnectedPlaceholderValue } = useResolveFilterInput(filterInput ?? workingFilter, connectToPlaceholder);
|
|
42
42
|
const [selectionType, setSelectionType] = useState(() => getSelectionTypeFromFilter(resolvedFilter) ?? "elements");
|
|
43
|
+
// Only re-sync the selection type when the resolved (applied) filter's TYPE actually changes.
|
|
44
|
+
// Re-syncing on every resolvedFilter reference change would clobber an in-progress mode switch
|
|
45
|
+
// whenever the consumer re-renders with a new (but same-type) filter object — e.g. an app that
|
|
46
|
+
// recomputes the filter from its own state each render. That reverted the just-clicked mode and
|
|
47
|
+
// forced the user to switch List <-> Text twice.
|
|
48
|
+
const lastResolvedSelectionTypeRef = useRef(getSelectionTypeFromFilter(resolvedFilter) ?? "elements");
|
|
43
49
|
useEffect(() => {
|
|
44
|
-
|
|
50
|
+
const nextSelectionType = getSelectionTypeFromFilter(resolvedFilter) ?? "elements";
|
|
51
|
+
if (nextSelectionType !== lastResolvedSelectionTypeRef.current) {
|
|
52
|
+
lastResolvedSelectionTypeRef.current = nextSelectionType;
|
|
53
|
+
setSelectionType(nextSelectionType);
|
|
54
|
+
}
|
|
45
55
|
}, [resolvedFilter]);
|
|
46
56
|
const isTextSelectionType = selectionType === "text";
|
|
57
|
+
// Remembers the List (elements) selection captured when leaving List mode, so switching
|
|
58
|
+
// List -> Text -> List can restore it (keys and inverted flag) instead of resetting to "All".
|
|
59
|
+
// Seeded from the initial filter; updated with the live working selection on each switch to Text.
|
|
60
|
+
const lastElementsFilterRef = useRef(resolvedFilter &&
|
|
61
|
+
!isArbitraryAttributeFilter(resolvedFilter) &&
|
|
62
|
+
!isMatchAttributeFilter(resolvedFilter)
|
|
63
|
+
? resolvedFilter
|
|
64
|
+
: undefined);
|
|
47
65
|
const originalSelectionType = getSelectionTypeFromFilter(resolvedFilter);
|
|
48
66
|
const selectionTypeChanged = originalSelectionType !== selectionType;
|
|
49
67
|
// The display form from the filter definition itself. For elements filters this is
|
|
@@ -253,7 +271,7 @@ export const useAttributeFilterController = (props) => {
|
|
|
253
271
|
elementsResetForModeSwitch,
|
|
254
272
|
textResetForModeSwitch,
|
|
255
273
|
]);
|
|
256
|
-
const { currentDisplayFormRef: elementsCurrentDisplayFormRef } = elementsFilterController;
|
|
274
|
+
const { currentDisplayFormRef: elementsCurrentDisplayFormRef, getWorkingElementsFilter } = elementsFilterController;
|
|
257
275
|
const onSelectionTypeChangeForControllers = useCallback((newMode) => {
|
|
258
276
|
if (!resolvedDisplayFormRef) {
|
|
259
277
|
return;
|
|
@@ -263,6 +281,12 @@ export const useAttributeFilterController = (props) => {
|
|
|
263
281
|
return;
|
|
264
282
|
}
|
|
265
283
|
if (newMode === "text") {
|
|
284
|
+
// Leaving List: snapshot the current working element selection (including changes made
|
|
285
|
+
// since the last Apply) so switching back to List restores exactly what was on screen.
|
|
286
|
+
const workingElementsFilter = getWorkingElementsFilter?.();
|
|
287
|
+
if (workingElementsFilter) {
|
|
288
|
+
lastElementsFilterRef.current = workingElementsFilter;
|
|
289
|
+
}
|
|
266
290
|
if (availableTextSelectionTypes.includes("arbitrary")) {
|
|
267
291
|
nextAvailableMode = "arbitrary";
|
|
268
292
|
}
|
|
@@ -275,7 +299,12 @@ export const useAttributeFilterController = (props) => {
|
|
|
275
299
|
? (userSelectedDisplayForm ?? elementsCurrentDisplayFormRef)
|
|
276
300
|
: elementsCurrentDisplayFormRef;
|
|
277
301
|
const displayAsDisplayFormForNewFilter = newMode === "text" ? undefined : userSelectedDisplayForm;
|
|
278
|
-
|
|
302
|
+
// Switching back to List: restore the previously applied element selection (keys + inverted
|
|
303
|
+
// flag) when we have it, so a List -> Text -> List round-trip preserves the user's selection
|
|
304
|
+
// instead of resetting to "All" (and, with stale committed elements, inverting it).
|
|
305
|
+
const restoredElementsFilter = newMode === "text" ? undefined : lastElementsFilterRef.current;
|
|
306
|
+
const newFilter = restoredElementsFilter ??
|
|
307
|
+
createEmptyFilterForAvailableSelectionType(nextAvailableMode, displayFormForNewFilter, localId);
|
|
279
308
|
handleSelectionTypeChange(newFilter, displayAsDisplayFormForNewFilter, selectionType, newMode);
|
|
280
309
|
}, [
|
|
281
310
|
availableTextSelectionTypes,
|
|
@@ -285,6 +314,7 @@ export const useAttributeFilterController = (props) => {
|
|
|
285
314
|
userSelectedDisplayForm,
|
|
286
315
|
elementsCurrentDisplayFormRef,
|
|
287
316
|
resolvedDisplayFormRef,
|
|
317
|
+
getWorkingElementsFilter,
|
|
288
318
|
]);
|
|
289
319
|
const { textFilterOperator, textFilterValues, textFilterLiteral, textFilterCaseSensitive, onCommitTextFilter, isApplyDisabled: isTextApplyDisabled, } = textFilterController;
|
|
290
320
|
const onApplyTextFilter = useCallback((applyRegardlessWithoutApplySetting = false, _applyToWorkingOnly = false) => {
|
|
@@ -364,11 +394,18 @@ export const useAttributeFilterController = (props) => {
|
|
|
364
394
|
onResetText?.();
|
|
365
395
|
onResetElements();
|
|
366
396
|
setSelectionType(getSelectionTypeFromFilter(resolvedFilter) ?? "elements");
|
|
397
|
+
// The remembered List selection is only meaningful within a single open session (List -> Text
|
|
398
|
+
// -> List). Clear it on close so a snapshot captured before a Text filter was applied is not
|
|
399
|
+
// reused on the next open, which would push the stale elements selection and undo the applied
|
|
400
|
+
// text state. Leaving List re-captures the working selection, so this is safe.
|
|
401
|
+
lastElementsFilterRef.current = undefined;
|
|
367
402
|
}, [onResetText, onResetElements, resolvedFilter]);
|
|
368
403
|
// Wrap elements onReset to also revert selectionType on dropdown close without Apply.
|
|
369
404
|
const onResetElementsMode = useCallback(() => {
|
|
370
405
|
onResetElements();
|
|
371
406
|
setSelectionType(getSelectionTypeFromFilter(resolvedFilter) ?? "elements");
|
|
407
|
+
// See onResetTextMode: the remembered List selection is session-scoped, clear it on close.
|
|
408
|
+
lastElementsFilterRef.current = undefined;
|
|
372
409
|
}, [onResetElements, resolvedFilter]);
|
|
373
410
|
if (isTextSelectionType) {
|
|
374
411
|
return {
|
|
@@ -3,7 +3,7 @@ import { type IMultiSelectAttributeFilterHandler } from "../../AttributeFilterHa
|
|
|
3
3
|
* @internal
|
|
4
4
|
*/
|
|
5
5
|
export declare const useAttributeFilterHandlerMethods: (handler: IMultiSelectAttributeFilterHandler) => {
|
|
6
|
-
init: (correlation?: string | undefined, skipElementsLoading?: boolean | undefined) => void;
|
|
6
|
+
init: (correlation?: string | undefined, skipElementsLoading?: boolean | undefined, preserveWorkingSelection?: boolean | undefined) => void;
|
|
7
7
|
onInitStart: import("../../index.js").CallbackRegistration<{
|
|
8
8
|
correlation: string;
|
|
9
9
|
}>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { debounce, difference, differenceBy, isEmpty, isEqual } from "lodash-es";
|
|
5
5
|
import { invariant } from "ts-invariant";
|
|
6
|
-
import { areObjRefsEqual, filterAttributeElements, filterObjRef, isArbitraryAttributeFilter, isAttributeElementsByRef, isAttributeElementsByValue, isAttributeFilterWithSelection, isMatchAttributeFilter, isNegativeAttributeFilter, isPositiveAttributeFilter, objRefToString, } from "@gooddata/sdk-model";
|
|
6
|
+
import { areObjRefsEqual, filterAttributeElements, filterLocalIdentifier, filterObjRef, isArbitraryAttributeFilter, isAttributeElementsByRef, isAttributeElementsByValue, isAttributeFilterWithSelection, isMatchAttributeFilter, isNegativeAttributeFilter, isPositiveAttributeFilter, newNegativeAttributeFilter, newPositiveAttributeFilter, objRefToString, } from "@gooddata/sdk-model";
|
|
7
7
|
import { UnexpectedSdkError } from "@gooddata/sdk-ui";
|
|
8
8
|
import { isValidSingleSelectionFilter } from "../utils.js";
|
|
9
9
|
import { DISPLAY_FORM_CHANGED_CORRELATION, IRRELEVANT_SELECTION, MAX_SELECTION_SIZE, PARENT_FILTERS_CORRELATION, RESET_CORRELATION, SEARCH_CORRELATION, SHOW_FILTERED_ELEMENTS_CORRELATION, } from "./constants.js";
|
|
@@ -158,6 +158,11 @@ function refreshByType(handler, change, supportsKeepingDependentFiltersSelection
|
|
|
158
158
|
if (change === "init-self") {
|
|
159
159
|
handler.init();
|
|
160
160
|
}
|
|
161
|
+
if (change === "init-self-preserve-selection") {
|
|
162
|
+
// Load the element options without re-deriving (and clobbering) the working selection
|
|
163
|
+
// that resetForModeSwitch just restored.
|
|
164
|
+
handler.init(undefined, false, true);
|
|
165
|
+
}
|
|
161
166
|
}
|
|
162
167
|
function isPrimaryLabelUsed(filter, displayForms) {
|
|
163
168
|
const primaryDisplayForm = displayForms.find((df) => df.isPrimary);
|
|
@@ -278,9 +283,10 @@ function useInitOrReload(handler, props, supportsKeepingDependentFiltersSelectio
|
|
|
278
283
|
return (!areFiltersEqual(filter, handler.getFilter()) &&
|
|
279
284
|
!areFiltersEqual(filter, handler.getFilterToDisplay()));
|
|
280
285
|
};
|
|
281
|
-
// When text→elements mode switch happens, resetForModeSwitch already committed
|
|
282
|
-
//
|
|
283
|
-
//
|
|
286
|
+
// When text→elements mode switch happens, resetForModeSwitch already committed the target
|
|
287
|
+
// selection (restored List selection or "All") to the handler. Suppress filterChanged so we
|
|
288
|
+
// don't override that reset with the stale filter prop (parent's onChange may not have
|
|
289
|
+
// propagated yet, so filter can still hold the previous mode's selection).
|
|
284
290
|
const justSwitchedFromText = prevIsTextModeRef.current && !isTextMode;
|
|
285
291
|
prevIsTextModeRef.current = isTextMode;
|
|
286
292
|
const filterChanged = justSwitchedFromText || selectionTypeChanged ? false : getFilterChanged(filter, handler);
|
|
@@ -307,10 +313,11 @@ function useInitOrReload(handler, props, supportsKeepingDependentFiltersSelectio
|
|
|
307
313
|
let change = resetOnParentFilterChange
|
|
308
314
|
? updateAutomaticResettingFilter(handler, updateProps, supportsCircularDependencyInFilters)
|
|
309
315
|
: updateNonResettingFilter(handler, updateProps, supportsKeepingDependentFiltersSelection);
|
|
310
|
-
//
|
|
311
|
-
// trigger
|
|
316
|
+
// Switching back to elements mode from text mode: elements were not loaded in text mode, so
|
|
317
|
+
// trigger an init to populate the list — but preserve the working selection that
|
|
318
|
+
// resetForModeSwitch restored instead of re-deriving it from the stale filter prop.
|
|
312
319
|
if (justSwitchedFromText) {
|
|
313
|
-
change = "init-self";
|
|
320
|
+
change = "init-self-preserve-selection";
|
|
314
321
|
}
|
|
315
322
|
refreshByType(handler, change, supportsKeepingDependentFiltersSelection, shouldReloadElements, setShouldReloadElements);
|
|
316
323
|
}, [
|
|
@@ -642,8 +649,7 @@ export function useElementsFilterController(props) {
|
|
|
642
649
|
handler.init(DISPLAY_FORM_CHANGED_CORRELATION, isTextMode);
|
|
643
650
|
}, [handler, isTextMode]);
|
|
644
651
|
const resetForModeSwitch = useCallback((newFilter, newDisplayAsLabel) => {
|
|
645
|
-
|
|
646
|
-
if (!handler || !isNegativeAttributeFilter(newFilter)) {
|
|
652
|
+
if (!handler) {
|
|
647
653
|
return;
|
|
648
654
|
}
|
|
649
655
|
const displayFormRef = filterObjRef(newFilter);
|
|
@@ -652,9 +658,40 @@ export function useElementsFilterController(props) {
|
|
|
652
658
|
handler.setDisplayAsLabel(newDisplayAsLabel);
|
|
653
659
|
}
|
|
654
660
|
handler.setSearch("");
|
|
655
|
-
|
|
661
|
+
// Apply the target filter's actual selection (keys + inverted flag) to the WORKING
|
|
662
|
+
// selection. For an empty negative filter this resets to "All"; for a restored List filter
|
|
663
|
+
// it reinstates the previously displayed element selection.
|
|
664
|
+
const elements = filterAttributeElements(newFilter);
|
|
665
|
+
const keys = elements
|
|
666
|
+
? isAttributeElementsByValue(elements)
|
|
667
|
+
? elements.values
|
|
668
|
+
: elements.uris
|
|
669
|
+
: [];
|
|
670
|
+
handler.changeSelection({ keys, isInverted: isNegativeAttributeFilter(newFilter) });
|
|
671
|
+
// Only commit in the withoutApply flow. With a deferred Apply the committed selection must
|
|
672
|
+
// stay equal to the applied filter prop — committing here would make the filter-prop
|
|
673
|
+
// re-sync (updateFilter) see a difference and clobber the just-restored working selection.
|
|
656
674
|
withoutApply && handler.commitSelection();
|
|
657
675
|
}, [handler, withoutApply]);
|
|
676
|
+
// Builds an elements filter from the current working (not yet applied) selection. Used to snapshot
|
|
677
|
+
// the List selection before switching to Text so it can be restored on switch-back, including any
|
|
678
|
+
// changes the user made after the last Apply.
|
|
679
|
+
const getWorkingElementsFilter = useCallback(() => {
|
|
680
|
+
if (!handler) {
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
const committedFilter = handler.getFilter();
|
|
684
|
+
const committedElements = filterAttributeElements(committedFilter);
|
|
685
|
+
const { keys, isInverted } = handler.getWorkingSelection();
|
|
686
|
+
const elements = isAttributeElementsByValue(committedElements)
|
|
687
|
+
? { values: keys }
|
|
688
|
+
: { uris: keys };
|
|
689
|
+
const displayForm = filterObjRef(committedFilter);
|
|
690
|
+
const localIdentifier = filterLocalIdentifier(committedFilter);
|
|
691
|
+
return isInverted
|
|
692
|
+
? newNegativeAttributeFilter(displayForm, elements, localIdentifier)
|
|
693
|
+
: newPositiveAttributeFilter(displayForm, elements, localIdentifier);
|
|
694
|
+
}, [handler]);
|
|
658
695
|
const callbacks = useCallbacks(handler, {
|
|
659
696
|
onApply: onApply ?? (() => { }),
|
|
660
697
|
onChange: onChange ?? (() => { }),
|
|
@@ -688,5 +725,6 @@ export function useElementsFilterController(props) {
|
|
|
688
725
|
...forcedInitErrorProp,
|
|
689
726
|
setDisplayForm,
|
|
690
727
|
resetForModeSwitch,
|
|
728
|
+
getWorkingElementsFilter,
|
|
691
729
|
};
|
|
692
730
|
}
|
|
@@ -17,7 +17,7 @@ export declare class AttributeFilterReduxBridge {
|
|
|
17
17
|
private callbacks;
|
|
18
18
|
constructor(config: AttributeFilterHandlerConfig);
|
|
19
19
|
private initializeBridge;
|
|
20
|
-
init: (correlation: string, skipElementsLoading?: boolean | undefined) => void;
|
|
20
|
+
init: (correlation: string, skipElementsLoading?: boolean | undefined, preserveWorkingSelection?: boolean | undefined) => void;
|
|
21
21
|
getInitStatus: () => AsyncOperationStatus;
|
|
22
22
|
getInitError: () => GoodDataSdkError | undefined;
|
|
23
23
|
onInitStart: CallbackRegistration<OnInitStartCallbackPayload>;
|
|
@@ -33,10 +33,11 @@ export class AttributeFilterReduxBridge {
|
|
|
33
33
|
//
|
|
34
34
|
// Init
|
|
35
35
|
//
|
|
36
|
-
init = (correlation, skipElementsLoading) => {
|
|
36
|
+
init = (correlation, skipElementsLoading, preserveWorkingSelection) => {
|
|
37
37
|
this.redux.dispatch(actions.init({
|
|
38
38
|
correlation,
|
|
39
39
|
skipElementsLoading,
|
|
40
|
+
preserveWorkingSelection,
|
|
40
41
|
}));
|
|
41
42
|
};
|
|
42
43
|
getInitStatus = () => {
|
|
@@ -15,7 +15,7 @@ export declare class AttributeFilterLoader implements IAttributeFilterLoader {
|
|
|
15
15
|
protected config: AttributeFilterHandlerConfig;
|
|
16
16
|
protected constructor(config: AttributeFilterHandlerConfig);
|
|
17
17
|
private validateStaticElementsLoad;
|
|
18
|
-
init: (correlation?: string, skipElementsLoading?: boolean | undefined) => void;
|
|
18
|
+
init: (correlation?: string, skipElementsLoading?: boolean | undefined, preserveWorkingSelection?: boolean | undefined) => void;
|
|
19
19
|
onInitStart: CallbackRegistration<OnInitStartCallbackPayload>;
|
|
20
20
|
onInitSuccess: CallbackRegistration<OnInitSuccessCallbackPayload>;
|
|
21
21
|
onInitError: CallbackRegistration<OnInitErrorCallbackPayload>;
|
|
@@ -21,9 +21,9 @@ export class AttributeFilterLoader {
|
|
|
21
21
|
//
|
|
22
22
|
// Init
|
|
23
23
|
//
|
|
24
|
-
init = (correlation = uuid(), skipElementsLoading) => {
|
|
24
|
+
init = (correlation = uuid(), skipElementsLoading, preserveWorkingSelection) => {
|
|
25
25
|
this.validateStaticElementsLoad();
|
|
26
|
-
this.bridge.init(correlation, skipElementsLoading);
|
|
26
|
+
this.bridge.init(correlation, skipElementsLoading, preserveWorkingSelection);
|
|
27
27
|
};
|
|
28
28
|
onInitStart = (cb) => {
|
|
29
29
|
return this.bridge.onInitStart(cb);
|
|
@@ -18,7 +18,7 @@ import { initTotalCountSaga } from "./initTotalCount.js";
|
|
|
18
18
|
export function* initWorker() {
|
|
19
19
|
yield takeLatest(actions.init.match, initSaga);
|
|
20
20
|
}
|
|
21
|
-
function* initSaga({ payload: { correlation, skipElementsLoading = false }, }) {
|
|
21
|
+
function* initSaga({ payload: { correlation, skipElementsLoading = false, preserveWorkingSelection = false }, }) {
|
|
22
22
|
try {
|
|
23
23
|
yield put(actions.initStart({ correlation }));
|
|
24
24
|
if (skipElementsLoading) {
|
|
@@ -36,7 +36,14 @@ function* initSaga({ payload: { correlation, skipElementsLoading = false }, }) {
|
|
|
36
36
|
const loadTotal = !isLimitingAttributeFiltersEmpty(limitingFilters) ||
|
|
37
37
|
limitingValidationItems.length > 0 ||
|
|
38
38
|
limitingDateFilters.length > 0;
|
|
39
|
-
|
|
39
|
+
// When switching back to elements mode from text mode, the working/committed selection has
|
|
40
|
+
// already been restored by resetForModeSwitch and its elements are still cached. Re-running
|
|
41
|
+
// initSelectionSaga would re-derive the selection from the (stale, last-applied) filter prop
|
|
42
|
+
// and clobber a pending working selection, so skip it. The element options list is
|
|
43
|
+
// still populated by initAttributeElementsPageSaga.
|
|
44
|
+
const sagas = preserveWorkingSelection
|
|
45
|
+
? [initAttributeElementsPageSaga]
|
|
46
|
+
: [initSelectionSaga, initAttributeElementsPageSaga];
|
|
40
47
|
if (hiddenElements?.length > 0) {
|
|
41
48
|
// the rest need the attribute already loaded for the hiddenElements to work
|
|
42
49
|
yield call(initAttributeSaga, correlation);
|
|
@@ -36,8 +36,10 @@ export interface IAttributeFilterLoader extends IAttributeLoader, IAttributeElem
|
|
|
36
36
|
*
|
|
37
37
|
* @param correlation - correlation that will be included in all callbacks fired by this method
|
|
38
38
|
* @param skipElementsLoading - when true, only loads attribute metadata (skips elements, selection, total count)
|
|
39
|
+
* @param preserveWorkingSelection - when true, keeps the current working/committed selection instead of
|
|
40
|
+
* re-deriving it from the filter (used when switching back to elements mode from text mode)
|
|
39
41
|
*/
|
|
40
|
-
init(correlation?: Correlation, skipElementsLoading?: boolean): void;
|
|
42
|
+
init(correlation?: Correlation, skipElementsLoading?: boolean, preserveWorkingSelection?: boolean): void;
|
|
41
43
|
/**
|
|
42
44
|
* Returns the current status of the initialization.
|
|
43
45
|
*/
|
|
@@ -100,6 +100,11 @@ export function FilterGroup(props) {
|
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
function AttributeFilterComponent(attributeFilterProps) {
|
|
103
|
+
const isMobile = useMediaQuery("mobileDevice");
|
|
104
|
+
// On mobile, render filters inside the group same as standard mobile attribute filter row.
|
|
105
|
+
const InnerDropdownButton = isMobile
|
|
106
|
+
? AttributeFilterDropdownButton
|
|
107
|
+
: FilterGroupItem;
|
|
103
108
|
// When the filter swaps between Loading / Error / DropdownButton renderings,
|
|
104
109
|
// the focused DOM node is unmounted. Capture focus on ref-cleanup and
|
|
105
110
|
// re-apply it when the replacement mounts, so the user doesn't visually lose focus.
|
|
@@ -125,14 +130,17 @@ export function FilterGroup(props) {
|
|
|
125
130
|
}, []);
|
|
126
131
|
const DropdownButtonComponent = useCallback(function DropdownButtonComponent({ buttonRef, ...props }) {
|
|
127
132
|
const titleExtension = getTitleExtension?.(filterIdentifier, props.title);
|
|
128
|
-
const CustomDropdownButtonComponent = attributeFilterProps.DropdownButtonComponent ??
|
|
129
|
-
FilterGroupItem;
|
|
133
|
+
const CustomDropdownButtonComponent = attributeFilterProps.DropdownButtonComponent ?? InnerDropdownButton;
|
|
130
134
|
const handleButtonRef = useMergeRefs(buttonRef, setFilterItemRef);
|
|
131
|
-
return (_jsx(CustomDropdownButtonComponent, { ...props, titleExtension: _jsxs(_Fragment, { children: [props.titleExtension, titleExtension, _jsx(FilterButtonCustomIcon, { customIcon: props.customIcon, disabled: props.disabled })
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
return (_jsx(CustomDropdownButtonComponent, { ...props, titleExtension: _jsxs(_Fragment, { children: [props.titleExtension, titleExtension, isMobile ? null : (_jsx(FilterButtonCustomIcon, { customIcon: props.customIcon, disabled: props.disabled }))] }), buttonRef: handleButtonRef, isOpen: isMobile ? true : props.isOpen }));
|
|
136
|
+
}, [
|
|
137
|
+
attributeFilterProps.DropdownButtonComponent,
|
|
138
|
+
InnerDropdownButton,
|
|
139
|
+
setFilterItemRef,
|
|
140
|
+
isMobile,
|
|
141
|
+
]);
|
|
142
|
+
const LoadingComponent = useCallback(() => (_jsx(InnerDropdownButton, { isLoading: true, buttonRef: setFilterItemRef, isOpen: isMobile ? true : undefined })), [setFilterItemRef, InnerDropdownButton, isMobile]);
|
|
143
|
+
const ErrorComponent = useCallback(() => (_jsx(InnerDropdownButton, { isError: true, buttonRef: setFilterItemRef, isOpen: isMobile ? true : undefined })), [setFilterItemRef, InnerDropdownButton, isMobile]);
|
|
136
144
|
const ElementsSearchBarComponent = useCallback((props) => (_jsx(AttributeFilterElementsSearchBar, { ...props, onKeyDown: (e) => {
|
|
137
145
|
// allow space key to be handled by filter search bar
|
|
138
146
|
// and not stolen by filter group dropdown keyboard navigation
|
|
@@ -153,14 +161,20 @@ export function FilterGroup(props) {
|
|
|
153
161
|
return;
|
|
154
162
|
}
|
|
155
163
|
function MeasureValueFilterComponent(measureValueFilterProps) {
|
|
164
|
+
const isMobile = useMediaQuery("mobileDevice");
|
|
156
165
|
const setFilterItemRef = useCallback((element) => {
|
|
157
166
|
filterItemRefs.current.set(filterIdentifier, element);
|
|
158
167
|
}, []);
|
|
159
|
-
const
|
|
168
|
+
const FilterGroupItemDropdownButton = useCallback(function DropdownButtonComponent({ buttonTitle, buttonSubtitle, buttonTitleExtension, disabled, isActive, onClick, }) {
|
|
160
169
|
const titleExtension = getTitleExtension?.(filterIdentifier, buttonTitle);
|
|
161
170
|
return (_jsx(FilterGroupItem, { title: buttonTitle, subtitle: buttonSubtitle ?? undefined, isOpen: isActive, isLoaded: true, disabled: disabled, titleExtension: _jsxs(_Fragment, { children: [buttonTitleExtension, titleExtension] }), onClick: onClick, buttonRef: setFilterItemRef }));
|
|
162
171
|
}, [setFilterItemRef]);
|
|
163
|
-
|
|
172
|
+
// On mobile, fall back to MeasureValueFilter's own default dropdown button (a
|
|
173
|
+
// standard mobile filter row) instead of FilterGroupItem.
|
|
174
|
+
const DropdownButtonComponent = isMobile
|
|
175
|
+
? measureValueFilterProps.DropdownButtonComponent
|
|
176
|
+
: (measureValueFilterProps.DropdownButtonComponent ?? FilterGroupItemDropdownButton);
|
|
177
|
+
return (_jsx(MeasureValueFilter, { ...measureValueFilterProps, alignPoints: ITEM_ALIGN_POINTS, DropdownButtonComponent: DropdownButtonComponent }));
|
|
164
178
|
}
|
|
165
179
|
result.set(filterIdentifier, MeasureValueFilterComponent);
|
|
166
180
|
});
|
|
@@ -198,12 +212,14 @@ export function FilterGroup(props) {
|
|
|
198
212
|
}, [getFilterIdentifier]);
|
|
199
213
|
const groupAriaLabel = intl.formatMessage({ id: "filterGroup.aria.label" }, { title });
|
|
200
214
|
const groupAriaLabelWithState = intl.formatMessage({ id: "filterGroup.aria.label.withState" }, { title, state: subtitle });
|
|
201
|
-
const renderBody = useCallback(({ isMobile, closeDropdown }) => (_jsx("div", { role: "dialog", "aria-label": groupAriaLabel, onKeyDownCapture: handleKeyDownCapture, onKeyDown: handleKeyDown, children: _jsx(DropdownList, { className: "gd-filter-group-body", items: filters, maxHeight: 450, itemHeight: 53, accessibilityConfig: {
|
|
215
|
+
const renderBody = useCallback(({ isMobile, closeDropdown }) => (_jsx("div", { role: "dialog", "aria-label": groupAriaLabel, onKeyDownCapture: handleKeyDownCapture, onKeyDown: handleKeyDown, children: _jsx(DropdownList, { className: cx("gd-filter-group-body", { "gd-is-mobile": isMobile }), items: filters, maxHeight: 450, itemHeight: isMobile ? 47 : 53, accessibilityConfig: {
|
|
202
216
|
role: "list",
|
|
203
217
|
ariaLabel: groupAriaLabel,
|
|
204
218
|
}, renderItem: renderItem, onKeyDownSelect: handleItemKeyboardAction, closeDropdown: closeDropdown, isMobile: isMobile }) })), [filters, renderItem, handleKeyDown, handleKeyDownCapture, handleItemKeyboardAction, groupAriaLabel]);
|
|
205
219
|
const isMobile = useMediaQuery("mobileDevice");
|
|
206
|
-
const renderButton = useCallback(({ toggleDropdown, isOpen, buttonRef, dropdownId }) => (_jsx("div", { className: cx({
|
|
220
|
+
const renderButton = useCallback(({ toggleDropdown, isOpen, buttonRef, dropdownId }) => (_jsx("div", { className: cx({
|
|
221
|
+
"gd-attribute-filter-mobile-button-wrapper gd-is-mobile gd-is-mobile--with-menu": isMobile && isOpen,
|
|
222
|
+
}), children: _jsx(AttributeFilterDropdownButton, { title: title, subtitle: subtitle, isLoaded: !isAnyFilterError, isOpen: isOpen, selectedItemsCount: selectedItemsCount, totalItemsCount: totalItemsCount, showSelectionCount: selectedItemsCount !== undefined || totalItemsCount !== undefined, icon: _jsx(UiIcon, { type: "folderSmall", size: 12, color: "complementary-6" }), dropdownId: dropdownId, buttonRef: buttonRef, onClick: toggleDropdown, isError: isAnyFilterError, ariaLabel: groupAriaLabelWithState }) })), [
|
|
207
223
|
title,
|
|
208
224
|
subtitle,
|
|
209
225
|
isAnyFilterError,
|
|
@@ -17,7 +17,6 @@ export interface IConditionInputSectionProps {
|
|
|
17
17
|
};
|
|
18
18
|
} | undefined;
|
|
19
19
|
usePercentage: boolean;
|
|
20
|
-
baseDisableAutofocus?: boolean;
|
|
21
20
|
separators?: ISeparators;
|
|
22
21
|
onValueChange: (index: number, value: number) => void;
|
|
23
22
|
onFromChange: (index: number, from: number) => void;
|
|
@@ -7,12 +7,12 @@ import { ComparisonInput } from "./ComparisonInput.js";
|
|
|
7
7
|
import { RangeInput } from "./RangeInput.js";
|
|
8
8
|
export const ConditionInputSection = memo(function ConditionInputSection(props) {
|
|
9
9
|
const intl = useIntl();
|
|
10
|
-
const { index, conditionNumber, condition, usePercentage,
|
|
10
|
+
const { index, conditionNumber, condition, usePercentage, separators, onValueChange, onFromChange, onToChange, onValueBlur, onFromBlur, onToBlur, onApply, } = props;
|
|
11
11
|
if (!condition || condition.operator === "ALL") {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
//
|
|
15
|
-
const disableAutofocus =
|
|
14
|
+
// Never autofocus the value inputs. Initial focus must stay on the operator dropdown button.
|
|
15
|
+
const disableAutofocus = true;
|
|
16
16
|
const errorId = `mvf-validation-error-${index}`;
|
|
17
17
|
if (isComparisonConditionOperator(condition.operator)) {
|
|
18
18
|
const v = condition.value.value;
|
|
@@ -25,7 +25,7 @@ export const ConditionInputSection = memo(function ConditionInputSection(props)
|
|
|
25
25
|
})
|
|
26
26
|
: undefined;
|
|
27
27
|
return (_jsxs(_Fragment, { children: [
|
|
28
|
-
_jsx(ComparisonInput, { value: condition.value.value, usePercentage: usePercentage, onValueChange: (v) => onValueChange(index, v), onEnterKeyPress: onApply, onBlur: () => onValueBlur(index), hasError: shouldShowError, ariaDescribedBy: shouldShowError ? errorId : undefined, disableAutofocus: disableAutofocus, separators: separators, conditionNumber: conditionNumber }), shouldShowError ? (_jsx("div", { id: errorId, className: "gd-mvf-input-error s-mvf-input-error", "data-testid": errorId,
|
|
28
|
+
_jsx(ComparisonInput, { value: condition.value.value, usePercentage: usePercentage, onValueChange: (v) => onValueChange(index, v), onEnterKeyPress: onApply, onBlur: () => onValueBlur(index), hasError: shouldShowError, ariaDescribedBy: shouldShowError ? errorId : undefined, disableAutofocus: disableAutofocus, separators: separators, conditionNumber: conditionNumber }), shouldShowError ? (_jsx("div", { id: errorId, className: "gd-mvf-input-error s-mvf-input-error", "data-testid": errorId, children: validationErrorText })) : null] }));
|
|
29
29
|
}
|
|
30
30
|
if (isRangeConditionOperator(condition.operator)) {
|
|
31
31
|
const { from = null, to = null } = condition.value;
|
|
@@ -25,7 +25,7 @@ const DropdownWithIntl = memo(function DropdownWithIntl(props) {
|
|
|
25
25
|
return (_jsx(FullScreenOverlay, { alignTo: "body", alignPoints: MOBILE_DROPDOWN_ALIGN_POINTS, onClose: onCancel, children: _jsxs("div", { className: "gd-mobile-dropdown-overlay overlay gd-flex-row-container gd-mvf-mobile-dropdown", children: [mobileHeader ? (_jsx("div", { className: "gd-mobile-dropdown-header gd-flex-item gd-mvf-mobile-dropdown-header gd-is-mobile", children: mobileHeader })) : null, _jsx("div", { className: "gd-mobile-dropdown-content gd-flex-item-stretch gd-flex-row-container gd-mvf-mobile-dropdown-content", children: body })
|
|
26
26
|
] }) }));
|
|
27
27
|
}
|
|
28
|
-
return (_jsx(Overlay, { alignTo: anchorEl, alignPoints: alignPoints ?? DROPDOWN_ALIGNMENTS, closeOnOutsideClick: true, closeOnParentScroll: true, closeOnMouseDrag: true, onClose: onCancel, children: body }));
|
|
28
|
+
return (_jsx(Overlay, { alignTo: anchorEl, alignPoints: alignPoints ?? DROPDOWN_ALIGNMENTS, closeOnOutsideClick: true, closeOnParentScroll: true, closeOnMouseDrag: true, closeOnEscape: true, onClose: onCancel, children: body }));
|
|
29
29
|
});
|
|
30
30
|
export function Dropdown(props) {
|
|
31
31
|
return (_jsx(IntlWrapper, { locale: props.locale, children: _jsx(DropdownWithIntl, { ...props }) }));
|
|
@@ -10,7 +10,6 @@ interface IDropdownBodyProps extends IMeasureValueFilterCustomComponentProps {
|
|
|
10
10
|
usePercentage?: boolean;
|
|
11
11
|
warningMessage?: WarningMessage;
|
|
12
12
|
locale?: string;
|
|
13
|
-
disableAutofocus?: boolean;
|
|
14
13
|
onCancel?: () => void;
|
|
15
14
|
measureTitle?: string;
|
|
16
15
|
onApply: IMeasureValueFilterDropdownCallback;
|