From 349b5b9eddc4a2af781badbb040760937c91fe57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ko=C5=82odzi=C5=84ski?= Date: Thu, 9 Jul 2026 11:15:12 +0200 Subject: [PATCH 1/2] feat: add default showed categories --- .../components/FiltersForm/FiltersForm.jsx | 10 ++++-- frontend/src/services/http/httpService.js | 15 ++++++-- frontend/tests/FiltersForm.test.jsx | 28 ++++++++++++++- goodmap/core_api.py | 2 ++ goodmap/db.py | 27 +++++++++++++- tests/unit_tests/test_core_api.py | 36 +++++++++++++++++++ tests/unit_tests/test_db.py | 19 +++++++++- 7 files changed, 129 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/FiltersForm/FiltersForm.jsx b/frontend/src/components/FiltersForm/FiltersForm.jsx index c4124ee2..89afb7ca 100644 --- a/frontend/src/components/FiltersForm/FiltersForm.jsx +++ b/frontend/src/components/FiltersForm/FiltersForm.jsx @@ -158,7 +158,7 @@ const LoadingSkeleton = () => ( ); export const FiltersForm = () => { - const { setCategories } = useCategories(); + const { categories: selectedFilters, setCategories } = useCategories(); const [categoriesData, setCategoriesData] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -188,8 +188,11 @@ export const FiltersForm = () => { useEffect(() => { const fetchCategories = async () => { setIsLoading(true); - const categoriesData = await httpService.getCategoriesData(); - setCategoriesData(categoriesData); + const { categories, defaultChecked } = await httpService.getCategoriesData(); + setCategoriesData(categories); + if (Object.keys(defaultChecked).length > 0) { + setCategories(defaultChecked); + } setIsLoading(false); }; fetchCategories(); @@ -208,6 +211,7 @@ export const FiltersForm = () => { type="checkbox" id={name} value={name} + checked={Boolean(selectedFilters[category]?.includes(name))} /> {translation} {tooltipData && ( diff --git a/frontend/src/services/http/httpService.js b/frontend/src/services/http/httpService.js index 678a90c5..93a0c97c 100644 --- a/frontend/src/services/http/httpService.js +++ b/frontend/src/services/http/httpService.js @@ -47,13 +47,15 @@ export const httpService = { * Fetches complete categories data including subcategories in a single request. * Uses the /api/categories-full endpoint to avoid waterfall requests. * - * @returns {Promise} Promise resolving to array of category data tuples + * @returns {Promise<{categories: Array, defaultChecked: Object}>} Promise resolving to + * the array of category data tuples plus a map of category key to the option values + * that should be pre-checked by default. */ getCategoriesData: async () => { const response = await fetch(CATEGORIES_FULL).then(res => res.json()); // Transform to expected format: [[key, name], options, help?, optionsHelp?] - return response.categories.map(category => { + const categories = response.categories.map(category => { const categoryTuple = [category.key, category.name]; const options = category.options; @@ -67,6 +69,15 @@ export const httpService = { } return [categoryTuple, options]; }); + + const defaultChecked = {}; + for (const category of response.categories) { + if (category.default_checked?.length) { + defaultChecked[category.key] = category.default_checked; + } + } + + return { categories, defaultChecked }; }, /** diff --git a/frontend/tests/FiltersForm.test.jsx b/frontend/tests/FiltersForm.test.jsx index cc4c2261..d97d390c 100644 --- a/frontend/tests/FiltersForm.test.jsx +++ b/frontend/tests/FiltersForm.test.jsx @@ -19,7 +19,7 @@ const categories = [ ], ]; -httpService.getCategoriesData.mockResolvedValue(categories); +httpService.getCategoriesData.mockResolvedValue({ categories, defaultChecked: {} }); describe('Creates good filter_form box', () => { beforeAll(() => { @@ -90,3 +90,29 @@ describe('Creates good filter_form box', () => { expect(queryByLabelText(/Help: Inaczej rodzaje/i)).toBeInTheDocument(); }); }); + +describe('Pre-checks options configured as default-checked', () => { + beforeEach(() => { + httpService.getCategoriesData.mockResolvedValueOnce({ + categories, + defaultChecked: { types: ['shoes'] }, + }); + return act(() => + render( + + + , + ), + ); + }); + + it('renders the default-checked option as checked', () => { + const shoesCheckbox = document.querySelector('#shoes'); + expect(shoesCheckbox.checked).toBe(true); + }); + + it('leaves options not listed as default-checked unchecked', () => { + const clothesCheckbox = document.querySelector('#clothes'); + expect(clothesCheckbox.checked).toBe(false); + }); +}); diff --git a/goodmap/core_api.py b/goodmap/core_api.py index 2def128f..47b7fce0 100644 --- a/goodmap/core_api.py +++ b/goodmap/core_api.py @@ -440,12 +440,14 @@ def get_categories_full(): result = [] categories_options_help = categories_data.get("categories_options_help", {}) + categories_default_checked = categories_data.get("categories_default_checked", {}) for key, options in categories_data["categories"].items(): category_entry = { "key": key, "name": gettext(key), "options": make_tuple_translation(options), + "default_checked": categories_default_checked.get(key, []), } if CategoriesHelp in feature_flags: diff --git a/goodmap/db.py b/goodmap/db.py index 91aec3c5..3b745381 100644 --- a/goodmap/db.py +++ b/goodmap/db.py @@ -608,11 +608,17 @@ def json_db_get_category_data(self, category_type=None): "categories_options_help": { category_type: self.data.get("categories_options_help", {}).get(category_type, []) }, + "categories_default_checked": { + category_type: self.data.get("categories_default_checked", {}).get( + category_type, [] + ) + }, } return { "categories": self.data["categories"], "categories_help": self.data.get("categories_help", []), "categories_options_help": self.data.get("categories_options_help", {}), + "categories_default_checked": self.data.get("categories_default_checked", {}), } @@ -627,11 +633,15 @@ def json_file_db_get_category_data(self, category_type=None): "categories_options_help": { category_type: data.get("categories_options_help", {}).get(category_type, []) }, + "categories_default_checked": { + category_type: data.get("categories_default_checked", {}).get(category_type, []) + }, } return { "categories": data["categories"], "categories_help": data.get("categories_help", []), "categories_options_help": data.get("categories_options_help", {}), + "categories_default_checked": data.get("categories_default_checked", {}), } @@ -645,11 +655,15 @@ def google_json_db_get_category_data(self, category_type=None): "categories_options_help": { category_type: data.get("categories_options_help", {}).get(category_type, []) }, + "categories_default_checked": { + category_type: data.get("categories_default_checked", {}).get(category_type, []) + }, } return { "categories": data.get("categories", {}), "categories_help": data.get("categories_help", []), "categories_options_help": data.get("categories_options_help", {}), + "categories_default_checked": data.get("categories_default_checked", {}), } @@ -668,13 +682,24 @@ def mongodb_db_get_category_data(self, category_type=None): category_type, [] ) }, + "categories_default_checked": { + category_type: config_doc.get("categories_default_checked", {}).get( + category_type, [] + ) + }, } return { "categories": config_doc.get("categories", {}), "categories_help": config_doc.get("categories_help", []), "categories_options_help": config_doc.get("categories_options_help", {}), + "categories_default_checked": config_doc.get("categories_default_checked", {}), } - return {"categories": {}, "categories_help": [], "categories_options_help": {}} + return { + "categories": {}, + "categories_help": [], + "categories_options_help": {}, + "categories_default_checked": {}, + } def get_category_data(db): diff --git a/tests/unit_tests/test_core_api.py b/tests/unit_tests/test_core_api.py index a613ccd5..75da7aed 100644 --- a/tests/unit_tests/test_core_api.py +++ b/tests/unit_tests/test_core_api.py @@ -170,6 +170,42 @@ def test_categories_full_endpoint(test_app): assert category["options"][0] == ["test", "test-translated"] assert category["options"][1] == ["test2", "test2-translated"] + # No default-checked options configured for this category + assert category["default_checked"] == [] + + +@mock.patch("goodmap.core_api.gettext", fake_translation) +def test_categories_full_endpoint_with_default_checked(): + test_app = create_test_app( + db_overrides={ + "categories": {"test-category": ["opt1", "opt2"]}, + "categories_default_checked": {"test-category": ["opt1"]}, + } + ) + response = test_app.get("/api/categories-full") + assert response.status_code == 200 + data = response.json + assert data is not None + + category = data["categories"][0] + assert category["default_checked"] == ["opt1"] + + +@mock.patch("goodmap.core_api.gettext", fake_translation) +def test_categories_full_endpoint_without_default_checked(): + test_app = create_test_app( + db_overrides={ + "categories": {"test-category": ["opt1", "opt2"]}, + } + ) + response = test_app.get("/api/categories-full") + assert response.status_code == 200 + data = response.json + assert data is not None + + category = data["categories"][0] + assert category["default_checked"] == [] + @mock.patch("goodmap.core_api.gettext", fake_translation) def test_categories_full_endpoint_with_multiple_categories(): diff --git a/tests/unit_tests/test_db.py b/tests/unit_tests/test_db.py index 893e53d9..6243166d 100644 --- a/tests/unit_tests/test_db.py +++ b/tests/unit_tests/test_db.py @@ -112,6 +112,7 @@ "location_obligatory_fields": [["name", "str"]], "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } data_json = json.dumps({"map": data}) @@ -1422,6 +1423,7 @@ def test_json_db_get_category_data(): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1434,6 +1436,7 @@ def test_json_db_get_category_data_specific_category(): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1449,6 +1452,7 @@ def test_json_file_db_get_category_data(tmp_path): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1464,6 +1468,7 @@ def test_json_file_db_get_category_data_specific_category(tmp_path): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1480,6 +1485,7 @@ def test_google_json_db_get_category_data(mock_cli): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1496,6 +1502,7 @@ def test_google_json_db_get_category_data_specific_category(mock_cli): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1509,6 +1516,7 @@ def test_mongodb_db_get_category_data(mock_client): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } db = MongoDB("mongodb://localhost:27017", "test_db") @@ -1518,6 +1526,7 @@ def test_mongodb_db_get_category_data(mock_client): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1531,6 +1540,7 @@ def test_mongodb_db_get_category_data_specific_category(mock_client): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } db = MongoDB("mongodb://localhost:27017", "test_db") @@ -1540,6 +1550,7 @@ def test_mongodb_db_get_category_data_specific_category(mock_client): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected @@ -1553,7 +1564,12 @@ def test_mongodb_db_get_category_data_no_config(mock_client): db = MongoDB("mongodb://localhost:27017", "test_db") extend_db_with_goodmap_queries(db, LocationBase) category_data = mongodb_db_get_category_data(db) - expected = {"categories": {}, "categories_help": [], "categories_options_help": {}} + expected = { + "categories": {}, + "categories_help": [], + "categories_options_help": {}, + "categories_default_checked": {}, + } assert category_data == expected @@ -1565,6 +1581,7 @@ def test_get_category_data(): "categories": {"test-category": ["searchable", "unsearchable"]}, "categories_help": ["Help text for categories"], "categories_options_help": {"test-category": ["Help for test category"]}, + "categories_default_checked": {"test-category": ["searchable"]}, } assert category_data == expected From c521ece52c1f04e79605473320b70e34d6107e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ko=C5=82odzi=C5=84ski?= Date: Wed, 22 Jul 2026 11:09:28 +0200 Subject: [PATCH 2/2] fixes after reveiw --- README.md | 4 +++ docs/quickstart.rst | 40 ++++++++++++++++++++++++ e2e-tests/e2e_test_data_initial.json | 5 +++ e2e-tests/tests/basic/test_left_panel.py | 31 ++++++++++++++++++ examples/e2e_test_data.json | 5 +++ 5 files changed, 85 insertions(+) diff --git a/README.md b/README.md index 7babf065..cf49e563 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ TODO: `obligatory_fields` is a new subsection, start using it in the actual appl ``` "accessible_by": ["bikes", "cars", "pedestrians"] ``` +- `categories_default_checked` - for a given category, the options that should be pre-checked in the filter panel when the app first loads. E.g. +``` +"accessible_by": ["cars", "pedestrians"] +``` - `visible_data` - when a datapoint will be rendered as a pin on a map, these fields will be shown in the box when clicking on a pin. E.g. ``` "name", diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 3a77ae95..3fa7f54e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -70,6 +70,46 @@ Example configuration in your data source: ] } +Categories and Filtering +~~~~~~~~~~~~~~~~~~~~~~~~ + +``categories`` is a dict of field name to the list of allowed values for that +field. Each category is rendered in the frontend as a group of filter +checkboxes, one per allowed value. + +``categories_help`` + List of category keys that should show a help tooltip next to the + category's title. The tooltip text is looked up via the translation key + ``categories_help_``. + +``categories_options_help`` + Dict of category key to the list of option values within that category + that should show a help tooltip. The tooltip text is looked up via the + translation key ``categories_options_help_``. + +``categories_default_checked`` + Dict of category key to the list of option values that should be + pre-checked in the filter panel when the app first loads, before the user + has made any selection. + +Example configuration in your data source: + +.. code-block:: json + + { + "categories": { + "accessible_by": ["bikes", "cars", "pedestrians"], + "type_of_place": ["big bridge", "small bridge"] + }, + "categories_help": ["accessible_by"], + "categories_options_help": { + "accessible_by": ["cars", "pedestrians"] + }, + "categories_default_checked": { + "accessible_by": ["cars"] + } + } + .. _data-model-visible_data: Database Types diff --git a/e2e-tests/e2e_test_data_initial.json b/e2e-tests/e2e_test_data_initial.json index 9aca44ce..37eead36 100644 --- a/e2e-tests/e2e_test_data_initial.json +++ b/e2e-tests/e2e_test_data_initial.json @@ -76,6 +76,11 @@ "pedestrians" ] }, + "categories_default_checked": { + "accessible_by": [ + "cars" + ] + }, "visible_data": [ "remark", "accessible_by", diff --git a/e2e-tests/tests/basic/test_left_panel.py b/e2e-tests/tests/basic/test_left_panel.py index 7042fd08..f9463e9c 100644 --- a/e2e-tests/tests/basic/test_left_panel.py +++ b/e2e-tests/tests/basic/test_left_panel.py @@ -347,6 +347,37 @@ def test_small_bridge_filter_has_helper_tooltip(self, page: Page): expect(tooltip).to_contain_text("A smaller pedestrian or bike bridge") +class TestLeftPanelDefaultCheckedFilters: + """Test suite for the categories_default_checked config option""" + + def test_configured_option_is_checked_by_default(self, page: Page): + """ + The 'cars' option of the accessible_by category is configured as + default-checked (see categories_default_checked in the test data), + so it should be pre-checked when the filter form loads. + """ + page.set_viewport_size({"width": 1200, "height": 800}) + page.goto(BASE_URL, wait_until="domcontentloaded") + + page.wait_for_selector("#filter-form", timeout=10000) + + cars_checkbox = page.locator("#filter-form input#cars") + expect(cars_checkbox).to_be_checked() + + def test_other_options_are_not_checked_by_default(self, page: Page): + """ + Options not listed in categories_default_checked should remain + unchecked when the filter form loads. + """ + page.set_viewport_size({"width": 1200, "height": 800}) + page.goto(BASE_URL, wait_until="domcontentloaded") + + page.wait_for_selector("#filter-form", timeout=10000) + + pedestrians_checkbox = page.locator("#filter-form input#pedestrians") + expect(pedestrians_checkbox).not_to_be_checked() + + class TestLeftPanelScrollbar: """Test suite for panel scrollbar styling""" diff --git a/examples/e2e_test_data.json b/examples/e2e_test_data.json index 8be5b4f2..f435d0ff 100644 --- a/examples/e2e_test_data.json +++ b/examples/e2e_test_data.json @@ -72,6 +72,11 @@ "pedestrians" ] }, + "categories_default_checked": { + "accessible_by": [ + "cars" + ] + }, "visible_data": [ "remark", "accessible_by",