search: Create a template for search descriptions.

This commit is contained in:
evykassirer 2023-06-29 19:45:08 -04:00 committed by Tim Abbott
parent 6346110b56
commit 8f5305a4ce
4 changed files with 119 additions and 51 deletions

View File

@ -1,7 +1,7 @@
import Handlebars from "handlebars/runtime";
import _ from "lodash"; import _ from "lodash";
import * as resolved_topic from "../shared/src/resolved_topic"; 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 * as hash_util from "./hash_util";
import {$t} from "./i18n"; import {$t} from "./i18n";
@ -1030,34 +1030,14 @@ export class Filter {
return ""; 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. // Convert a list of operators to a human-readable description.
static describe_unescaped(operators) { static parts_for_describe(operators) {
if (operators.length === 0) { const parts = [];
return "all messages";
}
let parts = []; if (operators.length === 0) {
parts.push({type: "plain_text", content: "all messages"});
return parts;
}
if (operators.length >= 2) { if (operators.length >= 2) {
const is = (term, expected) => term.operator === expected && !term.negated; 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")) { if (is(operators[0], "stream") && is(operators[1], "topic")) {
const stream = operators[0].operand; const stream = operators[0].operand;
const topic = operators[1].operand; const topic = operators[1].operand;
const part = "stream " + stream + " > " + topic; parts.push({
parts = [part]; type: "stream_topic",
stream,
topic,
});
operators = operators.slice(2); operators = operators.slice(2);
} }
} }
@ -1075,7 +1058,12 @@ export class Filter {
const operand = elem.operand; const operand = elem.operand;
const canonicalized_operator = Filter.canonicalize_operator(elem.operator); const canonicalized_operator = Filter.canonicalize_operator(elem.operator);
if (canonicalized_operator === "is") { 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") { if (canonicalized_operator === "has") {
// search_suggestion.get_suggestions takes care that this message will // search_suggestion.get_suggestions takes care that this message will
@ -1089,7 +1077,10 @@ export class Filter {
"attachments", "attachments",
]; ];
if (!valid_has_operands.includes(operand)) { 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( const prefix_for_operator = Filter.operator_to_prefix(
@ -1097,15 +1088,24 @@ export class Filter {
elem.negated, elem.negated,
); );
if (prefix_for_operator !== "") { 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) { 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) { static is_spectator_compatible(ops) {

View File

@ -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~}}

View File

@ -1156,9 +1156,10 @@ test("unparse", () => {
assert.deepEqual(Filter.unparse(operators), string); assert.deepEqual(Filter.unparse(operators), string);
}); });
test("describe", () => { test("describe", ({mock_template}) => {
let narrow; let narrow;
let string; let string;
mock_template("search_description.hbs", true, (_data, html) => html);
narrow = [{operator: "streams", operand: "public"}]; narrow = [{operator: "streams", operand: "public"}];
string = "streams public"; string = "streams public";
@ -1186,7 +1187,7 @@ test("describe", () => {
{operator: "stream", operand: "devel"}, {operator: "stream", operand: "devel"},
{operator: "topic", operand: "JS"}, {operator: "topic", operand: "JS"},
]; ];
string = "stream devel > JS"; string = "stream devel > JS";
assert.equal(Filter.search_description_as_html(narrow), string); assert.equal(Filter.search_description_as_html(narrow), string);
narrow = [ narrow = [

View File

@ -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"; const query = "fred";
override(narrow_state, "stream", () => "office"); override(narrow_state, "stream", () => "office");
@ -99,7 +101,9 @@ test("basic_get_suggestions_for_spectator", () => {
page_params.is_spectator = false; 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 query = "stream:Denmark topic:Hamlet shakespeare";
const suggestions = get_suggestions(query); const suggestions = get_suggestions(query);
@ -113,7 +117,9 @@ test("subset_suggestions", () => {
assert.deepEqual(suggestions.strings, expected); 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 query = "is:dm";
let suggestions = get_suggestions(query); let suggestions = get_suggestions(query);
let expected = [ let expected = [
@ -248,7 +254,9 @@ test("dm_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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 // Entering a comma in a "dm:" query should immediately
// generate suggestions for the next person. // generate suggestions for the next person.
let query = "dm:bob@zulip.com,"; let query = "dm:bob@zulip.com,";
@ -446,7 +454,9 @@ test("empty_query_suggestions", () => {
assert.equal(describe("has:attachment"), "Messages that contain attachments"); 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 // Checks that category wise suggestions are displayed instead of a single
// default suggestion when suggesting `has` operator. // default suggestion when suggesting `has` operator.
let query = "h"; let query = "h";
@ -506,7 +516,9 @@ test("has_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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: 44, name: "devel", subscribed: true});
stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true});
override(narrow_state, "stream", () => {}); override(narrow_state, "stream", () => {});
@ -587,7 +599,9 @@ test("check_is_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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", () => {}); override(narrow_state, "stream", () => {});
let query = ""; let query = "";
@ -659,7 +673,8 @@ test("sent_by_me_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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 suggestions;
let expected; let expected;
@ -703,7 +718,7 @@ test("topic_suggestions", ({override}) => {
return suggestions.lookup_table.get(q).description_html; return suggestions.lookup_table.get(q).description_html;
} }
assert.equal(describe("te"), "Search for te"); 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"); suggestions = get_suggestions("topic:staplers stream:office");
expected = ["topic:staplers stream:office", "topic:staplers"]; expected = ["topic:staplers stream:office", "topic:staplers"];
@ -783,7 +798,9 @@ test("topic_suggestions (limits)", () => {
assert_result("z", []); 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 const query = "stream:office "; // note trailing space
override(stream_topic_history_util, "get_server_history", () => {}); override(stream_topic_history_util, "get_server_history", () => {});
@ -796,7 +813,9 @@ test("whitespace_glitch", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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: 77, name: "office", subscribed: true});
stream_data.add_sub({stream_id: 88, name: "dev help", 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); 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"; let query = "te";
override(narrow_state, "stream", () => {}); override(narrow_state, "stream", () => {});
@ -920,7 +941,9 @@ test("people_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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); override(narrow_state, "stream", () => undefined);
// Completed operator should return nothing // Completed operator should return nothing
@ -951,7 +974,9 @@ test("operator_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); 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: 77, name: "office", subscribed: true});
stream_data.add_sub({stream_id: 88, name: "dev help", subscribed: true}); stream_data.add_sub({stream_id: 88, name: "dev help", subscribed: true});