From 614bd24b238f17856eeeff95a5b06410a5c4da7f Mon Sep 17 00:00:00 2001 From: Dima K Date: Fri, 27 Feb 2026 12:32:58 -0800 Subject: [PATCH 1/3] fix(ui): old examiner dashboard to show old setup for status filters --- strr-examiner-web/app/pages/dashboard.vue | 37 +++++++++++++++---- strr-examiner-web/package.json | 2 +- .../tests/unit/dashboard.spec.ts | 35 ++++++++++++++++-- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/strr-examiner-web/app/pages/dashboard.vue b/strr-examiner-web/app/pages/dashboard.vue index 1d4160fa0..e5e4ca03c 100644 --- a/strr-examiner-web/app/pages/dashboard.vue +++ b/strr-examiner-web/app/pages/dashboard.vue @@ -556,13 +556,7 @@ function handleColumnSort (column: string) { } } -// Limit status options to the active table to prevent mixed-status filters. -const applicationStatusOptions: { - label: string, - value: any, - disabled?: boolean, - childStatuses?: any[] -}[] = [ +const splitDashboardApplicationStatusFilters = [ { label: 'Open', value: 'OPEN', @@ -588,6 +582,35 @@ const applicationStatusOptions: { { label: 'Draft', value: ApplicationStatus.DRAFT } ] +const legacyApplicationStatusFilters = [ + { label: 'Application Status', value: undefined, disabled: true }, + { label: 'Full Review', value: ApplicationStatus.FULL_REVIEW }, + { label: 'Provisional Review', value: ApplicationStatus.PROVISIONAL_REVIEW }, + { label: 'Payment Due', value: ApplicationStatus.PAYMENT_DUE }, + { label: 'Provisional', value: ApplicationStatus.PROVISIONAL }, + { label: 'Paid', value: ApplicationStatus.PAID }, + { label: 'Additional Info Requested', value: ApplicationStatus.ADDITIONAL_INFO_REQUESTED }, + { label: 'Provisionally Approved', value: ApplicationStatus.PROVISIONALLY_APPROVED }, + { label: 'Declined', value: ApplicationStatus.DECLINED }, + { label: 'Provisionally Declined', value: ApplicationStatus.PROVISIONALLY_DECLINED }, + { label: 'Auto Approved', value: ApplicationStatus.AUTO_APPROVED }, + { label: 'Full Review Approved', value: ApplicationStatus.FULL_REVIEW_APPROVED }, + { label: 'NOC - Pending', value: ApplicationStatus.NOC_PENDING }, + { label: 'NOC - Expired', value: ApplicationStatus.NOC_EXPIRED }, + { label: 'NOC - Pending - Provisional', value: ApplicationStatus.PROVISIONAL_REVIEW_NOC_PENDING }, + { label: 'NOC - Expired - Provisional', value: ApplicationStatus.PROVISIONAL_REVIEW_NOC_EXPIRED } +] + +// Limit status options to the active table to prevent mixed-status filters. +const applicationStatusOptions: { + label: string, + value: any, + disabled?: boolean, + childStatuses?: any[] +}[] = isSplitDashboardTableEnabled.value + ? splitDashboardApplicationStatusFilters + : legacyApplicationStatusFilters + const registrationStatusOptions: { label: string; value: any; disabled?: boolean }[] = [ { label: 'Registration Status', value: undefined, disabled: true }, { label: 'Active', value: RegistrationStatus.ACTIVE }, diff --git a/strr-examiner-web/package.json b/strr-examiner-web/package.json index eb3fe2681..b8bdf25f3 100644 --- a/strr-examiner-web/package.json +++ b/strr-examiner-web/package.json @@ -2,7 +2,7 @@ "name": "strr-examiner-web", "private": true, "type": "module", - "version": "0.2.17", + "version": "0.2.18", "scripts": { "build-check": "nuxt build", "build": "nuxt generate", diff --git a/strr-examiner-web/tests/unit/dashboard.spec.ts b/strr-examiner-web/tests/unit/dashboard.spec.ts index c346cb0d6..aee499bd9 100644 --- a/strr-examiner-web/tests/unit/dashboard.spec.ts +++ b/strr-examiner-web/tests/unit/dashboard.spec.ts @@ -84,10 +84,12 @@ vi.stubGlobal('useAsyncData', () => ({ } })) +const { mockUseExaminerFeatureFlags } = vi.hoisted(() => ({ + mockUseExaminerFeatureFlags: vi.fn() +})) + vi.mock('@/composables/useExaminerFeatureFlags', () => ({ - useExaminerFeatureFlags: () => ({ - isSplitDashboardTableEnabled: ref(true) - }) + useExaminerFeatureFlags: mockUseExaminerFeatureFlags })) // Filter Persistence tests assert tab/store behavior without persistence side effects @@ -100,6 +102,7 @@ describe('Examiner Dashboard Page', () => { let wrapper: any beforeEach(async () => { + mockUseExaminerFeatureFlags.mockReturnValue({ isSplitDashboardTableEnabled: ref(true) }) mockStore = createMockStore() wrapper = await mountSuspended(Dashboard, { global: { plugins: [enI18n] } @@ -560,4 +563,30 @@ describe('Examiner Dashboard Page', () => { expect(result).toBe('-') }) }) + + describe('applicationStatusOptions', () => { + const hasValue = (options: any[], value: any) => + options.some((o: any) => o.value === value) + + it('uses splitDashboardApplicationStatusFilters when flag is enabled', () => { + // default beforeEach already sets flag to true and mounts + const statusOptions = wrapper.vm.applicationStatusOptions + expect(hasValue(statusOptions, 'OPEN')).toBe(true) + expect(hasValue(statusOptions, 'NOT_SUBMITTED')).toBe(true) + expect(hasValue(statusOptions, ApplicationStatus.PROVISIONAL)).toBe(false) + expect(hasValue(statusOptions, ApplicationStatus.PAID)).toBe(false) + }) + + it('uses legacyApplicationStatusFilters when flag is disabled', async () => { + mockUseExaminerFeatureFlags.mockReturnValue({ isSplitDashboardTableEnabled: ref(false) }) + wrapper = await mountSuspended(Dashboard, { + global: { plugins: [enI18n] } + }) + const statusOptions = wrapper.vm.applicationStatusOptions + expect(hasValue(statusOptions, ApplicationStatus.PROVISIONAL)).toBe(true) + expect(hasValue(statusOptions, ApplicationStatus.PAID)).toBe(true) + expect(hasValue(statusOptions, 'OPEN')).toBe(false) + expect(hasValue(statusOptions, 'NOT_SUBMITTED')).toBe(false) + }) + }) }) From a80463e87ae68a4ea89fdf9a358dc4d168d6828a Mon Sep 17 00:00:00 2001 From: Dima K Date: Mon, 2 Mar 2026 10:17:15 -0800 Subject: [PATCH 2/3] chore: sync hotfix branch to resolve conflicts (#1056) --- strr-api/pyproject.toml | 2 +- strr-api/src/strr_api/models/application.py | 9 ++ strr-api/src/strr_api/models/dataclass.py | 4 + strr-api/src/strr_api/models/rental.py | 31 ++++ .../src/strr_api/resources/application.py | 7 + .../src/strr_api/resources/registrations.py | 25 +++ .../test_registration_applications.py | 9 ++ .../unit/resources/test_registrations.py | 146 +++++++++++++++++- .../resources/test_renewal_applications.py | 16 +- .../app/components/Table/Header/Select.vue | 10 +- .../useExaminerDashboardPersistence.ts | 27 +++- strr-examiner-web/app/pages/dashboard.vue | 37 ++++- strr-examiner-web/app/stores/examiner.ts | 77 +++++++-- strr-examiner-web/package.json | 2 +- .../tests/unit/dashboard.spec.ts | 5 +- 15 files changed, 377 insertions(+), 30 deletions(-) diff --git a/strr-api/pyproject.toml b/strr-api/pyproject.toml index 880a5e1c5..6b8b80378 100644 --- a/strr-api/pyproject.toml +++ b/strr-api/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "strr-api" -version = "0.3.4" +version = "0.3.6" description = "" authors = ["thorwolpert "] license = "BSD 3-Clause" diff --git a/strr-api/src/strr_api/models/application.py b/strr-api/src/strr_api/models/application.py index 46dbdd274..ea7238a3d 100644 --- a/strr-api/src/strr_api/models/application.py +++ b/strr-api/src/strr_api/models/application.py @@ -216,6 +216,15 @@ def find_by_account( Application.type == ApplicationType.RENEWAL.value, ) ) + if filter_criteria.applications_only: + # Exclude applications that have a completed registration, except renewals + # Include: no registration yet or renewal applications + query = query.filter( + db.or_( + Application.registration_id.is_(None), + Application.type == ApplicationType.RENEWAL.value, + ) + ) sort_column = getattr(Application, filter_criteria.sort_by, Application.id) if filter_criteria.sort_order and filter_criteria.sort_order.lower() == "asc": query = query.order_by(sort_column.asc()) diff --git a/strr-api/src/strr_api/models/dataclass.py b/strr-api/src/strr_api/models/dataclass.py index fb08dd535..0e6fada08 100644 --- a/strr-api/src/strr_api/models/dataclass.py +++ b/strr-api/src/strr_api/models/dataclass.py @@ -52,6 +52,7 @@ class ApplicationSearch: requirements: list[str] | None = None include_draft_registration: bool = True include_draft_renewal: bool = True + applications_only: bool = False account_id: int | None = None @@ -70,3 +71,6 @@ class RegistrationSearch: assignee: str | None = None requirements: list[str] | None = None account_id: int | None = None + approval_methods: List[str] | None = None + noc_statuses: List[str] | None = None + is_set_aside: bool | None = None diff --git a/strr-api/src/strr_api/models/rental.py b/strr-api/src/strr_api/models/rental.py index cd5ed42a1..f4239bf23 100644 --- a/strr-api/src/strr_api/models/rental.py +++ b/strr-api/src/strr_api/models/rental.py @@ -141,6 +141,12 @@ def search_registrations(cls, filter_criteria: RegistrationSearch): ) if filter_criteria.requirements: query = cls._filter_by_registration_requirement(filter_criteria.requirements, query) + if filter_criteria.approval_methods: + query = cls._filter_by_approval_method(filter_criteria.approval_methods, query) + if filter_criteria.noc_statuses: + query = query.filter(Registration.noc_status.in_(filter_criteria.noc_statuses)) + if filter_criteria.is_set_aside is True: + query = query.filter(Registration.is_set_aside == True) # noqa: E712 sort_column = getattr(Registration, filter_criteria.sort_by, Registration.id) if filter_criteria.sort_order and filter_criteria.sort_order.lower() == "asc": query = query.order_by(sort_column.asc()) @@ -423,6 +429,31 @@ def _filter_by_registration_requirement(cls, requirement: list[str], query): query = query.filter(db.or_(*combined_conditions)) return query + @classmethod + def _filter_by_approval_method(cls, approval_methods: list[str], query): + """Filter registrations by application approval method. + + Only considers the most recent application (index 0 when sorted by + application_date desc) for each registration. Returns registrations + where that most recent application's status is in the given approval methods. + """ + if not approval_methods: + return query + # pylint: disable=import-outside-toplevel + from sqlalchemy import select + + from strr_api.models.application import Application + + # for each registration, get the status of the most recent application + latest_app_status_subq = ( + select(Application.status) + .where(Application.registration_id == Registration.id) + .order_by(Application.application_date.desc()) + .limit(1) + .scalar_subquery() + ) + return query.filter(latest_app_status_subq.in_(approval_methods)) + class RentalProperty(Versioned, BaseModel): """Rental Property""" diff --git a/strr-api/src/strr_api/resources/application.py b/strr-api/src/strr_api/resources/application.py index b676f067b..3b0d7b96e 100644 --- a/strr-api/src/strr_api/resources/application.py +++ b/strr-api/src/strr_api/resources/application.py @@ -264,6 +264,11 @@ def get_applications(): type: boolean default: true description: Include draft renewal applications + - in: query + name: applicationsOnly + type: boolean + default: false + description: When true, exclude applications that have a completed registration (except renewals). For split dashboard applications table. responses: 200: description: @@ -287,6 +292,7 @@ def get_applications(): requirements = request.args.getlist("requirement", None) include_draft_registration = request.args.get("includeDraftRegistration", "true").lower() == "true" include_draft_renewal = request.args.get("includeDraftRenewal", "true").lower() == "true" + applications_only = request.args.get("applicationsOnly", "false").lower() == "true" if sort_by not in VALID_SORT_FIELDS: sort_by = "id" if sort_order not in ["asc", "desc"]: @@ -304,6 +310,7 @@ def get_applications(): requirements=requirements, include_draft_registration=include_draft_registration, include_draft_renewal=include_draft_renewal, + applications_only=applications_only, ) application_list = ApplicationService.list_applications(account_id, filter_criteria=filter_criteria) return jsonify(application_list), HTTPStatus.OK diff --git a/strr-api/src/strr_api/resources/registrations.py b/strr-api/src/strr_api/resources/registrations.py index c3a001132..8ac7ee797 100644 --- a/strr-api/src/strr_api/resources/registrations.py +++ b/strr-api/src/strr_api/resources/registrations.py @@ -1015,6 +1015,24 @@ def search_registrations(): items: type: string description: Requirement filter (e.g., PR, BL, PROHIBITED, NO_REQ, PR_EXEMPT_STRATA_HOTEL, PR_EXEMPT_FARM_LAND, PR_EXEMPT_FRACTIONAL_OWNERSHIP, PLATFORM_MAJOR, PLATFORM_MEDIUM, PLATFORM_MINOR, STRATA_PR, STRATA_NO_PR). Can provide multiple values. + - in: query + name: approvalMethod + type: array + items: + type: string + enum: [FULL_REVIEW_APPROVED, AUTO_APPROVED, PROVISIONALLY_APPROVED] + description: Approval method filter. Can provide multiple values. + - in: query + name: nocStatus + type: array + items: + type: string + enum: [NOC_EXPIRED, NOC_PENDING] + description: NOC status filter. Can provide multiple values. + - in: query + name: isSetAside + type: boolean + description: Filter for set aside registrations when true. responses: 200: description: @@ -1034,6 +1052,10 @@ def search_registrations(): sort_order = request.args.get("sortOrder", "desc") assignee = request.args.get("assignee", None) requirements = request.args.getlist("requirement") or None + approval_methods = request.args.getlist("approvalMethod") or None + noc_statuses = request.args.getlist("nocStatus") or None + is_set_aside_param = request.args.get("isSetAside", None) + is_set_aside = is_set_aside_param.lower() == "true" if is_set_aside_param else None if sort_by not in VALID_REGISTRATION_SORT_FIELDS: sort_by = "id" if sort_order not in ["asc", "desc"]: @@ -1052,6 +1074,9 @@ def search_registrations(): sort_order=sort_order, assignee=assignee, requirements=requirements, + approval_methods=approval_methods, + noc_statuses=noc_statuses, + is_set_aside=is_set_aside, ) registration_list = RegistrationService.search_registrations(filter_criteria=filter_criteria) diff --git a/strr-api/tests/unit/resources/test_registration_applications.py b/strr-api/tests/unit/resources/test_registration_applications.py index bb4896b45..d6a0d3c16 100644 --- a/strr-api/tests/unit/resources/test_registration_applications.py +++ b/strr-api/tests/unit/resources/test_registration_applications.py @@ -984,6 +984,15 @@ def test_examiner_filter_record_number_application(session, client, jwt): assert rv.status_code == 200 assert len(response_json.get("applications")) == 0 + # Test applicationsOnly excludes approved registration applications (keeps renewals) + rv = client.get( + f"/applications?recordNumber={registration_number}&applicationsOnly=true", + headers=staff_headers, + ) + response_json = rv.json + assert rv.status_code == 200 + assert len(response_json.get("applications")) == 0 # Approved reg excluded when applicationsOnly=true + def test_examiner_multi_select_filters(session, client, jwt): staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") diff --git a/strr-api/tests/unit/resources/test_registrations.py b/strr-api/tests/unit/resources/test_registrations.py index 14d73c3f3..4f66f0c63 100644 --- a/strr-api/tests/unit/resources/test_registrations.py +++ b/strr-api/tests/unit/resources/test_registrations.py @@ -1,7 +1,7 @@ import json import os import random -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from http import HTTPStatus from unittest.mock import patch @@ -2412,3 +2412,147 @@ def test_search_registrations_requirement_with_sorting(session, client, jwt): registrations = rv.json assert "registrations" in registrations assert len(registrations.get("registrations")) >= 0 + + +@patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) +def test_search_registrations_with_approval_method_noc_status_set_aside_filters(session, client, jwt): + """Test search registrations with approvalMethod, nocStatus, and isSetAside filters.""" + with open(CREATE_HOST_REGISTRATION_REQUEST) as f: + json_data = json.load(f) + headers = create_header(jwt, [PUBLIC_USER], "Account-Id") + headers["Account-Id"] = ACCOUNT_ID + rv = client.post("/applications", json=json_data, headers=headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + application_number = response_json.get("header").get("applicationNumber") + + application = Application.find_by_application_number(application_number=application_number) + application.payment_status = PaymentStatus.COMPLETED.value + application.status = Application.Status.FULL_REVIEW + application.save() + + staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") + rv = client.put(f"/applications/{application_number}/assign", headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + status_update_request = {"status": Application.Status.FULL_REVIEW_APPROVED} + rv = client.put(f"/applications/{application_number}/status", json=status_update_request, headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + registration_number = response_json.get("header").get("registrationNumber") + + # Search by approval method - FULL_REVIEW_APPROVED + rv = client.get( + f"/registrations/search?approvalMethod=FULL_REVIEW_APPROVED&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + assert len(registrations.get("registrations")) >= 1 + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert found + + # Search by nocStatus - create registration with NOC_PENDING and verify filter + registration = Registration.query.filter_by(registration_number=registration_number).one_or_none() + registration.noc_status = RegistrationNocStatus.NOC_PENDING.value + registration.save() + + rv = client.get( + f"/registrations/search?nocStatus=NOC_PENDING&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + assert len(registrations.get("registrations")) >= 1 + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert found + + # Search by isSetAside - set aside the registration and verify filter + registration.is_set_aside = True + registration.save() + + rv = client.get( + f"/registrations/search?isSetAside=true&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + assert len(registrations.get("registrations")) >= 1 + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert found + + +@patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) +def test_search_registrations_approval_method_uses_most_recent_application_only( + mock_create_invoice, session, client, jwt +): + """Test that approvalMethod filter considers only the most recent application (index 0). + + When a registration has multiple applications (e.g. initial FULL_REVIEW_APPROVED, + renewal PROVISIONALLY_APPROVED), the filter should use only the most recent one. + So filtering for FULL_REVIEW_APPROVED should NOT return such a registration. + """ + from nanoid import generate + + with open(CREATE_HOST_REGISTRATION_REQUEST) as f: + json_data = json.load(f) + headers = create_header(jwt, [PUBLIC_USER], "Account-Id") + headers["Account-Id"] = ACCOUNT_ID + rv = client.post("/applications", json=json_data, headers=headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + application_number = response_json.get("header").get("applicationNumber") + + application = Application.find_by_application_number(application_number=application_number) + application.payment_status = PaymentStatus.COMPLETED.value + application.status = Application.Status.FULL_REVIEW + application.save() + + staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") + rv = client.put(f"/applications/{application_number}/assign", headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + status_update_request = {"status": Application.Status.FULL_REVIEW_APPROVED} + rv = client.put(f"/applications/{application_number}/status", json=status_update_request, headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + registration_number = response_json.get("header").get("registrationNumber") + registration = Registration.query.filter_by(registration_number=registration_number).one_or_none() + + # Refresh application to get its application_date from DB, then add a renewal that is + # explicitly newer (guarantees correct ordering regardless of test execution timing) + session.refresh(application) + renewal_app = Application( + application_json=application.application_json, + application_number=generate(alphabet="0123456789", size=14), + type=ApplicationType.RENEWAL.value, + registration_type=application.registration_type, + status=Application.Status.PROVISIONALLY_APPROVED, + registration_id=registration.id, + application_date=application.application_date + timedelta(seconds=1), + ) + session.add(renewal_app) + session.commit() + + # Filter for FULL_REVIEW_APPROVED - should NOT return this registration + # (most recent app is PROVISIONALLY_APPROVED) + rv = client.get( + f"/registrations/search?approvalMethod=FULL_REVIEW_APPROVED&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert ( + not found + ), "Registration with most recent app PROVISIONALLY_APPROVED should not match FULL_REVIEW_APPROVED" + + # Filter for PROVISIONALLY_APPROVED - should return this registration + rv = client.get( + f"/registrations/search?approvalMethod=PROVISIONALLY_APPROVED&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert ( + found + ), "Registration with most recent app PROVISIONALLY_APPROVED should match PROVISIONALLY_APPROVED filter" diff --git a/strr-api/tests/unit/resources/test_renewal_applications.py b/strr-api/tests/unit/resources/test_renewal_applications.py index 36069efa1..c6f28b59d 100644 --- a/strr-api/tests/unit/resources/test_renewal_applications.py +++ b/strr-api/tests/unit/resources/test_renewal_applications.py @@ -85,12 +85,22 @@ def test_host_renewal_application_submission(session, client, jwt, request_json, json_data["header"] = renewal_header_json rv = client.post("/applications", json=json_data, headers=headers) response_json = rv.json - application_number = response_json.get("header").get("applicationNumber") - assert application_number is not None - application = Application.find_by_application_number(application_number=application_number) + renewal_application_number = response_json.get("header").get("applicationNumber") + assert renewal_application_number is not None + application = Application.find_by_application_number(application_number=renewal_application_number) assert application.registration_id == registration_id assert application.type == "renewal" + # Test applicationsOnly includes renewal applications (renewals have registration_id but are shown) + rv = client.get( + f"/applications?recordNumber={renewal_application_number}&applicationsOnly=true", + headers=staff_headers, + ) + response_json = rv.json + assert rv.status_code == 200 + assert len(response_json.get("applications")) == 1 + assert response_json.get("applications")[0]["header"]["applicationNumber"] == renewal_application_number + @pytest.mark.parametrize( "request_json, isUnitOnPrincipalResidence", diff --git a/strr-examiner-web/app/components/Table/Header/Select.vue b/strr-examiner-web/app/components/Table/Header/Select.vue index c32d76dd3..98df31232 100644 --- a/strr-examiner-web/app/components/Table/Header/Select.vue +++ b/strr-examiner-web/app/components/Table/Header/Select.vue @@ -67,7 +67,13 @@ watch(filterModel, (newVal, oldVal) => { if (parentChecked) { childStatuses!.forEach(status => result.add(status)) // check all child statuses } else if (parentUnchecked) { - childStatuses!.forEach(status => result.delete(status)) // uncheck all child statuses + // cascade the uncheck to children only if all are still selected (user toggled parent off) + // skip cascade if any child is already gone, because of external change (clear filter button) + const allChildrenStillPresent = childStatuses!.every(status => result.has(status)) + // this check distinguishes between the two cases so the watcher doesn't fight with external resets + if (allChildrenStillPresent) { + childStatuses!.forEach(status => result.delete(status)) // uncheck all child statuses + } } else if (allChildrenSelected) { result.add(parentVal) // if all children checked - auto-check parent } else { @@ -165,7 +171,7 @@ onMounted(() => {