Static filter for Overview-Engine

Question

What is the intended way to inject static filters for Overview-Engine without allowing the user to change them?

Situation

We have two overviews of the same document model. One overview shows “Fachverfahren” and the other “Stammdaten”. Depending on the overview model that is used we want to set additional filters to the data service in order to tell the server to provide only “Fachverfahren” or “Stammdaten”. These filters are only technical nature and must not be set by the user. Therefore, the filter is not allowed to show up in the Overview-Engine.

Solution until 2023.06

We wrapped the data loader for document lists. In there we had access to the activity and known when to apply the filter. This was done by providing a modified activity to the wrapped data loader. As of version 2024.06 this is no longer possible because the corresponding data loader is now a data provider.

Solution approach for 2024.06

  • Reimplement the data provider to reflect our logic. Copy about 500 lines of code that is then not properly maintained is a no-go.
  • Use the request-filter-API to inject the static filter. This however does not work, because it is not possible the identify the related activity from this API and we need to additional information from the activity to set the correct filter.
  • Wrapping the data provider and modify the return of the getUiState-selector to include the static filter. This approach is very ugly, because we would have to re-implement the saga logic in order to write a fully functional proxy.
  • Providing a preset filter for the Overview-Engine that is not selectable by the used and therefore hidden in the UI. This solution partially works. In mobile mode the filter button does not work as long as the filter is not selectable by the user. Any changes in the filter will delete the preset filter.

Solution for 2024.06

  1. Add filter for the field to the Overview-Engine (Otherwise the preset filter is treated as invalid)
  2. Add preset filter to the overview activity
import { produce } from "immer";

import { Activity, ActivityActions } from "@com.mgmtp.a12.client/client-core/lib/core/activity";
import { StoreFactories } from "@com.mgmtp.a12.client/client-core/lib/core/store";
import { OverviewEngineActions } from "@com.mgmtp.a12.overviewengine/overviewengine-core/lib/main/client-extensions";

export const setFilterForOverviewMiddleware = StoreFactories.createMiddleware((api, next, action) => {
	if (ActivityActions.push.match(action)) {
		const activity = createActivity(action.payload.activity);
		if (activity) {
			return api.dispatch(ActivityActions.push({ activity }));
		}
	} else if (ActivityActions.cancel.match(action) && action.payload.replacementActivity) {
		const activity = createActivity(action.payload.replacementActivity);
		if (activity) {
			return api.dispatch(
				ActivityActions.cancel(
					produce(action.payload, draft => {
						draft.replacementActivity = activity;
					})
				)
			);
		}
	} else if (ActivityActions.commit.started.match(action) && action.payload.replacementActivity) {
		const activity = createActivity(action.payload.replacementActivity);
		if (activity) {
			return api.dispatch(
				ActivityActions.commit.started(
					produce(action.payload, draft => {
						draft.replacementActivity = activity;
					})
				)
			);
		}
	}

	return next(action);
});

function createActivity({
	id: activityId,
	descriptor: activityDescriptor,
	initiatingActivityId,
	dataHolders: [dataHolder]
}: Activity): Activity | undefined {
	if (!activityDescriptor.type === "overview") {
		return undefined;
	}

	const { loadingState, data, slices = {} } = dataHolder ?? {};
	if (Object.keys(slices).length > 0) {
		return undefined;
	}

	return OverviewEngineActions.createActivity(
		{ activityId, activityDescriptor, initiatingActivityId, loadingState, data },
		{
			activeFilters: {
				"root.type": {
					filterType: "Enumeration",
					// Select needed variant
					criteria: { selectedValues: ["VARIANT1"] },
					nonRemovable: true
				}
			}
		}
	).payload.activity;
}

  1. Customize Overview-Engine to exclude this filter from UI by overriding FilterButton and FilterBar in the componentMap as well as FilterSelector and FilterSelectorMobile in the widgetMap
// ...

const hiddenFilterIds = ["root.type"];

const CustomFilterButton: React.ComponentType<FilterButton.PropsType> = function CustomFilterButton({
	onClick,
	...props
}) {
	const showFilterSelector = useOverviewContentBoxContext(context => context.showFilterSelector);

	const activeFilters = useOverviewEngineState(UiStateSelector.activeFilters());
	const isAnyRelevantFilterActive =
		Object.keys(activeFilters ?? {}).filter(id => !hiddenFilterIds.includes(id)).length > 0;

	const onFilterSelectorVisibilityChange = useOverviewContentBoxContext(
		context => context.onFilterSelectorVisibilityChange
	);

	const onFilterButtonClick = React.useCallback(() => {
		if (!isAnyRelevantFilterActive) {
			onFilterSelectorVisibilityChange(!showFilterSelector);
		} else {
			onClick();
		}
	}, [isAnyRelevantFilterActive, showFilterSelector, onFilterSelectorVisibilityChange, onClick]);

	return (
		<DefaultComponentMap.FilterButton
			{...props}
			onClick={onFilterButtonClick}
			showBadge={props.showBadge && isAnyRelevantFilterActive}
		/>
	);
};

const CustomFilterBar: React.ComponentType<FilterBar.Props> = function CustomFilterBar(props) {
	const activeFilters = React.useMemo(
		() => props.activeFilters.filter(item => !hiddenFilterIds.includes(item.id)),
		[props.activeFilters]
	);
	return <DefaultComponentMap.FilterBar {...props} activeFilters={activeFilters} />;
};

const componentMap: ComponentMap = {
	...DefaultComponentMap,
	FilterButton: CustomFilterButton,
	FilterBar: CustomFilterBar
};

const CustomFilterSelectorMobile: React.ComponentType<FilterSelectorMobileProps> = function CustomFilterSelectorMobile(
	props
) {
	const activeFilters = React.useMemo(
		() => props.activeFilters.filter(item => !hiddenFilterIds.includes(item.id)),
		[props.activeFilters]
	);
	return <DefaultWidgetMap.FilterSelectorMobile {...props} activeFilters={activeFilters} />;
};

const CustomFilterSelector: React.ComponentType<FilterSelectorProps> = function CustomFilterSelector(props) {
	const activeFilters = React.useMemo(
		() => props.activeFilters.filter(item => !hiddenFilterIds.includes(item.id)),
		[props.activeFilters]
	);
	return <DefaultWidgetMap.FilterSelector {...props} activeFilters={activeFilters} />;
};

const widgetsMap: WidgetMap = {
	...DefaultWidgetMap,
	FilterSelectorMobile: CustomFilterSelectorMobile,
	FilterSelector: CustomFilterSelector
};

// ...