Skip to content

Feature: Add recent filters drop-down to the message browser#279

Open
mgaffigan wants to merge 5 commits into
OpenIntegrationEngine:mainfrom
mgaffigan:feature/mru-filters
Open

Feature: Add recent filters drop-down to the message browser#279
mgaffigan wants to merge 5 commits into
OpenIntegrationEngine:mainfrom
mgaffigan:feature/mru-filters

Conversation

@mgaffigan
Copy link
Copy Markdown
Contributor

@mgaffigan mgaffigan commented Mar 29, 2026

Adds a recent button to the message browser pane. Allows restoration of the last 10 searches on the channel.

image image

Closes #273

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 29, 2026

Test Results

  112 files  + 2    216 suites  +4   6m 57s ⏱️ + 1m 5s
  657 tests + 7    657 ✅ + 7  0 💤 ±0  0 ❌ ±0 
1 314 runs  +14  1 314 ✅ +14  0 💤 ±0  0 ❌ ±0 

Results for commit c360924. ± Comparison against base commit bfe3348.

♻️ This comment has been updated with latest results.

@mgaffigan mgaffigan changed the title Feature/mru filters Feature: Add recent filters drop-down to the message browser Mar 29, 2026
@mgaffigan mgaffigan force-pushed the feature/mru-filters branch from 83970d3 to 2cb78d3 Compare March 29, 2026 21:12
@mgaffigan mgaffigan requested a review from Copilot March 29, 2026 21:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a “Recent…” drop-down to the Message Browser so users can quickly re-apply the last ~10 searches per channel, backed by persisted client preferences. To support MRU de-duplication and display text, the shared MessageFilter model gains equality logic and a new criteria-oriented string formatter.

Changes:

  • Add client-side storage for per-channel recent message filters (persisted in user preferences) and a “Recent…” UI button to restore them.
  • Add applyFilter(...) plumbing to push a saved MessageFilter back into the advanced filter UI.
  • Enhance shared filter model objects with equals/hashCode and add new test coverage around equality / criteria formatting.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
server/test/com/mirth/connect/model/MessageFilterModelTest.java Adds unit tests covering MessageFilter equality, emptiness semantics, and criteria-string output.
server/src/com/mirth/connect/model/filters/elements/MetaDataSearchElement.java Implements equals/hashCode for metadata search elements (enables deep equality in filters).
server/src/com/mirth/connect/model/filters/elements/ContentSearchElement.java Implements equals/hashCode for content search elements (enables deep equality in filters).
server/src/com/mirth/connect/model/filters/MessageFilter.java Adds deep equality + isEmpty(), and introduces a criteria-style toString(Map, padding, includeEmptyCriteria) used by the client.
client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserRecentFilterStore.java New preference-backed store for MRU MessageFilter lists per channel.
client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserAdvancedFilter.java Adds applyFilter(...) to restore advanced selections from a saved MessageFilter.
client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowser.java Adds “Recent…” button/menu, saves filters on search, and reuses MessageFilter.toString(...) for the criteria pane.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/src/com/mirth/connect/model/filters/MessageFilter.java
Comment thread server/src/com/mirth/connect/model/filters/MessageFilter.java Outdated
Comment thread client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowser.java Outdated
Comment on lines +32 to +41
public List<MessageFilter> getRecentFilters() {
try {
String serialized = Preferences.userNodeForPackage(Mirth.class).get(prefKey, "");
if (StringUtils.isBlank(serialized)) return List.of();

return (List<MessageFilter>) ObjectXMLSerializer.getInstance().deserialize(serialized, List.class);
} catch (Exception e) {
// Fail quietly if the stored filters cannot be deserialized for any reason.
e.printStackTrace();
return List.of();
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

getRecentFilters() deserializes into a raw List.class and then unchecked-casts to List<MessageFilter>. If the stored preference value is corrupted or from a different version, elements may not actually be MessageFilter instances, leading to runtime failures later (e.g., when iterating in restoreRecentFilter()). Consider validating the deserialized value/type (ensure it’s a List and that each element is a MessageFilter) before returning it, otherwise return an empty list / clear the preference key.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the first version. That's future me's problem.

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.

new ArrayList<>()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@kpalang, not sure what you are referring to by that.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
@mgaffigan mgaffigan force-pushed the feature/mru-filters branch from 2cb78d3 to b1cefe0 Compare March 29, 2026 22:25
@mgaffigan mgaffigan marked this pull request as ready for review March 29, 2026 22:30
@mgaffigan mgaffigan requested review from a team, NicoPiel, gibson9583, jonbartels, kayyagari, kpalang, ssrowe and tonygermano and removed request for a team March 29, 2026 22:31
@NicoPiel
Copy link
Copy Markdown
Collaborator

Great feature. Will look at the code as soon as I am able to.

Copy link
Copy Markdown
Contributor

@jonbartels jonbartels left a comment

Choose a reason for hiding this comment

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

Pretty nice! Left two comments for improvements to property handling and maybe a simpler data structure

import com.mirth.connect.model.filters.MessageFilter;

class MessageBrowserRecentFilterStore {
private static final int MAX_RECENT_FILTERS = 10;
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.

Max should be configurable in mirth.properties

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can we take a feature issue on this? It seems useful even with the MRU size hard coded.

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.

Sure. How about a broad feature issue that finds hardcoded constants or limits in the Client and makes them configurable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That sounds like a big to me, but maybe with sub-issues for each constant, sure. The issues I had in mind were:

  1. Allow administrators to default and lock user preferences via mirth.properties
  2. Make MAX_RECENT_FILTERS a user preference

@pacmano1 pacmano1 self-requested a review May 26, 2026 16:02
@pacmano1
Copy link
Copy Markdown
Contributor

This feature may leave PII and PHI locally on a workstaiton. So in its current storage format, it's a no for me.

Copy link
Copy Markdown
Contributor

@pacmano1 pacmano1 left a comment

Choose a reason for hiding this comment

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

Can leave PII/PHI on local workstation. Storage format needs to be changed.

@gibson9583
Copy link
Copy Markdown
Contributor

This feature may leave PII and PHI locally on a workstaiton. So in its current storage format, it's a no for me.

agree that PII/PHI is a risk. Perhaps we should rethink how this works? While it may negate how useful this is, perhaps this is cleared on user logout? If thats the case, maybe we then dont cache by channelId but rather global so user doesnt have to build the cache on every channel.

@pacmano1
Copy link
Copy Markdown
Contributor

This feature may leave PII and PHI locally on a workstaiton. So in its current storage format, it's a no for me.

agree that PII/PHI is a risk. Perhaps we should rethink how this works? While it may negate how useful this is, perhaps this is cleared on user logout? If thats the case, maybe we then dont cache by channelId but rather global so user doesnt have to build the cache on every channel.

admin session can be terminated without logout.

@mgaffigan
Copy link
Copy Markdown
Contributor Author

@pacmano1 / @gibson9583

This feature may leave PII and PHI locally on a workstaiton. So in its current storage format, it's a no for me.

agree that PII/PHI is a risk. Perhaps we should rethink how this works? While it may negate how useful this is, perhaps this is cleared on user logout? If thats the case, maybe we then dont cache by channelId but rather global so user doesnt have to build the cache on every channel.

I agree with the concern, but think storing any text search only in memory or in preferences is likely sufficient. I'll update the PR to address the unencrypted-local-storage of text searches.

@gibson9583
Copy link
Copy Markdown
Contributor

@pacmano1 / @gibson9583

This feature may leave PII and PHI locally on a workstaiton. So in its current storage format, it's a no for me.

agree that PII/PHI is a risk. Perhaps we should rethink how this works? While it may negate how useful this is, perhaps this is cleared on user logout? If thats the case, maybe we then dont cache by channelId but rather global so user doesnt have to build the cache on every channel.

I agree with the concern, but think storing any text search only in memory or in preferences is likely sufficient. I'll update the PR to address the unencrypted-local-storage of text searches.

in-memory is good with me. It doesnt need to survive client restart. Preferences is the only issue I can see because that is stored unencrypted and available to any process on server.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
@mgaffigan mgaffigan requested a review from pacmano1 May 26, 2026 17:18
@mgaffigan
Copy link
Copy Markdown
Contributor Author

@pacmano1 / @gibson9583, updated to use in-memory storage only. That's tantamount to the rest of the PHI which already resides in memory.

Copy link
Copy Markdown
Contributor

@kpalang kpalang left a comment

Choose a reason for hiding this comment

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

Some suggestions and nitpicks, but overall a nice implementation.

Comments formatted as conventional comments.

Comment on lines +1 to +8
/*
* Copyright (c) Mirth Corporation. All rights reserved.
*
* http://www.mirthcorp.com
*
* The software in this package is published under the terms of the MPL license a copy of which has
* been included with this distribution in the LICENSE.txt file.
*/
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.

question: Why Mirth Corporation copyright if this is a new class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great question. I'll fix.

public List<MessageFilter> getRecentFilters() {
try {
String serialized = RECENT_FILTERS_BY_CHANNEL.getOrDefault(prefKey, "");
if (StringUtils.isBlank(serialized)) return List.of();
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.

suggestion: Use stdlib's String.isBlank() instead of the library call.

The StringUtils from commons is typically used to check for null and whitespace in a single call, but here it's realistically impossible to get a null value, as the prefKey is either concatenated from a certainly non-null string or defaulted to an empty string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Will fix.

var result = ObjectXMLSerializer.getInstance().deserialize(serialized, List.class);
if (result == null) return List.of();

return (List<MessageFilter>) result;
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.

question: Would using the ObjectXMLSerializer.getInstance().deserializeList(serialized, MessageFilter.class) call solve the unchecked (and redundant) cast squiggly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure. But deserializeList seems more semantically reasonable, so I'll switch to that.

return (List<MessageFilter>) result;
} catch (Exception e) {
// Fail quietly if the stored filters cannot be deserialized for any reason.
e.printStackTrace();
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.

suggestion: I know you hate future you, but add proper logging and possibly prepopulate log2j.xml with the appropriate knobs for visibility control.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah... fine. I guess I'll eat my broccoli.

Comment on lines +32 to +41
public List<MessageFilter> getRecentFilters() {
try {
String serialized = Preferences.userNodeForPackage(Mirth.class).get(prefKey, "");
if (StringUtils.isBlank(serialized)) return List.of();

return (List<MessageFilter>) ObjectXMLSerializer.getInstance().deserialize(serialized, List.class);
} catch (Exception e) {
// Fail quietly if the stored filters cannot be deserialized for any reason.
e.printStackTrace();
return List.of();
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.

new ArrayList<>()

}

if (minMessageId != null) {
if (text.length() > 0) text.append(padding);
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.

question: Why the length check instead of !isEmpty()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out-of-date habits.


if (textSearch != null) {
if (text.length() > 0) text.append(padding);
text.append("Text Search: " + textSearch);
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.

nitpick: Intellij complains about a concat inside an append().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's what was checked in originally. I agree it should be fixed, though. Not critical, but I'm here.

} else if (originalIdLower == null) {
text.append("Less than " + originalIdUpper);
} else {
text.append("Between " + originalIdLower + " and " + originalIdUpper);
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.

suggestion: A String.formatted() would be more readable here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's what was checked in originally. Moving it to separate append calls.

Comment on lines +420 to +421
if (element.getValue() instanceof Calendar) {
Calendar date = (Calendar) element.getValue();
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.

suggestion: Pattern matching because pretty.


if (includeEmptyCriteria
|| (includedMetaDataIds != null && !includedMetaDataIds.isEmpty())
|| (excludedMetaDataIds != null && !excludedMetaDataIds.isEmpty())) {
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.

nitpick: I prefer having the } at on a new line to create a better visual separation.

if (includeEmptyCriteria
    || (includedMetaDataIds != null && !includedMetaDataIds.isEmpty())
    || (excludedMetaDataIds != null && !excludedMetaDataIds.isEmpty()))
{
    getConnectorSearchCriteriaText(text, padding, connectors);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I prefer a linter be used to settle whitespace wars, but I've added a linebreak.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
@mgaffigan mgaffigan requested a review from kpalang May 26, 2026 21:16
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.

[IDEA] Recent filters drop down

7 participants