diff --git a/web/src/filter.js b/web/src/filter.js index 5e650356a7..7986b0bde7 100644 --- a/web/src/filter.js +++ b/web/src/filter.js @@ -1,7 +1,7 @@ -import Handlebars from "handlebars/runtime"; import _ from "lodash"; import * as resolved_topic from "../shared/src/resolved_topic"; +import render_search_description from "../templates/search_description.hbs"; import * as hash_util from "./hash_util"; import {$t} from "./i18n"; @@ -1030,34 +1030,14 @@ export class Filter { return ""; } - static describe_is_operator(operator) { - const verb = operator.negated ? "exclude " : ""; - const operand = operator.operand; - - switch (operand) { - case "starred": - case "alerted": - case "unread": - return verb + operand + " messages"; - case "mentioned": - return verb + "@-mentions"; - case "dm": - case "private": - return verb + "direct messages"; - case "resolved": - return verb + "topics marked as resolved"; - } - - return "invalid " + operand + " operand for is operator"; - } - // Convert a list of operators to a human-readable description. - static describe_unescaped(operators) { - if (operators.length === 0) { - return "all messages"; - } + static parts_for_describe(operators) { + const parts = []; - let parts = []; + if (operators.length === 0) { + parts.push({type: "plain_text", content: "all messages"}); + return parts; + } if (operators.length >= 2) { const is = (term, expected) => term.operator === expected && !term.negated; @@ -1065,8 +1045,11 @@ export class Filter { if (is(operators[0], "stream") && is(operators[1], "topic")) { const stream = operators[0].operand; const topic = operators[1].operand; - const part = "stream " + stream + " > " + topic; - parts = [part]; + parts.push({ + type: "stream_topic", + stream, + topic, + }); operators = operators.slice(2); } } @@ -1075,7 +1058,12 @@ export class Filter { const operand = elem.operand; const canonicalized_operator = Filter.canonicalize_operator(elem.operator); if (canonicalized_operator === "is") { - return Filter.describe_is_operator(elem); + const verb = elem.negated ? "exclude " : ""; + return { + type: "is_operator", + verb, + operand, + }; } if (canonicalized_operator === "has") { // search_suggestion.get_suggestions takes care that this message will @@ -1089,7 +1077,10 @@ export class Filter { "attachments", ]; if (!valid_has_operands.includes(operand)) { - return "invalid " + operand + " operand for has operator"; + return { + type: "invalid_has", + operand, + }; } } const prefix_for_operator = Filter.operator_to_prefix( @@ -1097,15 +1088,24 @@ export class Filter { elem.negated, ); if (prefix_for_operator !== "") { - return prefix_for_operator + " " + operand; + return { + type: "prefix_for_operator", + prefix_for_operator, + operand, + }; } - return "unknown operator"; + return { + type: "plain_text", + content: "unknown operator", + }; }); - return [...parts, ...more_parts].join(", "); + return [...parts, ...more_parts]; } static search_description_as_html(operators) { - return Handlebars.Utils.escapeExpression(Filter.describe_unescaped(operators)); + return render_search_description({ + parts: Filter.parts_for_describe(operators), + }); } static is_spectator_compatible(ops) { diff --git a/web/templates/search_description.hbs b/web/templates/search_description.hbs new file mode 100644 index 0000000000..044b53e08e --- /dev/null +++ b/web/templates/search_description.hbs @@ -0,0 +1,42 @@ +{{~#each parts ~}} + + {{#if (eq this.type "plain_text")~}} + {{~this.content~}} + {{else if (eq this.type "stream_topic")}} + {{~!-- squash whitespace --~}} + stream {{this.stream}} > {{this.topic}} + {{~!-- squash whitespace --~}} + {{else if (eq this.type "invalid_has")}} + {{~!-- squash whitespace --~}} + invalid {{this.operand}} operand for has operator + {{~!-- squash whitespace --~}} + {{else if (eq this.type "prefix_for_operator")}} + {{~!-- squash whitespace --~}} + {{this.prefix_for_operator}} {{this.operand}} + {{~!-- squash whitespace --~}} + {{else if (eq this.type "is_operator")}} + {{#if (eq this.operand "mentioned")}} + {{~!-- squash whitespace --~}} + {{this.verb}}@-mentions + {{~!-- squash whitespace --~}} + {{else if (or (eq this.operand "starred") (eq this.operand "alerted") (eq this.operand "unread"))}} + {{~!-- squash whitespace --~}} + {{this.verb}}{{this.operand}} messages + {{~!-- squash whitespace --~}} + {{else if (or (eq this.operand "dm") (eq this.operand "private"))}} + {{~!-- squash whitespace --~}} + {{this.verb}}direct messages + {{~!-- squash whitespace --~}} + {{else if (eq this.operand "resolved")}} + {{~!-- squash whitespace --~}} + {{this.verb}}topics marked as resolved + {{~!-- squash whitespace --~}} + {{else}} + {{~!-- squash whitespace --~}} + invalid {{this.operand}} operand for is operator + {{~!-- squash whitespace --~}} + {{~/if~}} + {{~/if~}} + {{~#if (not @last)~}}, {{/if~}} + +{{~/each~}} diff --git a/web/tests/filter.test.js b/web/tests/filter.test.js index bbf5a6a0ef..f83fc3b44f 100644 --- a/web/tests/filter.test.js +++ b/web/tests/filter.test.js @@ -1156,9 +1156,10 @@ test("unparse", () => { assert.deepEqual(Filter.unparse(operators), string); }); -test("describe", () => { +test("describe", ({mock_template}) => { let narrow; let string; + mock_template("search_description.hbs", true, (_data, html) => html); narrow = [{operator: "streams", operand: "public"}]; string = "streams public"; @@ -1186,7 +1187,7 @@ test("describe", () => { {operator: "stream", operand: "devel"}, {operator: "topic", operand: "JS"}, ]; - string = "stream devel > JS"; + string = "stream devel > JS"; assert.equal(Filter.search_description_as_html(narrow), string); narrow = [ diff --git a/web/tests/search_suggestion.test.js b/web/tests/search_suggestion.test.js index 26d77c8731..b8c969704b 100644 --- a/web/tests/search_suggestion.test.js +++ b/web/tests/search_suggestion.test.js @@ -79,7 +79,9 @@ function test(label, f) { }); } -test("basic_get_suggestions", ({override}) => { +test("basic_get_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (data, html) => html); + const query = "fred"; override(narrow_state, "stream", () => "office"); @@ -99,7 +101,9 @@ test("basic_get_suggestions_for_spectator", () => { page_params.is_spectator = false; }); -test("subset_suggestions", () => { +test("subset_suggestions", ({mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + const query = "stream:Denmark topic:Hamlet shakespeare"; const suggestions = get_suggestions(query); @@ -113,7 +117,9 @@ test("subset_suggestions", () => { assert.deepEqual(suggestions.strings, expected); }); -test("dm_suggestions", ({override}) => { +test("dm_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + let query = "is:dm"; let suggestions = get_suggestions(query); let expected = [ @@ -248,7 +254,9 @@ test("dm_suggestions", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("group_suggestions", () => { +test("group_suggestions", ({mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + // Entering a comma in a "dm:" query should immediately // generate suggestions for the next person. let query = "dm:bob@zulip.com,"; @@ -446,7 +454,9 @@ test("empty_query_suggestions", () => { assert.equal(describe("has:attachment"), "Messages that contain attachments"); }); -test("has_suggestions", ({override}) => { +test("has_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + // Checks that category wise suggestions are displayed instead of a single // default suggestion when suggesting `has` operator. let query = "h"; @@ -506,7 +516,9 @@ test("has_suggestions", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("check_is_suggestions", ({override}) => { +test("check_is_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + stream_data.add_sub({stream_id: 44, name: "devel", subscribed: true}); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); override(narrow_state, "stream", () => {}); @@ -587,7 +599,9 @@ test("check_is_suggestions", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("sent_by_me_suggestions", ({override}) => { +test("sent_by_me_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + override(narrow_state, "stream", () => {}); let query = ""; @@ -659,7 +673,8 @@ test("sent_by_me_suggestions", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("topic_suggestions", ({override}) => { +test("topic_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); let suggestions; let expected; @@ -703,7 +718,7 @@ test("topic_suggestions", ({override}) => { return suggestions.lookup_table.get(q).description_html; } assert.equal(describe("te"), "Search for te"); - assert.equal(describe("stream:office topic:team"), "Stream office > team"); + assert.equal(describe("stream:office topic:team"), "Stream office > team"); suggestions = get_suggestions("topic:staplers stream:office"); expected = ["topic:staplers stream:office", "topic:staplers"]; @@ -783,7 +798,9 @@ test("topic_suggestions (limits)", () => { assert_result("z", []); }); -test("whitespace_glitch", ({override}) => { +test("whitespace_glitch", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + const query = "stream:office "; // note trailing space override(stream_topic_history_util, "get_server_history", () => {}); @@ -796,7 +813,9 @@ test("whitespace_glitch", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("stream_completion", ({override}) => { +test("stream_completion", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); stream_data.add_sub({stream_id: 88, name: "dev help", subscribed: true}); @@ -818,7 +837,9 @@ test("stream_completion", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("people_suggestions", ({override}) => { +test("people_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + let query = "te"; override(narrow_state, "stream", () => {}); @@ -920,7 +941,9 @@ test("people_suggestions", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("operator_suggestions", ({override}) => { +test("operator_suggestions", ({override, mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + override(narrow_state, "stream", () => undefined); // Completed operator should return nothing @@ -951,7 +974,9 @@ test("operator_suggestions", ({override}) => { assert.deepEqual(suggestions.strings, expected); }); -test("queries_with_spaces", () => { +test("queries_with_spaces", ({mock_template}) => { + mock_template("search_description.hbs", true, (_data, html) => html); + stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); stream_data.add_sub({stream_id: 88, name: "dev help", subscribed: true});