mirror of
https://github.com/zulip/zulip.git
synced 2025-11-11 17:36:27 +00:00
search_suggestion: Show profile pictures in autocomplete suggestions.
Because the typeahead.js list items are currently just text, a user's full name and avatar should be displayed in `input_pill`. To use `input_pill`, a separate Handlebars partial view was created to provide a mandatory container (`<div class="pill-container">`) for `input_pill` and a flex container (`<div class="search_list_item">`) for vertically aligning the text. The description of each suggestion (i.e `description_html`) is rendered as raw HTML, so every special character (e.g. whitespace) should be HTML-escaped. This enables highlighting the substring in each search suggestion that matches the query. Fixes: #20267
This commit is contained in:
@@ -30,9 +30,9 @@ const search_pill = zrequire("search_pill");
|
|||||||
const {Filter} = zrequire("../js/filter");
|
const {Filter} = zrequire("../js/filter");
|
||||||
|
|
||||||
function test(label, f) {
|
function test(label, f) {
|
||||||
run_test(label, ({override}) => {
|
run_test(label, ({override, mock_template}) => {
|
||||||
page_params.search_pills_enabled = true;
|
page_params.search_pills_enabled = true;
|
||||||
f({override});
|
f({override, mock_template});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +81,23 @@ test("update_button_visibility", () => {
|
|||||||
assert.ok(!$search_button.prop("disabled"));
|
assert.ok(!$search_button.prop("disabled"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("initialize", () => {
|
test("initialize", ({mock_template}) => {
|
||||||
const $search_query_box = $("#search_query");
|
const $search_query_box = $("#search_query");
|
||||||
const $searchbox_form = $("#searchbox_form");
|
const $searchbox_form = $("#searchbox_form");
|
||||||
const $search_button = $(".search_button");
|
const $search_button = $(".search_button");
|
||||||
const $searchbox = $("#searchbox");
|
const $searchbox = $("#searchbox");
|
||||||
|
|
||||||
|
mock_template("search_list_item.hbs", true, (data, html) => {
|
||||||
|
assert.equal(typeof data.description_html, "string");
|
||||||
|
if (data.is_person) {
|
||||||
|
assert.equal(typeof data.user_pill_context.id, "number");
|
||||||
|
assert.equal(typeof data.user_pill_context.display_value, "string");
|
||||||
|
assert.equal(typeof data.user_pill_context.has_image, "boolean");
|
||||||
|
assert.equal(typeof data.user_pill_context.img_src, "string");
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
});
|
||||||
|
|
||||||
$search_query_box[0] = "stub";
|
$search_query_box[0] = "stub";
|
||||||
|
|
||||||
search_pill.get_search_string_for_current_filter = () => "is:starred";
|
search_pill.get_search_string_for_current_filter = () => "is:starred";
|
||||||
@@ -106,7 +117,7 @@ test("initialize", () => {
|
|||||||
[
|
[
|
||||||
"stream:Verona",
|
"stream:Verona",
|
||||||
{
|
{
|
||||||
description_html: "Stream <strong>Ver</strong>ona",
|
description_html: "Stream <strong>Ver</strong>ona",
|
||||||
search_string: "stream:Verona",
|
search_string: "stream:Verona",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -128,16 +139,98 @@ test("initialize", () => {
|
|||||||
assert.equal(source, expected_source_value);
|
assert.equal(source, expected_source_value);
|
||||||
|
|
||||||
/* Test highlighter */
|
/* Test highlighter */
|
||||||
let expected_value = "Search for ver";
|
let expected_value = `<div class="search_list_item">\n Search for ver\n</div>\n`;
|
||||||
assert.equal(opts.highlighter(source[0]), expected_value);
|
assert.equal(opts.highlighter(source[0]), expected_value);
|
||||||
|
|
||||||
expected_value = "Stream <strong>Ver</strong>ona";
|
expected_value = `<div class="search_list_item">\n Stream <strong>Ver</strong>ona\n</div>\n`;
|
||||||
assert.equal(opts.highlighter(source[1]), expected_value);
|
assert.equal(opts.highlighter(source[1]), expected_value);
|
||||||
|
|
||||||
/* Test sorter */
|
/* Test sorter */
|
||||||
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
|
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const search_suggestions = {
|
||||||
|
lookup_table: new Map([
|
||||||
|
[
|
||||||
|
"group-pm-with:zo",
|
||||||
|
{
|
||||||
|
description_html: "group private messages including",
|
||||||
|
is_person: true,
|
||||||
|
search_string: "group-pm-with:user7@zulipdev.com",
|
||||||
|
user_pill_context: {
|
||||||
|
display_value: "<strong>Zo</strong>e",
|
||||||
|
has_image: true,
|
||||||
|
id: 7,
|
||||||
|
img_src:
|
||||||
|
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"pm-with:zo",
|
||||||
|
{
|
||||||
|
description_html: "private messages with",
|
||||||
|
is_person: true,
|
||||||
|
search_string: "pm-with:user7@zulipdev.com",
|
||||||
|
user_pill_context: {
|
||||||
|
display_value: "<strong>Zo</strong>e",
|
||||||
|
has_image: true,
|
||||||
|
id: 7,
|
||||||
|
img_src:
|
||||||
|
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"sender:zo",
|
||||||
|
{
|
||||||
|
description_html: "sent by",
|
||||||
|
is_person: true,
|
||||||
|
search_string: "sender:user7@zulipdev.com",
|
||||||
|
user_pill_context: {
|
||||||
|
display_value: "<strong>Zo</strong>e",
|
||||||
|
has_image: true,
|
||||||
|
id: 7,
|
||||||
|
img_src:
|
||||||
|
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zo",
|
||||||
|
{
|
||||||
|
description_html: "Search for zo",
|
||||||
|
search_string: "zo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
strings: ["zo", "sender:zo", "pm-with:zo", "group-pm-with:zo"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Test source */
|
||||||
|
search_suggestion.get_suggestions = () => search_suggestions;
|
||||||
|
const expected_source_value = search_suggestions.strings;
|
||||||
|
const source = opts.source("zo");
|
||||||
|
assert.equal(source, expected_source_value);
|
||||||
|
|
||||||
|
/* Test highlighter */
|
||||||
|
let expected_value = `<div class="search_list_item">\n Search for zo\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[0]), expected_value);
|
||||||
|
|
||||||
|
expected_value = `<div class="search_list_item">\n sent by\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50" />\n <span class="pill-value"><strong>Zo</strong>e</span>\n <div class="exit">\n <span aria-hidden="true">×</span>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[1]), expected_value);
|
||||||
|
|
||||||
|
expected_value = `<div class="search_list_item">\n private messages with\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50" />\n <span class="pill-value"><strong>Zo</strong>e</span>\n <div class="exit">\n <span aria-hidden="true">×</span>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[2]), expected_value);
|
||||||
|
|
||||||
|
expected_value = `<div class="search_list_item">\n group private messages including\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50" />\n <span class="pill-value"><strong>Zo</strong>e</span>\n <div class="exit">\n <span aria-hidden="true">×</span>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[3]), expected_value);
|
||||||
|
|
||||||
|
/* Test sorter */
|
||||||
|
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let operators;
|
let operators;
|
||||||
let is_blurred;
|
let is_blurred;
|
||||||
|
|||||||
@@ -73,11 +73,22 @@ run_test("update_button_visibility", () => {
|
|||||||
assert.ok(!$search_button.prop("disabled"));
|
assert.ok(!$search_button.prop("disabled"));
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("initialize", () => {
|
run_test("initialize", ({mock_template}) => {
|
||||||
const $search_query_box = $("#search_query");
|
const $search_query_box = $("#search_query");
|
||||||
const $searchbox_form = $("#searchbox_form");
|
const $searchbox_form = $("#searchbox_form");
|
||||||
const $search_button = $(".search_button");
|
const $search_button = $(".search_button");
|
||||||
|
|
||||||
|
mock_template("search_list_item.hbs", true, (data, html) => {
|
||||||
|
assert.equal(typeof data.description_html, "string");
|
||||||
|
if (data.is_person) {
|
||||||
|
assert.equal(typeof data.user_pill_context.id, "number");
|
||||||
|
assert.equal(typeof data.user_pill_context.display_value, "string");
|
||||||
|
assert.equal(typeof data.user_pill_context.has_image, "boolean");
|
||||||
|
assert.equal(typeof data.user_pill_context.img_src, "string");
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
});
|
||||||
|
|
||||||
search_suggestion.max_num_of_search_results = 999;
|
search_suggestion.max_num_of_search_results = 999;
|
||||||
$search_query_box.typeahead = (opts) => {
|
$search_query_box.typeahead = (opts) => {
|
||||||
assert.equal(opts.fixed, true);
|
assert.equal(opts.fixed, true);
|
||||||
@@ -92,7 +103,7 @@ run_test("initialize", () => {
|
|||||||
[
|
[
|
||||||
"stream:Verona",
|
"stream:Verona",
|
||||||
{
|
{
|
||||||
description_html: "Stream <strong>Ver</strong>ona",
|
description_html: "Stream <strong>Ver</strong>ona",
|
||||||
search_string: "stream:Verona",
|
search_string: "stream:Verona",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -114,16 +125,98 @@ run_test("initialize", () => {
|
|||||||
assert.equal(source, expected_source_value);
|
assert.equal(source, expected_source_value);
|
||||||
|
|
||||||
/* Test highlighter */
|
/* Test highlighter */
|
||||||
let expected_value = "Search for ver";
|
let expected_value = `<div class="search_list_item">\n Search for ver\n</div>\n`;
|
||||||
assert.equal(opts.highlighter(source[0]), expected_value);
|
assert.equal(opts.highlighter(source[0]), expected_value);
|
||||||
|
|
||||||
expected_value = "Stream <strong>Ver</strong>ona";
|
expected_value = `<div class="search_list_item">\n Stream <strong>Ver</strong>ona\n</div>\n`;
|
||||||
assert.equal(opts.highlighter(source[1]), expected_value);
|
assert.equal(opts.highlighter(source[1]), expected_value);
|
||||||
|
|
||||||
/* Test sorter */
|
/* Test sorter */
|
||||||
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
|
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const search_suggestions = {
|
||||||
|
lookup_table: new Map([
|
||||||
|
[
|
||||||
|
"group-pm-with:zo",
|
||||||
|
{
|
||||||
|
description_html: "group private messages including",
|
||||||
|
is_person: true,
|
||||||
|
search_string: "group-pm-with:user7@zulipdev.com",
|
||||||
|
user_pill_context: {
|
||||||
|
display_value: "<strong>Zo</strong>e",
|
||||||
|
has_image: true,
|
||||||
|
id: 7,
|
||||||
|
img_src:
|
||||||
|
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"pm-with:zo",
|
||||||
|
{
|
||||||
|
description_html: "private messages with",
|
||||||
|
is_person: true,
|
||||||
|
search_string: "pm-with:user7@zulipdev.com",
|
||||||
|
user_pill_context: {
|
||||||
|
display_value: "<strong>Zo</strong>e",
|
||||||
|
has_image: true,
|
||||||
|
id: 7,
|
||||||
|
img_src:
|
||||||
|
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"sender:zo",
|
||||||
|
{
|
||||||
|
description_html: "sent by",
|
||||||
|
is_person: true,
|
||||||
|
search_string: "sender:user7@zulipdev.com",
|
||||||
|
user_pill_context: {
|
||||||
|
display_value: "<strong>Zo</strong>e",
|
||||||
|
has_image: true,
|
||||||
|
id: 7,
|
||||||
|
img_src:
|
||||||
|
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zo",
|
||||||
|
{
|
||||||
|
description_html: "Search for zo",
|
||||||
|
search_string: "zo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
strings: ["zo", "sender:zo", "pm-with:zo", "group-pm-with:zo"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Test source */
|
||||||
|
search_suggestion.get_suggestions = () => search_suggestions;
|
||||||
|
const expected_source_value = search_suggestions.strings;
|
||||||
|
const source = opts.source("zo");
|
||||||
|
assert.equal(source, expected_source_value);
|
||||||
|
|
||||||
|
/* Test highlighter */
|
||||||
|
let expected_value = `<div class="search_list_item">\n Search for zo\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[0]), expected_value);
|
||||||
|
|
||||||
|
expected_value = `<div class="search_list_item">\n sent by\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50" />\n <span class="pill-value"><strong>Zo</strong>e</span>\n <div class="exit">\n <span aria-hidden="true">×</span>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[1]), expected_value);
|
||||||
|
|
||||||
|
expected_value = `<div class="search_list_item">\n private messages with\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50" />\n <span class="pill-value"><strong>Zo</strong>e</span>\n <div class="exit">\n <span aria-hidden="true">×</span>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[2]), expected_value);
|
||||||
|
|
||||||
|
expected_value = `<div class="search_list_item">\n group private messages including\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50" />\n <span class="pill-value"><strong>Zo</strong>e</span>\n <div class="exit">\n <span aria-hidden="true">×</span>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||||
|
assert.equal(opts.highlighter(source[3]), expected_value);
|
||||||
|
|
||||||
|
/* Test sorter */
|
||||||
|
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let operators;
|
let operators;
|
||||||
let is_blurred;
|
let is_blurred;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const {strict: assert} = require("assert");
|
const {strict: assert} = require("assert");
|
||||||
|
|
||||||
const {mock_esm, with_overrides, zrequire} = require("../zjsunit/namespace");
|
const {mock_esm, zrequire} = require("../zjsunit/namespace");
|
||||||
const {run_test} = require("../zjsunit/test");
|
const {run_test} = require("../zjsunit/test");
|
||||||
const {page_params} = require("../zjsunit/zpage_params");
|
const {page_params} = require("../zjsunit/zpage_params");
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ const jeff = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
const example_avatar_url = "http://example.com/example.png";
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
page_params.is_admin = true;
|
page_params.is_admin = true;
|
||||||
@@ -831,6 +832,7 @@ function people_suggestion_setup() {
|
|||||||
email: "bob@zulip.com",
|
email: "bob@zulip.com",
|
||||||
user_id: 202,
|
user_id: 202,
|
||||||
full_name: "Bob Térry",
|
full_name: "Bob Térry",
|
||||||
|
avatar_url: example_avatar_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
people.add_active_user(bob);
|
people.add_active_user(bob);
|
||||||
@@ -860,16 +862,35 @@ test("people_suggestions", ({override}) => {
|
|||||||
|
|
||||||
assert.deepEqual(suggestions.strings, expected);
|
assert.deepEqual(suggestions.strings, expected);
|
||||||
|
|
||||||
const describe = (q) => suggestions.lookup_table.get(q).description_html;
|
const is_person = (q) => suggestions.lookup_table.get(q).is_person;
|
||||||
|
assert.equal(is_person("pm-with:ted@zulip.com"), true);
|
||||||
|
assert.equal(is_person("sender:ted@zulip.com"), true);
|
||||||
|
assert.equal(is_person("group-pm-with:ted@zulip.com"), true);
|
||||||
|
|
||||||
assert.equal(
|
const has_image = (q) => suggestions.lookup_table.get(q).user_pill_context.has_image;
|
||||||
describe("pm-with:ted@zulip.com"),
|
assert.equal(has_image("pm-with:bob@zulip.com"), true);
|
||||||
"Private messages with <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
|
assert.equal(has_image("sender:bob@zulip.com"), true);
|
||||||
);
|
assert.equal(has_image("group-pm-with:bob@zulip.com"), true);
|
||||||
assert.equal(
|
|
||||||
describe("sender:ted@zulip.com"),
|
const describe = (q) => suggestions.lookup_table.get(q).description_html;
|
||||||
"Sent by <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
|
assert.equal(describe("pm-with:ted@zulip.com"), "Private messages with");
|
||||||
);
|
assert.equal(describe("sender:ted@zulip.com"), "Sent by");
|
||||||
|
assert.equal(describe("group-pm-with:ted@zulip.com"), "Group private messages including");
|
||||||
|
|
||||||
|
let expectedString = "<strong>Te</strong>d Smith";
|
||||||
|
|
||||||
|
const get_full_name = (q) =>
|
||||||
|
suggestions.lookup_table.get(q).user_pill_context.display_value.string;
|
||||||
|
assert.equal(get_full_name("sender:ted@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_full_name("pm-with:ted@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_full_name("group-pm-with:ted@zulip.com"), expectedString);
|
||||||
|
|
||||||
|
expectedString = `${example_avatar_url}?s=50`;
|
||||||
|
|
||||||
|
const get_avatar_url = (q) => suggestions.lookup_table.get(q).user_pill_context.img_src;
|
||||||
|
assert.equal(get_avatar_url("pm-with:bob@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_avatar_url("group-pm-with:bob@zulip.com"), expectedString);
|
||||||
|
|
||||||
suggestions = get_suggestions("", "Ted "); // note space
|
suggestions = get_suggestions("", "Ted "); // note space
|
||||||
expected = [
|
expected = [
|
||||||
@@ -906,39 +927,6 @@ test("people_suggestions", ({override}) => {
|
|||||||
assert.deepEqual(suggestions.strings, expected);
|
assert.deepEqual(suggestions.strings, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("people_suggestion (Admin only email visibility)", ({override}) => {
|
|
||||||
/* Suggestions when realm_email_address_visibility is set to admin
|
|
||||||
only */
|
|
||||||
override(narrow_state, "stream", noop);
|
|
||||||
people_suggestion_setup();
|
|
||||||
|
|
||||||
const query = "te";
|
|
||||||
const suggestions = with_overrides(({override}) => {
|
|
||||||
override(page_params, "is_admin", false);
|
|
||||||
return get_suggestions("", query);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
"te",
|
|
||||||
"sender:bob@zulip.com",
|
|
||||||
"sender:ted@zulip.com",
|
|
||||||
"pm-with:bob@zulip.com", // bob térry
|
|
||||||
"pm-with:ted@zulip.com",
|
|
||||||
"group-pm-with:bob@zulip.com",
|
|
||||||
"group-pm-with:ted@zulip.com",
|
|
||||||
];
|
|
||||||
|
|
||||||
assert.deepEqual(suggestions.strings, expected);
|
|
||||||
|
|
||||||
const describe = (q) => suggestions.lookup_table.get(q).description_html;
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
describe("pm-with:ted@zulip.com"),
|
|
||||||
"Private messages with <strong>Te</strong>d Smith",
|
|
||||||
);
|
|
||||||
assert.equal(describe("sender:ted@zulip.com"), "Sent by <strong>Te</strong>d Smith");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("operator_suggestions", ({override}) => {
|
test("operator_suggestions", ({override}) => {
|
||||||
override(narrow_state, "stream", () => undefined);
|
override(narrow_state, "stream", () => undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const {strict: assert} = require("assert");
|
const {strict: assert} = require("assert");
|
||||||
|
|
||||||
const {mock_esm, with_overrides, zrequire} = require("../zjsunit/namespace");
|
const {mock_esm, zrequire} = require("../zjsunit/namespace");
|
||||||
const {run_test} = require("../zjsunit/test");
|
const {run_test} = require("../zjsunit/test");
|
||||||
const {page_params} = require("../zjsunit/zpage_params");
|
const {page_params} = require("../zjsunit/zpage_params");
|
||||||
|
|
||||||
@@ -50,6 +50,8 @@ const jeff = {
|
|||||||
full_name: "Jeff Zoolipson",
|
full_name: "Jeff Zoolipson",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const example_avatar_url = "http://example.com/example.png";
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
page_params.is_admin = true;
|
page_params.is_admin = true;
|
||||||
page_params.search_pills_enabled = false;
|
page_params.search_pills_enabled = false;
|
||||||
@@ -805,6 +807,7 @@ test("people_suggestions", ({override}) => {
|
|||||||
email: "bob@zulip.com",
|
email: "bob@zulip.com",
|
||||||
user_id: 202,
|
user_id: 202,
|
||||||
full_name: "Bob Térry",
|
full_name: "Bob Térry",
|
||||||
|
avatar_url: example_avatar_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
const alice = {
|
const alice = {
|
||||||
@@ -829,17 +832,45 @@ test("people_suggestions", ({override}) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
assert.deepEqual(suggestions.strings, expected);
|
assert.deepEqual(suggestions.strings, expected);
|
||||||
|
|
||||||
|
function is_person(q) {
|
||||||
|
return suggestions.lookup_table.get(q).is_person;
|
||||||
|
}
|
||||||
|
assert.equal(is_person("pm-with:ted@zulip.com"), true);
|
||||||
|
assert.equal(is_person("sender:ted@zulip.com"), true);
|
||||||
|
assert.equal(is_person("group-pm-with:ted@zulip.com"), true);
|
||||||
|
|
||||||
|
function has_image(q) {
|
||||||
|
return suggestions.lookup_table.get(q).user_pill_context.has_image;
|
||||||
|
}
|
||||||
|
assert.equal(has_image("pm-with:bob@zulip.com"), true);
|
||||||
|
assert.equal(has_image("sender:bob@zulip.com"), true);
|
||||||
|
assert.equal(has_image("group-pm-with:bob@zulip.com"), true);
|
||||||
|
|
||||||
function describe(q) {
|
function describe(q) {
|
||||||
return suggestions.lookup_table.get(q).description_html;
|
return suggestions.lookup_table.get(q).description_html;
|
||||||
}
|
}
|
||||||
assert.equal(
|
assert.equal(describe("pm-with:ted@zulip.com"), "Private messages with");
|
||||||
describe("pm-with:ted@zulip.com"),
|
assert.equal(describe("sender:ted@zulip.com"), "Sent by");
|
||||||
"Private messages with <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
|
assert.equal(describe("group-pm-with:ted@zulip.com"), "Group private messages including");
|
||||||
);
|
|
||||||
assert.equal(
|
let expectedString = "<strong>Te</strong>d Smith";
|
||||||
describe("sender:ted@zulip.com"),
|
|
||||||
"Sent by <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
|
function get_full_name(q) {
|
||||||
);
|
return suggestions.lookup_table.get(q).user_pill_context.display_value.string;
|
||||||
|
}
|
||||||
|
assert.equal(get_full_name("sender:ted@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_full_name("pm-with:ted@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_full_name("group-pm-with:ted@zulip.com"), expectedString);
|
||||||
|
|
||||||
|
expectedString = example_avatar_url + "?s=50";
|
||||||
|
|
||||||
|
function get_avatar_url(q) {
|
||||||
|
return suggestions.lookup_table.get(q).user_pill_context.img_src;
|
||||||
|
}
|
||||||
|
assert.equal(get_avatar_url("pm-with:bob@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString);
|
||||||
|
assert.equal(get_avatar_url("group-pm-with:bob@zulip.com"), expectedString);
|
||||||
|
|
||||||
suggestions = get_suggestions("", "Ted "); // note space
|
suggestions = get_suggestions("", "Ted "); // note space
|
||||||
|
|
||||||
@@ -926,59 +957,3 @@ test("queries_with_spaces", () => {
|
|||||||
expected = ["stream:offi", "stream:office"];
|
expected = ["stream:offi", "stream:office"];
|
||||||
assert.deepEqual(suggestions.strings, expected);
|
assert.deepEqual(suggestions.strings, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
function people_suggestion_setup() {
|
|
||||||
const ted = {
|
|
||||||
email: "ted@zulip.com",
|
|
||||||
user_id: 201,
|
|
||||||
full_name: "Ted Smith",
|
|
||||||
};
|
|
||||||
people.add_active_user(ted);
|
|
||||||
|
|
||||||
const bob = {
|
|
||||||
email: "bob@zulip.com",
|
|
||||||
user_id: 202,
|
|
||||||
full_name: "Bob Térry",
|
|
||||||
};
|
|
||||||
|
|
||||||
people.add_active_user(bob);
|
|
||||||
const alice = {
|
|
||||||
email: "alice@zulip.com",
|
|
||||||
user_id: 203,
|
|
||||||
full_name: "Alice Ignore",
|
|
||||||
};
|
|
||||||
people.add_active_user(alice);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("people_suggestion (Admin only email visibility)", ({override}) => {
|
|
||||||
/* Suggestions when realm_email_address_visibility is set to admin
|
|
||||||
only */
|
|
||||||
override(narrow_state, "stream", () => {});
|
|
||||||
people_suggestion_setup();
|
|
||||||
|
|
||||||
const query = "te";
|
|
||||||
const suggestions = with_overrides(({override}) => {
|
|
||||||
override(page_params, "is_admin", false);
|
|
||||||
return get_suggestions("", query);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
"te",
|
|
||||||
"sender:bob@zulip.com",
|
|
||||||
"sender:ted@zulip.com",
|
|
||||||
"pm-with:bob@zulip.com", // bob térry
|
|
||||||
"pm-with:ted@zulip.com",
|
|
||||||
"group-pm-with:bob@zulip.com",
|
|
||||||
"group-pm-with:ted@zulip.com",
|
|
||||||
];
|
|
||||||
|
|
||||||
assert.deepEqual(suggestions.strings, expected);
|
|
||||||
|
|
||||||
const describe = (q) => suggestions.lookup_table.get(q).description_html;
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
describe("pm-with:ted@zulip.com"),
|
|
||||||
"Private messages with <strong>Te</strong>d Smith",
|
|
||||||
);
|
|
||||||
assert.equal(describe("sender:ted@zulip.com"), "Sent by <strong>Te</strong>d Smith");
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
|
|
||||||
|
import render_search_list_item from "../templates/search_list_item.hbs";
|
||||||
|
|
||||||
import {Filter} from "./filter";
|
import {Filter} from "./filter";
|
||||||
import * as message_view_header from "./message_view_header";
|
import * as message_view_header from "./message_view_header";
|
||||||
import * as narrow from "./narrow";
|
import * as narrow from "./narrow";
|
||||||
@@ -94,7 +96,7 @@ export function initialize() {
|
|||||||
naturalSearch: true,
|
naturalSearch: true,
|
||||||
highlighter(item) {
|
highlighter(item) {
|
||||||
const obj = search_map.get(item);
|
const obj = search_map.get(item);
|
||||||
return obj.description_html;
|
return render_search_list_item(obj);
|
||||||
},
|
},
|
||||||
matcher() {
|
matcher() {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import * as huddle_data from "./huddle_data";
|
|||||||
import * as narrow_state from "./narrow_state";
|
import * as narrow_state from "./narrow_state";
|
||||||
import {page_params} from "./page_params";
|
import {page_params} from "./page_params";
|
||||||
import * as people from "./people";
|
import * as people from "./people";
|
||||||
import * as settings_data from "./settings_data";
|
|
||||||
import * as stream_data from "./stream_data";
|
import * as stream_data from "./stream_data";
|
||||||
import * as stream_topic_history from "./stream_topic_history";
|
import * as stream_topic_history from "./stream_topic_history";
|
||||||
import * as stream_topic_history_util from "./stream_topic_history_util";
|
import * as stream_topic_history_util from "./stream_topic_history_util";
|
||||||
@@ -22,15 +21,22 @@ function make_person_highlighter(query) {
|
|||||||
const highlight_query = typeahead_helper.make_query_highlighter(query);
|
const highlight_query = typeahead_helper.make_query_highlighter(query);
|
||||||
|
|
||||||
return function (person) {
|
return function (person) {
|
||||||
if (settings_data.show_email()) {
|
|
||||||
return (
|
|
||||||
highlight_query(person.full_name) + " <" + highlight_query(person.email) + ">"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return highlight_query(person.full_name);
|
return highlight_query(person.full_name);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function highlight_person(person, highlighter) {
|
||||||
|
const avatar_url = people.small_avatar_url_for_person(person);
|
||||||
|
const highlighted_name = highlighter(person);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: person.user_id,
|
||||||
|
display_value: new Handlebars.SafeString(highlighted_name),
|
||||||
|
has_image: true,
|
||||||
|
img_src: avatar_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function match_criteria(operators, criteria) {
|
function match_criteria(operators, criteria) {
|
||||||
const filter = new Filter(operators);
|
const filter = new Filter(operators);
|
||||||
return criteria.some((cr) => {
|
return criteria.some((cr) => {
|
||||||
@@ -115,7 +121,7 @@ function get_stream_suggestions(last, operators) {
|
|||||||
const prefix = "stream";
|
const prefix = "stream";
|
||||||
const highlighted_stream = highlight_query(regex, stream);
|
const highlighted_stream = highlight_query(regex, stream);
|
||||||
const verb = last.negated ? "exclude " : "";
|
const verb = last.negated ? "exclude " : "";
|
||||||
const description_html = verb + prefix + " " + highlighted_stream;
|
const description_html = verb + prefix + " " + highlighted_stream;
|
||||||
const term = {
|
const term = {
|
||||||
operator: "stream",
|
operator: "stream",
|
||||||
operand: stream,
|
operand: stream,
|
||||||
@@ -178,15 +184,24 @@ function get_group_suggestions(last, operators) {
|
|||||||
operand: all_but_last_part + "," + person.email,
|
operand: all_but_last_part + "," + person.email,
|
||||||
negated,
|
negated,
|
||||||
};
|
};
|
||||||
const name = person_highlighter(person);
|
|
||||||
|
// Note that description_html won't contain the user's
|
||||||
|
// identity; that instead will be rendered in the separate
|
||||||
|
// user pill.
|
||||||
const description_html =
|
const description_html =
|
||||||
prefix + " " + Handlebars.Utils.escapeExpression(all_but_last_part) + "," + name;
|
prefix + Handlebars.Utils.escapeExpression(" " + all_but_last_part + ",");
|
||||||
|
|
||||||
let terms = [term];
|
let terms = [term];
|
||||||
if (negated) {
|
if (negated) {
|
||||||
terms = [{operator: "is", operand: "private"}, term];
|
terms = [{operator: "is", operand: "private"}, term];
|
||||||
}
|
}
|
||||||
const search_string = Filter.unparse(terms);
|
|
||||||
return {description_html, search_string};
|
return {
|
||||||
|
description_html,
|
||||||
|
search_string: Filter.unparse(terms),
|
||||||
|
is_person: true,
|
||||||
|
user_pill_context: highlight_person(person, person_highlighter),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return suggestions;
|
return suggestions;
|
||||||
@@ -254,8 +269,6 @@ function get_person_suggestions(people_getter, last, operators, autocomplete_ope
|
|||||||
const person_highlighter = make_person_highlighter(query);
|
const person_highlighter = make_person_highlighter(query);
|
||||||
|
|
||||||
const objs = persons.map((person) => {
|
const objs = persons.map((person) => {
|
||||||
const name = person_highlighter(person);
|
|
||||||
const description_html = prefix + " " + name;
|
|
||||||
const terms = [
|
const terms = [
|
||||||
{
|
{
|
||||||
operator: autocomplete_operator,
|
operator: autocomplete_operator,
|
||||||
@@ -268,8 +281,13 @@ function get_person_suggestions(people_getter, last, operators, autocomplete_ope
|
|||||||
// because we assume the user still wants to narrow to PMs
|
// because we assume the user still wants to narrow to PMs
|
||||||
terms.unshift({operator: "is", operand: "private"});
|
terms.unshift({operator: "is", operand: "private"});
|
||||||
}
|
}
|
||||||
const search_string = Filter.unparse(terms);
|
|
||||||
return {description_html, search_string};
|
return {
|
||||||
|
description_html: prefix,
|
||||||
|
search_string: Filter.unparse(terms),
|
||||||
|
is_person: true,
|
||||||
|
user_pill_context: highlight_person(person, person_highlighter),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return objs;
|
return objs;
|
||||||
|
|||||||
@@ -73,6 +73,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.pill-container-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.exit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
|
|||||||
@@ -751,6 +751,16 @@ strong {
|
|||||||
hsl(200, 100%, 35%)
|
hsl(200, 100%, 35%)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search_list_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_list_item .pill-container {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* styles defined for user_circle here only deal with positioning of user_presence_circle
|
/* styles defined for user_circle here only deal with positioning of user_presence_circle
|
||||||
in typeahead list in order to ensure they are rendered correctly in in all screen sizes.
|
in typeahead list in order to ensure they are rendered correctly in in all screen sizes.
|
||||||
Most of the style rules related to color, gradient etc. which are generally common throughout
|
Most of the style rules related to color, gradient etc. which are generally common throughout
|
||||||
|
|||||||
8
static/templates/search_list_item.hbs
Normal file
8
static/templates/search_list_item.hbs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="search_list_item">
|
||||||
|
{{{ description_html }}}
|
||||||
|
{{#if is_person}}
|
||||||
|
<span class="pill-container pill-container-btn">
|
||||||
|
{{> input_pill user_pill_context}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user