unresolved: Add support for searching for unresolved topics.

Fixes: #31725.
This commit is contained in:
Maneesh Shukla
2025-01-15 03:37:10 +05:30
committed by Tim Abbott
parent 3f88fe5903
commit d8f609d088
6 changed files with 66 additions and 8 deletions

View File

@@ -750,6 +750,20 @@ export class Filter {
const operand = term.operand; const operand = term.operand;
const canonicalized_operator = Filter.canonicalize_operator(term.operator); const canonicalized_operator = Filter.canonicalize_operator(term.operator);
if (canonicalized_operator === "is") { if (canonicalized_operator === "is") {
// Some operands have their own negative words, like
// unresolved, rather than the default "exclude " prefix.
const custom_negated_operand_phrases: Record<string, string> = {
resolved: "unresolved",
};
const negated_phrase = custom_negated_operand_phrases[operand];
if (term.negated && negated_phrase !== undefined) {
return {
type: "is_operator",
verb: "",
operand: negated_phrase,
};
}
const verb = term.negated ? "exclude " : ""; const verb = term.negated ? "exclude " : "";
return { return {
type: "is_operator", type: "is_operator",

View File

@@ -589,12 +589,29 @@ function get_special_filter_suggestions(
// Negating suggestions on is_search_operand_negated is required for // Negating suggestions on is_search_operand_negated is required for
// suggesting negated terms. // suggesting negated terms.
if (last.negated === true || is_search_operand_negated) { if (last.negated === true || is_search_operand_negated) {
suggestions = suggestions.map((suggestion) => ({ suggestions = suggestions
search_string: "-" + suggestion.search_string, .map((suggestion) => {
description_html: "exclude " + suggestion.description_html, // If the search_string is "is:resolved", we want to suggest "Unresolved topics"
incompatible_patterns: suggestion.incompatible_patterns, // instead of "Exclude resolved topics".
is_people: false, if (suggestion.search_string === "is:resolved") {
})); return {
...suggestion,
search_string: "-" + suggestion.search_string,
description_html: "unresolved topics",
};
} else if (suggestion.search_string === "-is:resolved") {
return null;
}
return {
...suggestion,
search_string: "-" + suggestion.search_string,
description_html: "exclude " + suggestion.description_html,
};
})
.filter(
(suggestion): suggestion is SuggestionAndIncompatiblePatterns =>
suggestion !== null,
);
} }
const last_string = Filter.unparse([last]).toLowerCase(); const last_string = Filter.unparse([last]).toLowerCase();
@@ -713,6 +730,17 @@ function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Sugge
{operator: "dm-including"}, {operator: "dm-including"},
], ],
}, },
{
search_string: "-is:resolved",
description_html: "unresolved topics",
is_people: false,
incompatible_patterns: [
{operator: "is", operand: "resolved"},
{operator: "is", operand: "dm"},
{operator: "dm"},
{operator: "dm-including"},
],
},
]; ];
const special_filtered_suggestions = get_special_filter_suggestions(last, terms, suggestions); const special_filtered_suggestions = get_special_filter_suggestions(last, terms, suggestions);
// Suggest "is:dm" to anyone with "is:private" in their muscle memory // Suggest "is:dm" to anyone with "is:private" in their muscle memory

View File

@@ -50,6 +50,10 @@
{{~!-- squash whitespace --~}} {{~!-- squash whitespace --~}}
{{this.verb}}followed topics {{this.verb}}followed topics
{{~!-- squash whitespace --~}} {{~!-- squash whitespace --~}}
{{else if (eq this.operand "unresolved")}}
{{~!-- squash whitespace --~}}
{{this.verb}}unresolved topics
{{~!-- squash whitespace --~}}
{{else}} {{else}}
{{~!-- squash whitespace --~}} {{~!-- squash whitespace --~}}
invalid {{this.operand}} operand for is operator invalid {{this.operand}} operand for is operator

View File

@@ -133,6 +133,12 @@
{{t 'Narrow to messages in resolved topics.'}} {{t 'Narrow to messages in resolved topics.'}}
</td> </td>
</tr> </tr>
<tr>
<td class="operator">-is:resolved</td>
<td class="definition">
{{t 'Narrow to messages in unresolved topics.'}}
</td>
</tr>
<tr> <tr>
<td class="operator">is:followed</td> <td class="operator">is:followed</td>
<td class="definition"> <td class="definition">

View File

@@ -1563,13 +1563,18 @@ test("describe", ({mock_template, override}) => {
assert.equal(Filter.search_description_as_html(narrow, false), string); assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "resolved"}]; narrow = [{operator: "is", operand: "resolved"}];
string = "topics marked as resolved"; string = "resolved topics";
assert.equal(Filter.search_description_as_html(narrow, false), string); assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "followed"}]; narrow = [{operator: "is", operand: "followed"}];
string = "followed topics"; string = "followed topics";
assert.equal(Filter.search_description_as_html(narrow, false), string); assert.equal(Filter.search_description_as_html(narrow, false), string);
// operands with their own negative words, like resolved.
narrow = [{operator: "is", operand: "resolved", negated: true}];
string = "unresolved topics";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "something_we_do_not_support"}]; narrow = [{operator: "is", operand: "something_we_do_not_support"}];
string = "invalid something_we_do_not_support operand for is operator"; string = "invalid something_we_do_not_support operand for is operator";
assert.equal(Filter.search_description_as_html(narrow, false), string); assert.equal(Filter.search_description_as_html(narrow, false), string);

View File

@@ -377,6 +377,7 @@ test("empty_query_suggestions", () => {
"is:alerted", "is:alerted",
"is:unread", "is:unread",
"is:resolved", "is:resolved",
"-is:resolved",
"sender:myself@zulip.com", "sender:myself@zulip.com",
`channel:${devel_id}`, `channel:${devel_id}`,
`channel:${office_id}`, `channel:${office_id}`,
@@ -518,7 +519,7 @@ test("check_is_suggestions", ({override, mock_template}) => {
assert.equal(describe("-is:mentioned"), "Exclude @-mentions"); assert.equal(describe("-is:mentioned"), "Exclude @-mentions");
assert.equal(describe("-is:alerted"), "Exclude alerted messages"); assert.equal(describe("-is:alerted"), "Exclude alerted messages");
assert.equal(describe("-is:unread"), "Exclude unread messages"); assert.equal(describe("-is:unread"), "Exclude unread messages");
assert.equal(describe("-is:resolved"), "Exclude topics marked as resolved"); assert.equal(describe("-is:resolved"), "Unresolved topics");
assert.equal(describe("-is:followed"), "Exclude followed topics"); assert.equal(describe("-is:followed"), "Exclude followed topics");
// operand suggestions follow. // operand suggestions follow.