Feature: Add recent filters drop-down to the message browser#279
Feature: Add recent filters drop-down to the message browser#279mgaffigan wants to merge 5 commits into
Conversation
83970d3 to
2cb78d3
Compare
There was a problem hiding this comment.
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 savedMessageFilterback into the advanced filter UI. - Enhance shared filter model objects with
equals/hashCodeand 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.
| 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This is the first version. That's future me's problem.
There was a problem hiding this comment.
@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>
2cb78d3 to
b1cefe0
Compare
|
Great feature. Will look at the code as soon as I am able to. |
jonbartels
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Max should be configurable in mirth.properties
There was a problem hiding this comment.
Can we take a feature issue on this? It seems useful even with the MRU size hard coded.
There was a problem hiding this comment.
Sure. How about a broad feature issue that finds hardcoded constants or limits in the Client and makes them configurable?
There was a problem hiding this comment.
That sounds like a big to me, but maybe with sub-issues for each constant, sure. The issues I had in mind were:
- Allow administrators to default and lock user preferences via mirth.properties
- Make
MAX_RECENT_FILTERSa user preference
|
This feature may leave PII and PHI locally on a workstaiton. So in its current storage format, it's a no for me. |
pacmano1
left a comment
There was a problem hiding this comment.
Can leave PII/PHI on local workstation. Storage format needs to be changed.
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. |
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>
|
@pacmano1 / @gibson9583, updated to use in-memory storage only. That's tantamount to the rest of the PHI which already resides in memory. |
kpalang
left a comment
There was a problem hiding this comment.
Some suggestions and nitpicks, but overall a nice implementation.
Comments formatted as conventional comments.
| /* | ||
| * 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. | ||
| */ |
There was a problem hiding this comment.
question: Why Mirth Corporation copyright if this is a new class?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Agreed. Will fix.
| var result = ObjectXMLSerializer.getInstance().deserialize(serialized, List.class); | ||
| if (result == null) return List.of(); | ||
|
|
||
| return (List<MessageFilter>) result; |
There was a problem hiding this comment.
question: Would using the ObjectXMLSerializer.getInstance().deserializeList(serialized, MessageFilter.class) call solve the unchecked (and redundant) cast squiggly?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
suggestion: I know you hate future you, but add proper logging and possibly prepopulate log2j.xml with the appropriate knobs for visibility control.
There was a problem hiding this comment.
Ah... fine. I guess I'll eat my broccoli.
| 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(); |
| } | ||
|
|
||
| if (minMessageId != null) { | ||
| if (text.length() > 0) text.append(padding); |
There was a problem hiding this comment.
question: Why the length check instead of !isEmpty()?
There was a problem hiding this comment.
Out-of-date habits.
|
|
||
| if (textSearch != null) { | ||
| if (text.length() > 0) text.append(padding); | ||
| text.append("Text Search: " + textSearch); |
There was a problem hiding this comment.
nitpick: Intellij complains about a concat inside an append().
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
suggestion: A String.formatted() would be more readable here.
There was a problem hiding this comment.
It's what was checked in originally. Moving it to separate append calls.
| if (element.getValue() instanceof Calendar) { | ||
| Calendar date = (Calendar) element.getValue(); |
There was a problem hiding this comment.
suggestion: Pattern matching because pretty.
|
|
||
| if (includeEmptyCriteria | ||
| || (includedMetaDataIds != null && !includedMetaDataIds.isEmpty()) | ||
| || (excludedMetaDataIds != null && !excludedMetaDataIds.isEmpty())) { |
There was a problem hiding this comment.
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);
}There was a problem hiding this comment.
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>
Adds a recent button to the message browser pane. Allows restoration of the last 10 searches on the channel.
Closes #273