diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f5883c0..43aafca31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ You can also check the rare cases - Sorting of values in the multi filter panel is now consistent with the rest of the application + - Cascading filters in the editing mode now work correctly again and not + exclude the last filter anymore - Styles - Added missing paddings in details panel autocomplete component and in banner component for smaller breakpoints diff --git a/app/rdf/query-dimension-values.spec.ts b/app/rdf/query-dimension-values.spec.ts index 05fb4b1eb..f6284d333 100644 --- a/app/rdf/query-dimension-values.spec.ts +++ b/app/rdf/query-dimension-values.spec.ts @@ -3,9 +3,10 @@ import rdf from "rdf-ext"; import ParsingClient from "sparql-http-client/ParsingClient"; import { describe, expect, it, vi } from "vitest"; -import { FilterValue } from "@/config-types"; +import { Filters, FilterValue } from "@/config-types"; import * as ns from "@/rdf/namespace"; import { + getFiltersList, getQueryFilters, loadDimensionValuesWithMetadata, } from "@/rdf/query-dimension-values"; @@ -98,3 +99,196 @@ describe("getQueryFilters", () => { expect(queryPart).toContain(""); }); }); + +describe("getFiltersList", () => { + describe("when no filters are provided", () => { + it("should return empty array for undefined filters", () => { + const result = getFiltersList(undefined, "http://example.com/dimension1"); + expect(result).toEqual([]); + }); + + it("should return empty array for empty filters object", () => { + const result = getFiltersList({}, "http://example.com/dimension1"); + expect(result).toEqual([]); + }); + }); + + describe("normal usage pattern (full filter set)", () => { + const filters: Filters = { + "http://example.com/dimension1": { type: "single", value: "value1" }, + "http://example.com/dimension2": { type: "single", value: "value2" }, + "http://example.com/dimension3": { + type: "multi", + values: { value3a: true, value3b: true }, + }, + "http://example.com/dimension4": { type: "single", value: "value4" }, + }; + + it("should return filters before the current dimension (first dimension)", () => { + const result = getFiltersList(filters, "http://example.com/dimension1"); + expect(result).toEqual([]); + }); + + it("should return filters before the current dimension (middle dimension)", () => { + const result = getFiltersList(filters, "http://example.com/dimension3"); + expect(result).toEqual([ + ["http://example.com/dimension1", { type: "single", value: "value1" }], + ["http://example.com/dimension2", { type: "single", value: "value2" }], + ]); + }); + + it("should return filters before the current dimension (last dimension)", () => { + const result = getFiltersList(filters, "http://example.com/dimension4"); + expect(result).toEqual([ + ["http://example.com/dimension1", { type: "single", value: "value1" }], + ["http://example.com/dimension2", { type: "single", value: "value2" }], + [ + "http://example.com/dimension3", + { type: "multi", values: { value3a: true, value3b: true } }, + ], + ]); + }); + + it("should handle mixed filter types correctly", () => { + const mixedFilters: Filters = { + "http://example.com/filter1": { type: "single", value: "single_value" }, + "http://example.com/filter2": { + type: "multi", + values: { multi1: true, multi2: true }, + }, + "http://example.com/filter3": { + type: "range", + from: "2020", + to: "2023", + }, + "http://example.com/current": { + type: "single", + value: "current_value", + }, + }; + + const result = getFiltersList(mixedFilters, "http://example.com/current"); + expect(result).toEqual([ + [ + "http://example.com/filter1", + { type: "single", value: "single_value" }, + ], + [ + "http://example.com/filter2", + { type: "multi", values: { multi1: true, multi2: true } }, + ], + [ + "http://example.com/filter3", + { type: "range", from: "2020", to: "2023" }, + ], + ]); + }); + }); + + describe("pre-sliced usage pattern (DataFilterSelectGeneric)", () => { + const preSlicedFilters: Filters = { + "http://example.com/constraint1": { type: "single", value: "value1" }, + "http://example.com/constraint2": { + type: "multi", + values: { value2a: true, value2b: true }, + }, + }; + + it("should return all filters when current dimension is not found (pre-sliced case)", () => { + const result = getFiltersList( + preSlicedFilters, + "http://example.com/target_dimension" + ); + expect(result).toEqual([ + ["http://example.com/constraint1", { type: "single", value: "value1" }], + [ + "http://example.com/constraint2", + { type: "multi", values: { value2a: true, value2b: true } }, + ], + ]); + }); + + it("should preserve filter order when using all filters", () => { + const orderedFilters: Filters = { + "http://example.com/first": { type: "single", value: "first_value" }, + "http://example.com/second": { type: "single", value: "second_value" }, + "http://example.com/third": { type: "single", value: "third_value" }, + }; + + const result = getFiltersList( + orderedFilters, + "http://example.com/not_found" + ); + expect(result).toEqual([ + ["http://example.com/first", { type: "single", value: "first_value" }], + [ + "http://example.com/second", + { type: "single", value: "second_value" }, + ], + ["http://example.com/third", { type: "single", value: "third_value" }], + ]); + }); + }); + + describe("edge cases", () => { + it("should handle single filter correctly", () => { + const singleFilter: Filters = { + "http://example.com/only_filter": { + type: "single", + value: "only_value", + }, + }; + + const result = getFiltersList( + singleFilter, + "http://example.com/not_found" + ); + expect(result).toEqual([ + [ + "http://example.com/only_filter", + { type: "single", value: "only_value" }, + ], + ]); + }); + + it("should return empty array when current dimension is the only filter", () => { + const singleFilter: Filters = { + "http://example.com/current": { + type: "single", + value: "current_value", + }, + }; + + const result = getFiltersList(singleFilter, "http://example.com/current"); + expect(result).toEqual([]); + }); + + it("should handle filters with special characters in IRIs", () => { + const specialFilters: Filters = { + "https://example.com/path/to/dimension?param=1": { + type: "single", + value: "value1", + }, + "https://example.com/path/to/dimension#fragment": { + type: "single", + value: "value2", + }, + }; + + const result = getFiltersList( + specialFilters, + "http://example.com/target" + ); + expect(result).toEqual([ + [ + "https://example.com/path/to/dimension?param=1", + { type: "single", value: "value1" }, + ], + [ + "https://example.com/path/to/dimension#fragment", + { type: "single", value: "value2" }, + ], + ]); + }); + }); +}); diff --git a/app/rdf/query-dimension-values.ts b/app/rdf/query-dimension-values.ts index 23bf4b514..073463eed 100644 --- a/app/rdf/query-dimension-values.ts +++ b/app/rdf/query-dimension-values.ts @@ -418,7 +418,7 @@ const parseDimensionValue = ( }; const parseMaybeUndefined = (value: string, fallbackValue: string) => { - return value === ns.cube.Undefined.value ? "-" : fallbackValue ?? value; + return value === ns.cube.Undefined.value ? "-" : (fallbackValue ?? value); }; type LoadMaxDimensionValuesProps = Omit; @@ -463,21 +463,25 @@ ${getQueryFilters(filterList, cubeDimensions, dimensionIri)}` } } -const getFiltersList = (filters: Filters | undefined, dimensionIri: string) => { +export const getFiltersList = ( + filters: Filters | undefined, + dimensionIri: string +) => { if (!filters) { return []; } const entries = Object.entries(filters); + const currentIndex = entries.findIndex(([iri]) => iri == dimensionIri); + const filteredEntries = entries.slice( + 0, + // Make sure to not exclude the last filter in case of pre-sliced filters + currentIndex >= 0 ? currentIndex : undefined + ); + // Consider filters before the current filter to fetch the values for // the current filter - return sortBy( - entries.slice( - 0, - entries.findIndex(([iri]) => iri == dimensionIri) - ), - ([, v]) => getFilterOrder(v) - ); + return sortBy(filteredEntries, ([, v]) => getFilterOrder(v)); }; export const getQueryFilters = (