Skip to content

Conversation

@roldy
Copy link

@roldy roldy commented Dec 11, 2025

Fixes #13711

Summary by CodeRabbit

  • New Features

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

    • Public APIs, export formats and API docs updated to accept/return a list of contact proximities; DTOs and schemas reflect the change.
  • Database

    • Schema changed to persist multiple proximities per contact (new join table) and broad history/versioning improvements.
  • Backend

    • Backend logic updated to handle proximities as collections and provides a retrieval method mapping contact IDs to proximity sets.
  • 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

coderabbitai bot commented Dec 11, 2025

Walkthrough

This PR converts contact proximity from a single ContactProximity value to a collection (Set) across API DTOs, JPA entities, DB schema, backend services, transformers, REST specs, UI, tests, and adds post-query proximity population by contact ID.

Changes

Cohort / File(s) Summary
API DTOs — Contact-related
sormas-api/src/main/java/de/symeda/sormas/api/contact/
\ContactDto.java`, `ContactExportDto.java`, `ContactIndexDto.java`, `ContactIndexDetailedDto.java`, `SimilarContactDto.java``
Renamed CONTACT_PROXIMITYCONTACT_PROXIMITIES; replaced ContactProximity contactProximity with Set<ContactProximity> contactProximities; updated getters/setters/constructors and added lazy init where applicable.
Backend entity & JPA mapping
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/
\Contact.java``
Replaced single proximity with @ElementCollection(fetch = LAZY) mapped Set<ContactProximity> contactProximities persisted to contact_contactproximities table; constant renamed and lazy initialization added.
Facade, service & population logic
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/
\ContactFacadeEjb.java`, `ContactService.java``
Removed proximity from multi-select queries; added post-query proximity loading getContactProximitiesByContactIds(List<Long>) and populate DTOs from Map<id,Set<...>>; updated completeness and follow-up logic to operate on collections.
Result transformers & criteria
sormas-backend/src/main/java/de/symeda/sormas/backend/contact/
\ContactIndexDtoResultTransformer.java`, `ContactIndexDetailedDtoResultTransformer.java`, `ContactListCriteriaBuilder.java``
Query selection changed from proximity to contact ID; transformers adjusted constructor arg types (ContactProximity → Long id) and sort/property references updated to CONTACT_PROXIMITIES.
Database schema & migrations
sormas-backend/src/main/resources/sql/
\sormas_schema.sql``
Added join table contact_contactproximities and many schema/versioning/history trigger changes; converted proximity to multi-valued storage and applied related migration changes.
REST API specs
sormas-rest/
\swagger.json`, `swagger.yaml``
Replaced contactProximity (enum string) with contactProximities (array of enum) across multiple schemas; added other DTO property extensions.
UI — grids, forms, components
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ and subpaths
\AbstractContactGrid.java`, `ContactCreateForm.java`, `ContactDataForm.java`, `ContactGridDetailed.java`, `ContactSelectionField.java`, `ContactSelectionGrid.java`, `ContactCaseConversionSelectionGrid.java`, `SourceContactListEntry.java`, ...``
Switched single-select widgets to multi-select OptionGroup, updated renderers to join localized captions, changed bindings to CONTACT_PROXIMITIES, added deduceContactCategory(Set<...>) helper and NullableOptionGroup.setNullableValue(Object).
Backend tests & templates
sormas-backend/src/test/... and resources
\ContactFacadeEjbTest.java`, `docgeneration/emailTemplates/contacts/ContactEmail.cmp`, `ContactEmail.txt``
Added tests for multi-select proximities (multi, empty, single, update, index/detailed checks); updated email template variable to $contact_contactProximities and empty formatting.

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 multi-select query (select contact fields + contact.id)
    DB-->>Facade: Return rows (without element-collection proximities)
    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

  • Areas needing extra attention:
    • DB schema migration and the new join table contact_contactproximities in sormas_schema.sql
    • Performance and correctness of post-query population logic in ContactFacadeEjb.java
    • Follow-up calculation changes in ContactService.java (collection-based logic)
    • UI multi-select interactions and category deduction in ContactDataForm / ContactCreateForm
    • Result transformers and query selection adjustments (type shifts from enum → id)

Suggested reviewers

  • obinna-h-n
  • KarnaiahPesula

Poem

🐰 I once picked one, now I pick a set,
Checkboxes cheerful, no longer a fret.
Proximities gather in tidy arrays,
DTOs and tables hop in new ways.
Hooray — many touches brighten my days!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Multiple changes appear out-of-scope: extensive database schema modifications, test additions, swagger/REST API updates, and template file changes are not strictly necessary to fix the UI checkbox behavior issue. Review and justify database schema changes, REST API modifications, and test additions. Consider separating schema migrations and API documentation updates into separate PRs if not directly required for the checkbox fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the main change: converting contact type checkboxes to allow multiple selections instead of behaving like radio buttons.
Description check ✅ Passed Description only contains 'Fixes #13711' with template comments. While minimal, it is fully related to the changeset and links to the issue with clear objectives.
Linked Issues check ✅ Passed Changes comprehensively address #13711 objectives: converts contactProximity field to Set contactProximities and updates UI components from NullableOptionGroup (radio-button behavior) to OptionGroup with multi-select enabled, allowing multiple simultaneous selections.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-13711-type_of_contact_checkboxes

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb45c17 and ff1b689.

📒 Files selected for processing (1)
  • sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (8 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 (26)
  • GitHub Check: android app test (27)
  • GitHub Check: Lint Code Base
  • GitHub Check: SORMAS CI
🔇 Additional comments (7)
sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java (7)

35-35: LGTM: Import addition supports multi-select functionality.

The Set import is correctly added to support the new multi-select contact proximities feature.


51-51: LGTM: Field type change enables multi-select.

Changing from NullableOptionGroup to OptionGroup is correct for supporting multi-select contact proximities via checkboxes. The contactCategory field correctly remains as NullableOptionGroup since it's single-select.

Also applies to: 186-186


144-144: LGTM: Layout reference correctly updated.

The HTML layout correctly references ContactDto.CONTACT_PROXIMITIES to match the renamed field.


740-740: LGTM: Soft required styling correctly applied.

The soft required style is correctly updated to reference contactProximities.


793-830: LGTM: Multi-proximity risk calculation correctly implemented.

The refactored updateContactCategory method and new deduceContactCategory helper correctly handle multiple proximity selections:

  1. Proper null/empty handling: Line 798 checks for null and empty set before processing.
  2. Correct risk priority logic: The if-else chain (lines 802-824) properly determines the highest risk category from multiple selections, with HIGH_RISK taking precedence over lower risk categories.
  3. Single value assignment: Line 827 correctly calls setNullableValue(highestRiskCategory) with a single ContactCategory value (past review concern about passing a Set was addressed in commit 9f91e1c).
  4. updateDiseaseConfiguration: Line 930 correctly references the multi-select field.

Note: When the proximities set is empty, the category is not updated (line 798 returns early). This seems reasonable, but verify this is the desired behavior—you might want to explicitly clear or set a default category when all proximities are deselected.

Also applies to: 930-930


309-312: No action needed. The caption Contact_contactProximityLongForm is correctly configured for the multi-select field. The actual caption text is "Type of contact - if multiple pick the closest contact proximity," which explicitly addresses the multi-select scenario and provides appropriate guidance to users on handling multiple selections.


317-322: Null safety is properly handled through defensive initialization in the getter; logic grouping is correct.

The getContactProximities() getter in ContactDto has defensive initialization that guarantees a non-null Set return. The condition logic A && (B || C) correctly ensures updates only occur when internal value exists and either proximities differ or category changed. Both concerns from the past review are addressed by the current implementation.


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

@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.

Copy link

@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

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
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

2 participants