Skip to content

#13711 - Fixed type of contact checkboxes to allow multiple selections#13778

Merged
roldy merged 6 commits intodevelopmentfrom
bugfix-13711-type_of_contact_checkboxes
Jan 28, 2026
Merged

#13711 - Fixed type of contact checkboxes to allow multiple selections#13778
roldy merged 6 commits intodevelopmentfrom
bugfix-13711-type_of_contact_checkboxes

Conversation

@roldy
Copy link
Copy Markdown
Contributor

@roldy roldy commented Dec 11, 2025

Fixes #13711

Summary by CodeRabbit

  • New Features

    • Contacts now support multiple proximity types (multi-select) shown as comma-separated, localized labels.
  • APIs & Exports

    • Public DTOs and API schemas updated to accept/return contact proximities as arrays/lists.
  • Database

    • Schema extended to persist multiple proximities per contact and broader history/versioning enhancements.
  • Backend

    • Business logic and retrieval updated to handle proximity collections and to bulk-load proximities by contact IDs.
  • UI

    • Forms, grids and selection components adapted for multi-select proximities and updated category deduction.
  • Tests

    • End-to-end tests added for empty, single, multiple, retrieval and update scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 11, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Converts contact proximity from a single value to a multi-valued Set across API DTOs, JPA entity mapping, backend services/transformers, REST schemas, UI, tests, and adds post-query proximity population by contact IDs.

Changes

Cohort / File(s) Summary
API DTOs — Contact-related
\sormas-api/.../contact/`<br>`ContactDto.java`, `ContactExportDto.java`, `ContactIndexDto.java`, `ContactIndexDetailedDto.java`, `SimilarContactDto.java``
Renamed CONTACT_PROXIMITYCONTACT_PROXIMITIES; replaced single ContactProximity fields/params with Set<ContactProximity> and corresponding getters/setters/constructors; lazy-init for sets; some constructors now accept id placeholders.
Backend entity & JPA mapping
\sormas-backend/.../contact/Contact.java``
Replaced single proximity with @ElementCollection(fetch = LAZY) Set<ContactProximity> contactProximities; added @CollectionTable mapping to contact_contactproximities; constant rename and lazy init.
Facade, service & population logic
\sormas-backend/.../contact/`<br>`ContactFacadeEjb.java`, `ContactService.java``
Removed proximity from multiselect queries; added getContactProximitiesByContactIds(...) to fetch proximities post-query; populate DTOs from Map<id,Set<...>>; updated completeness and follow-up logic to evaluate collections.
Result transformers & criteria
\sormas-backend/.../contact/`<br>`ContactIndexDtoResultTransformer.java`, `ContactIndexDetailedDtoResultTransformer.java`, `ContactListCriteriaBuilder.java``
Query selections switched from proximity to contact ID; transformer tuple mapping updated (ContactProximity → Long id); sort/property refs updated to new constant.
Database schema & migrations
\sormas-backend/src/main/resources/sql/sormas_schema.sql``
Schema changes to support element-collection storage (join table for proximities) and broad temporal/versioning updates across many tables (history triggers, new tables, indices).
REST API specs
\sormas-rest/`<br>`swagger.json`, `swagger.yaml``
Replaced contactProximity (single enum) with contactProximities (array of enum) across many schemas; additional DTO properties added in several schemas.
UI — grids, forms, components
\sormas-ui/.../contact/` and subpaths<br>`AbstractContactGrid.java`, `ContactCreateForm.java`, `ContactDataForm.java`, `ContactGridDetailed.java`, `ContactSelectionField.java`, `ContactSelectionGrid.java`, `ContactCaseConversionSelectionGrid.java`, `SourceContactListEntry.java`, ...``
Switched single-select to multi-select OptionGroup; renderers now join localized captions from Set<ContactProximity>; updated bindings to CONTACT_PROXIMITIES; added helper to deduce category from a set; added NullableOptionGroup.setNullableValue.
Backend tests & templates
\sormas-backend/src/test/...` and resources<br>`ContactFacadeEjbTest.java`, `docgeneration/emailTemplates/contacts/ContactEmail.cmp`, `ContactEmail.txt``
Added tests for multi-select proximities (multi/empty/single/update/index/detailed); email template variable renamed to $contact_contactProximities and empty formatting adjusted.
Infra & validation tests
\sormas-backend/src/test/java/.../InfraValidationSoundnessTest.java``
Generalized DTO-tree building/injection helpers to support Set (Collection) in addition to List.

Sequence Diagram(s)

sequenceDiagram
    participant UI as "UI (Grid / Form)"
    participant Facade as "ContactFacadeEjb"
    participant Service as "ContactService"
    participant DB as "Database"

    UI->>Facade: Request contacts (index/export/detailed)
    Facade->>DB: Execute query selecting contact fields + contact.id
    DB-->>Facade: Return rows (element-collection proximities not in multiselect)
    Facade->>Service: getContactProximitiesByContactIds(listOfIds)
    Service->>DB: Fetch Contact entities by IDs (includes contact_contactproximities)
    DB-->>Service: Return Contacts with proximities
    Service-->>Facade: Map<contactId, Set<ContactProximity)>
    Facade->>Facade: Populate DTOs via setContactProximities(map.get(id))
    Facade-->>UI: Return DTOs with contactProximities populated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • pre-release issue fixes #13586 — modifies ContactIndexDto/ContactIndexDetailedDto constructors and may overlap constructor/transformer changes in this PR.

Suggested reviewers

  • obinna-h-n
  • KarnaiahPesula

Poem

🐰 I swapped one carrot for a cheerful bunch,
Checkboxes hop freely — no single-pick crunch.
Proximities gather in rows so sweet,
DTOs and tables now dance to a beat.
Hooray — many touches make the garden complete!

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description only contains 'Fixes #13711' without providing detailed context about the changes made, violating the expected template structure with substantial implementation details missing. Provide a comprehensive description including: the problem statement from issue #13711, the solution approach (converting single ContactProximity to Set), affected components, and testing strategy.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While the PR includes extensive schema changes (SQL, new tables, triggers, test resources), these appear to be necessary infrastructure changes to support the multi-select functionality, though the scope is broader than typical bug fixes. Clarify whether schema changes to contact_contactproximities table, test resource updates, and infrastructure modifications are necessary for this fix or represent scope creep beyond the bug fix.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: converting contact type checkboxes from single-selection radio button behavior to multi-select checkboxes, matching the primary objective of issue #13711.
Linked Issues check ✅ Passed The pull request successfully addresses issue #13711 by converting the contact proximity field from a single value to a Set, enabling multi-select checkboxes throughout the API, backend, and UI layers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java (1)

186-199: CONTACT_PROXIMITIES cannot be sorted using the generic contact.get(sortProperty.propertyName) approach.

The contactProximities field is an @ElementCollection(fetch = FetchType.LAZY) of enum values (line 416 in Contact.java), which JPA cannot directly select or sort on. Attempting to sort by this field will cause a runtime exception. The code must either:

  • Remove ContactIndexDto.CONTACT_PROXIMITIES from the sortable cases, or
  • Provide alternative sorting logic that handles ElementCollections appropriately (e.g., sorting by count, existence, or a related scalar field)
🧹 Nitpick comments (18)
sormas-backend/src/main/resources/sql/sormas_schema.sql (3)

15101-15104: Clarify intent of WHERE IS NOT NULL condition in data migration.

The migration query only copies non-NULL proximity values to the new join table. Verify that contacts with NULL proximity are intentionally excluded—if any contacts should have their NULL values preserved or handled differently, this migration would silently lose that information.

Additionally, consider whether the migration should be wrapped in a transaction or include explicit logging for visibility into how many records were migrated vs. skipped.


15086-15099: Review cascading delete trigger logic for potential redundancy or conflicts.

The migration creates or drops three separate DELETE triggers on the contact table:

  • delete_history_trigger_contact_contactproximities (lines 15086-15089)
  • delete_history_trigger (lines 15091-15094)
  • delete_history_trigger_contacts_visits (lines 15096-15099)

In PostgreSQL, multiple DELETE triggers can coexist and will all fire on deletion. Verify that:

  1. All three triggers are necessary and intentional (not redundant).
  2. They don't conflict or cause unintended cascading behavior.
  3. The order of execution (if order-dependent) is preserved or immaterial.

This pattern suggests possible schema drift or trigger duplication that could become fragile as the codebase evolves.


15067-15084: Verify temporal tracking compatibility in the join table.

The join table includes a sys_period column and a versioning trigger for temporal history tracking. Confirm that:

  1. The temporal tstzrange semantics (open-ended upper bound for current rows) are correctly applied during initial insert (line 15101).
  2. The history table contact_contactproximities_history is correctly populated by the versioning trigger during subsequent updates or deletes.
  3. Querying current vs. historical proximity values across both the main table and history table yields the expected results.

If this temporal design is new or differs from other multi-valued relationships in the schema, consider documenting the pattern for consistency.

sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactService.java (2)

943-947: Follow-up suppression logic correctly generalized to multiple proximities

Using Set<ContactProximity> contactProximities = contact.getContactProximities(); and suppressing follow-up when the set is non-empty and noneMatch(ContactProximity::hasFollowUp) preserves the old semantics while supporting multiple proximity types:

  • Any proximity that requires follow-up keeps the contact in follow-up.
  • Only when all selected proximities have no follow-up (or disease has no follow-up) is follow-up disabled.

Given getContactProximities() now returns an empty set instead of null, the explicit null-check is redundant but harmless.


2033-2059: New getContactProximitiesByContactIds helper is functionally correct; consider minor refinements

The helper correctly:

  • Short-circuits null/empty input with an empty Map.
  • Retrieves Contact entities for the given IDs.
  • Maps each ID to a defensive HashSet copy of its contactProximities (or an empty set).

Two minor refinements you might consider:

  • Return Collections.emptyMap() instead of new HashMap<>() for the empty-input case, unless callers rely on mutability.
  • Document or watch usage for large ID batches, since loading full Contact entities just to read an element collection may be heavier than a more targeted query with joins, if this ever becomes performance‑critical.
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/Contact.java (1)

20-23: JPA element-collection mapping for contactProximities is sound and matches the new model

The changes cleanly migrate from a single proximity to a collection:

  • CONTACT_PROXIMITIES constant reflects the plural field name used throughout DTOs and UI.
  • Set<ContactProximity> contactProximities mapped via @ElementCollection(fetch = FetchType.LAZY), @Enumerated(EnumType.STRING), and @CollectionTable(name = "contact_contactproximities", ...) with @Column(name = "contactproximity", nullable = false) is a standard pattern for enum collections.
  • getContactProximities() lazily initializes the set, ensuring non-null collections and simplifying downstream logic such as noneMatch(ContactProximity::hasFollowUp) in ContactService.
  • setContactProximities straightforwardly replaces the collection.

Optionally, you could:

  • Initialize contactProximities at declaration (= new HashSet<>()) and drop the lazy-init in the getter.
  • Decide whether setContactProximities(null) should be treated as “no proximities” (normalize to an empty set in the setter) to make the API more robust to callers.

Also applies to: 71-71, 172-172, 414-427

sormas-backend/src/test/java/de/symeda/sormas/backend/contact/ContactFacadeEjbTest.java (1)

2350-2586: New contactProximities tests are thorough and correctly exercise the multi-select behavior

The added tests:

  • Validate persistence and round-trip retrieval of:
    • Multiple proximities (TOUCHED_FLUID, PHYSICAL_CONTACT, SAME_ROOM).
    • An explicitly empty set.
    • A single value (CLOSE_CONTACT).
  • Assert that proximities are exposed correctly in:
    • ContactIndexDto (index list).
    • ContactIndexDetailedDto (detailed index list).
  • Verify update semantics when adding additional proximities to an existing contact.

This gives good coverage of the new multi-valued model across core retrieval paths. If you want to reduce duplication, the repeated RDCF/user/case/contact setup could be factored into a small helper, but that’s purely a test-maintainability nicety.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java (1)

44-55: Grid now binds to CONTACT_PROXIMITIES, consistent with SimilarContactDto

Updating the column to SimilarContactDto.CONTACT_PROXIMITIES correctly aligns the grid with the new collection-based field on SimilarContactDto. This ensures proximities are visible in the selection grid alongside other contact attributes.

If the default Set.toString() rendering is not user-friendly enough, you might later introduce a custom renderer (e.g., comma-separated, localized captions) for this column, but the binding itself is correct.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java (2)

3-3: Consider avoiding wildcard imports.

Replacing explicit imports with a wildcard import (com.vaadin.ui.*) reduces IDE clarity and can make it harder to identify which specific classes are used.


160-166: Consider i18n for proximity values and use String.join for clarity.

The proximity concatenation uses Object::toString which produces non-localized enum names. Additionally, the reduce operation can be simplified.

Consider this approach:

-		final Label lblContactProximity = new Label(
-			referenceContact.getContactProximities() != null && !referenceContact.getContactProximities().isEmpty()
-				? referenceContact.getContactProximities().stream().map(Object::toString).reduce((a, b) -> a + ", " + b).orElse("")
-				: "");
+		final Label lblContactProximity = new Label(
+			referenceContact.getContactProximities() != null && !referenceContact.getContactProximities().isEmpty()
+				? referenceContact.getContactProximities().stream()
+					.map(p -> p.toString())
+					.collect(Collectors.joining(", "))
+				: "");

Note: For proper internationalization, consider using I18nProperties.getEnumCaption(proximity) instead of toString() to display localized enum values, similar to patterns used elsewhere in the UI layer.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java (1)

93-103: Apply i18n and consider extracting duplicate concatenation logic.

This proximity concatenation logic duplicates the pattern in ContactSelectionField.java (lines 160-163) and similarly lacks internationalization.

Consider:

  1. Using String.join(", ", ...) or Collectors.joining(", ") instead of reduce for clarity
  2. Applying I18nProperties.getEnumCaption(proximity) for localized enum values
  3. Extracting the concatenation logic into a shared utility method to eliminate duplication

Example:

 		if (contact.getContactProximities() != null && !contact.getContactProximities().isEmpty()) {
-			String proximitiesString = contact.getContactProximities().stream()
-				.map(Object::toString)
-				.reduce((a, b) -> a + ", " + b)
-				.orElse("");
+			String proximitiesString = contact.getContactProximities().stream()
+				.map(p -> I18nProperties.getEnumCaption(p))
+				.collect(Collectors.joining(", "));
 			Label lblContactProximity = new Label(StringUtils.abbreviate(proximitiesString, 50));
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java (1)

41-41: Avoid wildcard imports for clarity.

The wildcard import de.symeda.sormas.ui.utils.* reduces code clarity and may pull in unnecessary classes. Consider explicit imports for only the classes used from this package.

sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java (1)

82-82: Consider initializing contactProximities to an empty set.

The field is declared but not initialized, leaving it null until explicitly set via setContactProximities. While the javadoc explains deferred population, returning null from getContactProximities() may cause NPEs in callers that iterate without null-checking. Consider initializing to Collections.emptySet() or new HashSet<>() for defensive programming.

-	private Set<ContactProximity> contactProximities;
+	private Set<ContactProximity> contactProximities = new HashSet<>();
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java (2)

23-23: Avoid wildcard imports for better code clarity.

The wildcard import com.vaadin.v7.ui.* and others at lines 44-49 reduce code clarity. Consider explicit imports to make dependencies clearer.


358-364: Null-safe handling of proximities in updateContactProximity.

The code properly handles the case where getValue() might not be a Set, defaulting to null. Consider using Collections.emptySet() as the fallback for safer iteration downstream.

 	private void updateContactProximity() {
 
 		Object valueObj = contactProximities.getValue();
 		@SuppressWarnings("unchecked")
-		Set<ContactProximity> value = valueObj instanceof Set ? (Set<ContactProximity>) valueObj : null;
+		Set<ContactProximity> value = valueObj instanceof Set ? (Set<ContactProximity>) valueObj : Collections.emptySet();
 		FieldHelper.updateEnumData(
 			contactProximities,
 			Arrays.asList(ContactProximity.getValues(disease, FacadeProvider.getConfigFacade().getCountryLocale())));
 		contactProximities.setValue(value);
 	}
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (1)

31-31: Consider avoiding wildcard imports.

Wildcard imports (import com.vaadin.v7.ui.*;) can introduce naming conflicts and make dependencies less explicit. If the project's coding guidelines permit them, this is acceptable, but explicit imports are generally preferred for maintainability.

sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java (1)

90-96: Inconsistent null-handling compared to ContactDto.

ContactDto.getContactProximities() lazily initializes an empty HashSet if null, but SimilarContactDto.getContactProximities() returns the field directly, which may be null. Consider aligning the approach for consistency:

 public Set<ContactProximity> getContactProximities() {
+    if (contactProximities == null) {
+        contactProximities = new HashSet<>();
+    }
     return contactProximities;
 }
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java (1)

32-32: Consider using explicit imports instead of wildcards.

Wildcard imports (e.g., import de.symeda.sormas.api.contact.*;) can reduce code clarity and may cause naming conflicts. Explicit imports make dependencies clearer and IDE navigation more precise.

Also applies to: 62-62, 64-64, 67-67

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af6249b and 401265c.

📒 Files selected for processing (27)
  • sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactDto.java (4 hunks)
  • sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactExportDto.java (5 hunks)
  • sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java (3 hunks)
  • sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java (7 hunks)
  • sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java (6 hunks)
  • sormas-backend/src/main/java/de/symeda/sormas/backend/contact/Contact.java (5 hunks)
  • sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java (12 hunks)
  • sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDetailedDtoResultTransformer.java (2 hunks)
  • sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDtoResultTransformer.java (2 hunks)
  • sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java (2 hunks)
  • sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactService.java (3 hunks)
  • sormas-backend/src/main/resources/sql/sormas_schema.sql (2 hunks)
  • sormas-backend/src/test/java/de/symeda/sormas/backend/contact/ContactFacadeEjbTest.java (1 hunks)
  • sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.cmp (1 hunks)
  • sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.txt (1 hunks)
  • sormas-rest/swagger.json (3 hunks)
  • sormas-rest/swagger.yaml (3 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java (4 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java (1 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java (11 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (8 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java (1 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java (2 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java (1 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java (1 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/contactfield/ContactLineField.java (1 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/layout/LineListingLayout.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java (1)
  • SimilarContactDto (12-157)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java (1)
  • SimilarContactDto (12-157)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java (1)
  • SimilarContactDto (12-157)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java (1)
  • ContactIndexDetailedDto (20-183)
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java (1)
  • ContactIndexDto (35-485)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java (2)
sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java (1)
  • ContactIndexDto (35-485)
sormas-api/src/main/java/de/symeda/sormas/api/i18n/I18nProperties.java (1)
  • I18nProperties (39-536)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java (1)
sormas-ui/src/main/java/de/symeda/sormas/ui/utils/CssStyles.java (1)
  • CssStyles (26-546)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java (2)
sormas-ui/src/main/java/de/symeda/sormas/ui/utils/LayoutUtil.java (1)
  • LayoutUtil (30-418)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (1)
  • ContactDataForm (68-1149)
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java (2)
sormas-api/src/main/java/de/symeda/sormas/api/utils/fieldaccess/checkers/AnnotationBasedFieldAccessChecker.java (1)
  • AnnotationBasedFieldAccessChecker (23-68)
sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java (1)
  • ContactIndexDto (35-485)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: android app test (26)
  • GitHub Check: SORMAS CI
  • GitHub Check: android app test (28)
  • GitHub Check: android app test (27)
  • GitHub Check: Lint Code Base
🔇 Additional comments (39)
sormas-backend/src/main/resources/sql/sormas_schema.sql (1)

15107-15108: Ensure DROP COLUMN IF EXISTS is intentional and safe.

The use of DROP COLUMN IF EXISTS suggests this migration may be idempotent or re-runnable. Verify:

  1. Whether re-running the migration script is a supported use case for this codebase.
  2. Whether any dependent views, stored procedures, or application code still reference the old contactproximity column and could break.
  3. Whether the IF EXISTS clauses prevent proper error detection if the migration partially fails.

If this migration should run only once, consider removing IF EXISTS to fail loudly if the column has already been dropped, signaling a deployment or script-ordering issue.

sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.cmp (1)

19-21: Empty-properties marker change looks fine; just confirm dual markers are intentional

Switching line 20 to [] is consistent with representing empty collections and aligns with the updated text template. With ./. still present on line 21, please confirm both markers are expected in this comparison template and not a leftover from the old format.

sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.txt (1)

21-26: Template variable rename correctly reflects collection-based proximities

Using $contact_contactProximities here matches the new contactProximities field and constant naming in the API/DTOs. This looks correct; just ensure any remaining $contact_contactProximity placeholders in other templates were also updated.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/layout/LineListingLayout.java (1)

5-5: Correctly adapts line-listing typeOfContact to the new contactProximities set

Wrapping the single typeOfContact value into Collections.singleton(...) and assigning it to contactProximities is a clean way to bridge the existing single-select line-listing UI with the new multi-valued backend model. The null guard avoids accidentally overwriting with an empty set.

Also applies to: 25-25, 199-202

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/contactfield/ContactLineField.java (1)

112-119: Caption key updated to CONTACT_PROXIMITIES and is consistent with the new field name

Using ContactDto.CONTACT_PROXIMITIES for the typeOfContact caption keeps this line-listing field aligned with the renamed contact property and i18n keys. The control itself remains a single-select ComboBox<ContactProximity>, which is fine for now given that line listing was already single-select; just confirm that you don’t also want multi-select behavior in this specific UI context, now that the backend supports it.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java (1)

39-39: LGTM: Constant name updated to reflect multi-proximity support.

The change from CONTACT_PROXIMITY to CONTACT_PROXIMITIES aligns with the API-wide migration to support multiple proximity values per contact.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java (1)

42-42: LGTM: Column reference updated for multi-proximity support.

The constant name change from CONTACT_PROXIMITY to CONTACT_PROXIMITIES correctly updates the column insertion logic to align with the new multi-value proximity field.

sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java (1)

111-111: Post-query enrichment pattern correctly implements contactProximities population.

The query selection changed from contact.get(Contact.CONTACT_PROXIMITY) to contact.get(Contact.ID) because contactProximities is an ElementCollection that cannot be selected in multiselect queries. The implementation correctly populates it separately after querying using ContactService.getContactProximitiesByContactIds(), as seen in ContactFacadeEjb.java (lines 1283–1287 and 1340–1344). The pattern is intentional and documented in the code.

sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDtoResultTransformer.java (1)

49-49: Verify alignment between ContactIndexDto constructor, query projection, and result transformer casting.

The tuple element cast changed from ContactProximity to Long at line 49. Confirmed this aligns with:

  1. The ContactIndexDto constructor (line 123) which expects Long id at this parameter position
  2. The query projection in ContactListCriteriaBuilder (line 112) which selects contact.get(Contact.ID)

The change is correct and properly aligned across all three components.

sormas-rest/swagger.yaml (1)

11642-11660: All contactProximity references have been properly converted to contactProximities across the swagger.yaml file.

The changes are correct and complete:

  • No orphaned singular contactProximity property definitions remain
  • All 14 enum values match exactly with the Java ContactProximity enum definition
  • All three Contact-related schemas (ContactDto, ContactIndexDetailedDto, ContactIndexDto) have been consistently updated
  • The OpenAPI/Swagger syntax is correct

The conversion aligns with the PR objective to enable multiple proximity selections and maintains consistency across the API specification.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java (2)

166-177: LGTM! Renderer implementation is correct.

The renderer properly handles null and empty sets, uses localized enum captions via I18nProperties.getEnumCaption, and joins multiple proximities with commas. The null/empty check prevents NPEs.


224-224: Column reference correctly updated.

The column reference is properly updated to CONTACT_PROXIMITIES to align with the new multi-value data model.

sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java (2)

116-119: Javadoc clearly explains the design decision.

Good documentation explaining why contactProximities is populated separately due to JPA ElementCollection limitations.


473-479: New id accessors support the post-query population pattern.

The getId() and setId() methods enable the backend to populate contactProximities after the initial query using the contact's ID.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java (3)

86-86: Type change from NullableOptionGroup to OptionGroup is correct.

The change to OptionGroup (with setMultiSelect(true)) properly enables multiple checkbox selections, which is the core fix for issue #13711.


193-205: Multi-select implementation correctly enables multiple proximity selections.

The implementation:

  • Uses OptionGroup with setMultiSelect(true) to allow multiple selections
  • Properly handles the value as a Set<ContactProximity> in the value change listener
  • Includes appropriate type checking (instanceof Set) before casting

This directly addresses the bug where checkboxes behaved like radio buttons.


329-332: Delegation to ContactDataForm.deduceContactCategory promotes code reuse.

Good refactoring to delegate category deduction logic to ContactDataForm, avoiding duplication and ensuring consistent behavior across forms.

sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java (2)

69-69: Constructor parameter correctly updated to Long id.

The parameter change from ContactProximity to Long id aligns with the parent class ContactIndexDto and the overall migration to post-query proximity population.


84-88: Super constructor call properly passes id parameter.

The super call correctly passes the id parameter to ContactIndexDto, maintaining consistency with the parent class's updated signature.

sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactExportDto.java (4)

123-123: Field type correctly changed to Set<ContactProximity>.

The field declaration aligns with the multi-value proximity model. Note that like ContactIndexDto, this field starts as null until populated post-query.


250-253: Clear javadoc explaining deferred population pattern.

Good documentation explaining why contactProximities cannot be selected directly in JPA queries due to ElementCollection limitations.


308-308: Inline comment clarifies intentional design.

The comment clearly indicates that contactProximities is intentionally not set in the constructor and will be populated separately.


610-614: Export annotation and getter correctly updated.

The @ExportProperty annotation correctly references ContactDto.CONTACT_PROXIMITIES, and the return type is properly changed to Set<ContactProximity>. The backend export logic properly handles Set serialization—ContactFacadeEjb explicitly populates contactProximities via getContactProximitiesByContactIds(), which returns Map<Long, Set<ContactProximity>>, confirming the field integrates seamlessly into the export flow.

sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDetailedDtoResultTransformer.java (1)

53-53: Tuple mapping correctly updated for Long id parameter.

The cast to (Long) aligns with the ContactIndexDetailedDto constructor signature where the id field is positioned before ContactClassification. The ContactProximities are populated separately after query execution through a post-processing step that uses the contact IDs retrieved from the DTO.

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (2)

268-271: Multi-select OptionGroup setup looks correct.

The field is properly configured with setMultiSelect(true) and the horizontal style is removed for better UX with multiple checkboxes.


755-789: Static helper method is a good pattern for testability.

Extracting deduceContactCategory as a package-visible static method allows unit testing the category deduction logic independently of the UI form.

sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactDto.java (2)

548-553: Lazy initialization in getter is acceptable but mutates state.

The getter initializes contactProximities if null. This pattern prevents NPEs when iterating over the collection, but be aware that calling getContactProximities() modifies internal state—subsequent serialization or comparison may behave differently than expected if the getter was never called.


87-87: API constant rename is a breaking change.

Renaming CONTACT_PROXIMITY to CONTACT_PROXIMITIES is a public API change. Ensure all consumers (including external integrations and REST clients) are updated accordingly.

#!/bin/bash
# Search for any remaining references to the old constant name
rg -n "CONTACT_PROXIMITY[^I]" --type java
sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java (2)

150-156: New id field getter/setter looks correct.

The id field enables post-query population of proximities by contact ID. This is a reasonable pattern when proximities are fetched separately to avoid N+1 query issues.


44-64: No changes needed – constructor behavior is intentional.

contactProximities is not initialized in the constructor by design. Element collections cannot be selected in JPA multiselect queries, so contactProximities is populated separately in ContactFacadeEjb.getMatchingContacts() via setContactProximities() before returning. This pattern matches ContactIndexDto and ContactExportDto. Callers properly null-check before accessing the field, and the field is always populated before objects are exposed externally.

sormas-rest/swagger.json (2)

13980-13989: Consistency verified across ContactIndexDto and ContactIndexDetailedDto.

The schema changes in hunks 2 and 3 maintain consistency with hunk 1. All three Contact-related DTOs correctly define contactProximities as an array of enum strings with the same 14 proximity types.

Also applies to: 14151-14160


13633-13639: Schema migration from singular to plural is complete; document breaking API change.

The migration from contactProximity to contactProximities is complete across all affected schemas (ContactDto, ContactIndexDto, ContactIndexDetailedDto). All three schema definitions are consistent in structure and no stale singular references remain.

This is a breaking change for REST clients expecting the old contactProximity property. Consider documenting the breaking change in release notes and providing a migration guide for API consumers.

sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java (7)

753-758: LGTM! Proximity loading pattern correctly handles JPA limitation.

The separate query to populate contactProximities is a proper workaround for JPA's ElementCollection limitation in multiselect queries. The use of getOrDefault with an empty HashSet ensures safe handling of missing entries.


1282-1288: LGTM! Consistent proximity loading implementation.

The proximity population follows the same correct pattern established in getExportList, maintaining consistency across the codebase.


1339-1345: LGTM! Proximity loading consistently applied.


1502-1502: LGTM! Entity mapping correctly updated for collection.

The change from singular to plural correctly reflects the new collection-based proximity model.


1854-1854: LGTM! Excellent defensive copy implementation.

Creating a new HashSet prevents unintended sharing of mutable collection state between the entity and DTO. The null-safe check is also appropriate.


2080-2080: LGTM! Matching contacts query correctly adapted.

The query selection change from Contact.PROXIMITY to Contact.ID aligns with the separate proximity loading pattern, maintaining consistency across all query methods.

Also applies to: 2092-2098


2363-2363: LGTM! Completeness calculation correctly handles collection.

The condition properly checks both null and emptiness, ensuring that only contacts with actual proximity data contribute to the completeness score.

Comment thread sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 401265c and 9f91e1c.

📒 Files selected for processing (2)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (8 hunks)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/utils/NullableOptionGroup.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: android app test (28)
  • GitHub Check: android app test (27)
  • GitHub Check: android app test (26)
  • GitHub Check: Lint Code Base
  • GitHub Check: SORMAS CI
🔇 Additional comments (4)
sormas-ui/src/main/java/de/symeda/sormas/ui/utils/NullableOptionGroup.java (1)

74-80: LGTM! Well-designed helper method.

The setNullableValue method properly handles setting a single value on this multi-select component by wrapping it in a singleton set. This complements the existing getNullableValue getter and enables clean single-value semantics where needed (e.g., setting contact category from multiple proximities).

sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (3)

267-270: LGTM! Correctly implements multi-select proximity.

The field is properly configured as an OptionGroup with setMultiSelect(true), enabling the checkbox behavior described in issue #13711. Removing the horizontal styling is appropriate for a multi-select checkbox group.


102-102: LGTM! References consistently updated.

All references to the contact proximity field have been correctly updated from the singular contactProximity to the plural contactProximities, maintaining consistency throughout the form layout, styling, and data population logic.

Also applies to: 697-697, 888-888


750-788: Confirm: Past issue resolved and German system coverage is complete, but unmapped enum values pose maintenance risk.

The refactoring correctly handles multiple proximities:

  • Line 785 correctly uses setNullableValue(highestRiskCategory) instead of passing a Set to setValue, resolving the critical bug from the previous review
  • Static helper method deduceContactCategory is well-designed for testability
  • Null and empty checks are present

For German systems (where this method is used), all ContactProximity values returned by ContactProximity.getValues() for CORONAVIRUS are covered:

  • FACE_TO_FACE_LONG, TOUCHED_FLUID, AEROSOL → HIGH_RISK
  • MEDICAL_UNSAFE → HIGH_RISK_MED
  • MEDICAL_LIMITED → MEDIUM_RISK_MED
  • SAME_ROOM, FACE_TO_FACE_SHORT, MEDICAL_SAME_ROOM → LOW_RISK
  • MEDICAL_SAFE, MEDICAL_DISTANT → NO_RISK

However, four ContactProximity enum values remain unmapped: PHYSICAL_CONTACT, CLOTHES_OR_OTHER, CLOSE_CONTACT, and AIRPLANE. If these are ever passed to the method (via API or future changes), the contact category will silently remain unchanged. Consider either mapping these values or adding a default case with explicit handling and a comment explaining why they are excluded.

Comment on lines +275 to 279
contactProximities.addValueChangeListener(e -> {
if (!getInternalValue().getContactProximities().equals(e.getProperty().getValue()) || contactCategory.isModified()) {
updateContactCategory((Set<ContactProximity>) contactProximities.getValue());
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -B2 -A2 'getContactProximities\(\)' --type=java -g '!*Test.java'

Repository: SORMAS-Foundation/SORMAS-Project

Length of output: 8605


🏁 Script executed:

cat -n sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java | sed -n '250,300p'

Repository: SORMAS-Foundation/SORMAS-Project

Length of output: 2865


🏁 Script executed:

rg -n 'addValueChangeListener' sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java -A5 | head -50

Repository: SORMAS-Foundation/SORMAS-Project

Length of output: 2334


Add null safety check for form internal value in listener.

The comparison at line 276 chains calls without checking if getInternalValue() is null. While getContactProximities() has defensive initialization in its getter, the setter can explicitly set the value to null (as seen in ContactFacadeEjb.java:1854), and more importantly, getInternalValue() itself may be null during form initialization or if the form is not properly bound. This pattern should be defensive, as shown in similar listeners elsewhere in the file (e.g., line 452: if (getValue() != null)).

Wrap the comparison with a null check: if (getInternalValue() != null && !getInternalValue().getContactProximities().equals(...) || ...)

🤖 Prompt for AI Agents
In sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java
around lines 275-279, the listener calls
getInternalValue().getContactProximities() without guarding against a null
internal value; update the if condition to check getInternalValue() != null
before accessing its fields and ensure correct grouping so the overall condition
becomes: (getInternalValue() != null &&
!getInternalValue().getContactProximities().equals(e.getProperty().getValue()))
|| contactCategory.isModified(); this prevents NPE during form initialization or
when the internal value is explicitly set to null.

… to prevent potential NullPointerExceptions during form initialization or when the form is not properly bound
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the formatter has collapsed package imports. Please check because with the eclipse formatter it should not happen.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the modifications for DTOs are causing a test to fail:

Error:    InfraValidationSoundnessIncomingTest>InfraValidationSoundnessTest.testShareContactValidation:654->InfraValidationSoundnessTest.assertValidationDto:541->InfraValidationSoundnessTest.getExpected:521->InfraValidationSoundnessTest.getInfraPaths:468->InfraValidationSoundnessTest.buildDtoTree:238->InfraValidationSoundnessTest.buildDtoTree:238->InfraValidationSoundnessTest.buildDtoTree:228 » NotImplemented Ping @JonasCir
Error:    InfraValidationSoundnessOutgoingTest>InfraValidationSoundnessTest.testShareContactValidation:654->InfraValidationSoundnessTest.assertValidationDto:541->InfraValidationSoundnessTest.getExpected:521->InfraValidationSoundnessTest.getInfraPaths:468->InfraValidationSoundnessTest.buildDtoTree:238->InfraValidationSoundnessTest.buildDtoTree:238->InfraValidationSoundnessTest.buildDtoTree:228 » NotImplemented Ping @JonasCir 

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sormas-backend/src/test/java/de/symeda/sormas/backend/sormastosormas/validation/InfraValidationSoundnessTest.java (1)

380-418: Missing SetHashSet conversion will cause instantiation failure.

The traversal logic at lines 403-418 now correctly supports Set, but the instantiation logic at lines 380-383 only handles ListArrayList conversion. When a Set field is null and needs to be created, line 396 will throw NoSuchMethodException because Set is an interface with no constructor.

🐛 Proposed fix
 Class<?> erasedType = currentField.getType().getErasedType();
 if (erasedType == List.class) {
     erasedType = ArrayList.class;
 }
+if (erasedType == Set.class) {
+    erasedType = HashSet.class;
+}

# Conflicts:
#	sormas-backend/src/main/resources/sql/sormas_schema.sql
@roldy roldy merged commit e85d966 into development Jan 28, 2026
5 of 9 checks passed
@roldy roldy deleted the bugfix-13711-type_of_contact_checkboxes branch January 28, 2026 11:41
@sormas-vitagroup
Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type of Contact Checkboxes behave like Radio Buttons - Create new Contact Form

3 participants