mirror of https://github.com/zulip/zulip.git
search pills: Validate pills with shake animation when invalid.
This commit is contained in:
parent
fc95987b8d
commit
c3786223e5
|
@ -209,39 +209,6 @@ async function search_silent_user(page: Page, str: string, item: string): Promis
|
||||||
await expect_home(page);
|
await expect_home(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expect_non_existing_user(page: Page): Promise<void> {
|
|
||||||
await common.get_current_msg_list_id(page, true);
|
|
||||||
await page.waitForSelector(".empty_feed_notice", {visible: true});
|
|
||||||
const expected_message = "This user does not exist!";
|
|
||||||
assert.strictEqual(
|
|
||||||
await common.get_text_from_selector(page, ".empty_feed_notice"),
|
|
||||||
expected_message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expect_non_existing_users(page: Page): Promise<void> {
|
|
||||||
await common.get_current_msg_list_id(page, true);
|
|
||||||
await page.waitForSelector(".empty_feed_notice", {visible: true});
|
|
||||||
const expected_message = "One or more of these users do not exist!";
|
|
||||||
assert.strictEqual(
|
|
||||||
await common.get_text_from_selector(page, ".empty_feed_notice"),
|
|
||||||
expected_message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function search_non_existing_user(page: Page, str: string, item: string): Promise<void> {
|
|
||||||
await page.click(".search_icon");
|
|
||||||
await page.waitForSelector(".navbar-search.expanded", {visible: true});
|
|
||||||
// Close the "in: home" pill
|
|
||||||
await page.click(".navbar-search .pill-close-button");
|
|
||||||
await common.select_item_via_typeahead(page, "#search_query", str, item);
|
|
||||||
// Enter to trigger search
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
await expect_non_existing_user(page);
|
|
||||||
await un_narrow(page);
|
|
||||||
await expect_home(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function search_tests(page: Page): Promise<void> {
|
async function search_tests(page: Page): Promise<void> {
|
||||||
await search_and_check(
|
await search_and_check(
|
||||||
page,
|
page,
|
||||||
|
@ -292,24 +259,6 @@ async function search_tests(page: Page): Promise<void> {
|
||||||
);
|
);
|
||||||
|
|
||||||
await search_silent_user(page, "sender:emailgateway@zulip.com", "");
|
await search_silent_user(page, "sender:emailgateway@zulip.com", "");
|
||||||
|
|
||||||
await search_non_existing_user(page, "sender:dummyuser@zulip.com", "");
|
|
||||||
|
|
||||||
await search_and_check(
|
|
||||||
page,
|
|
||||||
"dm:dummyuser@zulip.com",
|
|
||||||
"",
|
|
||||||
expect_non_existing_user,
|
|
||||||
"Invalid user - Zulip Dev - Zulip",
|
|
||||||
);
|
|
||||||
|
|
||||||
await search_and_check(
|
|
||||||
page,
|
|
||||||
"dm:dummyuser@zulip.com,dummyuser2@zulip.com",
|
|
||||||
"",
|
|
||||||
expect_non_existing_users,
|
|
||||||
"Invalid users - Zulip Dev - Zulip",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expect_all_direct_messages(page: Page): Promise<void> {
|
async function expect_all_direct_messages(page: Page): Promise<void> {
|
||||||
|
|
|
@ -210,7 +210,12 @@ export function initialize({on_narrow_search}: {on_narrow_search: OnNarrowSearch
|
||||||
if (e.key === "Escape" && $search_query_box.is(":focus")) {
|
if (e.key === "Escape" && $search_query_box.is(":focus")) {
|
||||||
exit_search({keep_search_narrow_open: false});
|
exit_search({keep_search_narrow_open: false});
|
||||||
} else if (keydown_util.is_enter_event(e) && $search_query_box.is(":focus")) {
|
} else if (keydown_util.is_enter_event(e) && $search_query_box.is(":focus")) {
|
||||||
narrow_or_search_for_term({on_narrow_search});
|
const text_terms = Filter.parse(get_search_bar_text());
|
||||||
|
if (text_terms.every((term) => Filter.is_valid_search_term(term))) {
|
||||||
|
narrow_or_search_for_term({on_narrow_search});
|
||||||
|
} else {
|
||||||
|
$("#search_query").addClass("shake");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -287,7 +292,11 @@ function reset_searchbox(): void {
|
||||||
assert(search_pill_widget !== null);
|
assert(search_pill_widget !== null);
|
||||||
search_pill_widget.clear();
|
search_pill_widget.clear();
|
||||||
search_input_has_changed = false;
|
search_input_has_changed = false;
|
||||||
search_pill.set_search_bar_contents(narrow_state.search_terms(), search_pill_widget);
|
search_pill.set_search_bar_contents(
|
||||||
|
narrow_state.search_terms(),
|
||||||
|
search_pill_widget,
|
||||||
|
set_search_bar_text,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exit_search(opts: {keep_search_narrow_open: boolean}): void {
|
function exit_search(opts: {keep_search_narrow_open: boolean}): void {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import assert from "minimalistic-assert";
|
import $ from "jquery";
|
||||||
|
|
||||||
import {Filter} from "./filter";
|
import {Filter} from "./filter";
|
||||||
import * as input_pill from "./input_pill";
|
import * as input_pill from "./input_pill";
|
||||||
|
@ -39,8 +39,12 @@ type SearchPill =
|
||||||
|
|
||||||
export type SearchPillWidget = InputPillContainer<SearchPill>;
|
export type SearchPillWidget = InputPillContainer<SearchPill>;
|
||||||
|
|
||||||
export function create_item_from_search_string(search_string: string): SearchPill {
|
export function create_item_from_search_string(search_string: string): SearchPill | undefined {
|
||||||
const search_terms = Filter.parse(search_string);
|
const search_terms = Filter.parse(search_string);
|
||||||
|
if (!search_terms.every((term) => Filter.is_valid_search_term(term))) {
|
||||||
|
// This will cause pill validation to fail and trigger a shake animation.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const description_html = Filter.search_description_as_html(search_terms);
|
const description_html = Filter.search_description_as_html(search_terms);
|
||||||
return {
|
return {
|
||||||
display_value: search_string,
|
display_value: search_string,
|
||||||
|
@ -60,6 +64,9 @@ export function create_pills($pill_container: JQuery): SearchPillWidget {
|
||||||
get_text_from_item: get_search_string_from_item,
|
get_text_from_item: get_search_string_from_item,
|
||||||
split_text_on_comma: false,
|
split_text_on_comma: false,
|
||||||
});
|
});
|
||||||
|
// We don't automatically create pills on paste. When the user
|
||||||
|
// presses enter, we validate the input then.
|
||||||
|
pills.createPillonPaste(() => false);
|
||||||
return pills;
|
return pills;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,42 +103,53 @@ const user_pill_operators = new Set(["dm", "dm-including", "sender"]);
|
||||||
export function set_search_bar_contents(
|
export function set_search_bar_contents(
|
||||||
search_terms: NarrowTerm[],
|
search_terms: NarrowTerm[],
|
||||||
pill_widget: SearchPillWidget,
|
pill_widget: SearchPillWidget,
|
||||||
set_search_bar_text?: (text: string) => void,
|
set_search_bar_text: (text: string) => void,
|
||||||
): void {
|
): void {
|
||||||
pill_widget.clear();
|
pill_widget.clear();
|
||||||
let partial_pill = "";
|
let partial_pill = "";
|
||||||
|
const invalid_inputs = [];
|
||||||
|
|
||||||
for (const term of search_terms) {
|
for (const term of search_terms) {
|
||||||
if (user_pill_operators.has(term.operator) && term.operand !== "") {
|
|
||||||
const user_emails = term.operand.split(",");
|
|
||||||
const users = user_emails.map((email) => {
|
|
||||||
const user = people.get_by_email(email);
|
|
||||||
assert(user !== undefined);
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
append_user_pill(users, pill_widget, term.operator, term.negated ?? false);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const input = Filter.unparse([term]);
|
const input = Filter.unparse([term]);
|
||||||
|
|
||||||
// If the last term looks something like `dm:`, we
|
// If the last term looks something like `dm:`, we
|
||||||
// don't want to make it a pill, since it isn't isn't
|
// don't want to make it a pill, since it isn't isn't
|
||||||
// a complete search term yet.
|
// a complete search term yet.
|
||||||
// Instead, we keep the partial pill to the end of the
|
// Instead, we keep the partial pill to the end of the
|
||||||
// search box as text input, which will update the
|
// search box as text input, which will update the
|
||||||
// typeahead to show operand suggestions.
|
// typeahead to show operand suggestions.
|
||||||
if (
|
if (input.at(-1) === ":" && term.operand === "" && term === search_terms.at(-1)) {
|
||||||
set_search_bar_text !== undefined &&
|
|
||||||
input.at(-1) === ":" &&
|
|
||||||
term.operand === "" &&
|
|
||||||
term === search_terms.at(-1)
|
|
||||||
) {
|
|
||||||
partial_pill = input;
|
partial_pill = input;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Filter.is_valid_search_term(term)) {
|
||||||
|
invalid_inputs.push(input);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_pill_operators.has(term.operator) && term.operand !== "") {
|
||||||
|
const users = term.operand.split(",").map((email) => {
|
||||||
|
// This is definitely not undefined, because we just validated it
|
||||||
|
// with `Filter.is_valid_search_term`.
|
||||||
|
const user = people.get_by_email(email)!;
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
append_user_pill(users, pill_widget, term.operator, term.negated ?? false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
pill_widget.appendValue(input);
|
pill_widget.appendValue(input);
|
||||||
}
|
}
|
||||||
pill_widget.clear_text();
|
pill_widget.clear_text();
|
||||||
if (set_search_bar_text !== undefined) {
|
|
||||||
set_search_bar_text(partial_pill);
|
const search_bar_text_strings = [...invalid_inputs];
|
||||||
|
if (partial_pill !== "") {
|
||||||
|
search_bar_text_strings.push(partial_pill);
|
||||||
|
}
|
||||||
|
set_search_bar_text(search_bar_text_strings.join(" "));
|
||||||
|
if (invalid_inputs.length) {
|
||||||
|
$("#search_query").addClass("shake");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ const search_suggestion = mock_esm("../src/search_suggestion");
|
||||||
|
|
||||||
const search = zrequire("search");
|
const search = zrequire("search");
|
||||||
const search_pill = zrequire("search_pill");
|
const search_pill = zrequire("search_pill");
|
||||||
|
const stream_data = zrequire("stream_data");
|
||||||
|
|
||||||
function stub_pills() {
|
function stub_pills() {
|
||||||
const $pill_container = $("#searchbox-input-container.pill-container");
|
const $pill_container = $("#searchbox-input-container.pill-container");
|
||||||
|
@ -25,6 +26,14 @@ set_global("getSelection", () => ({
|
||||||
|
|
||||||
let typeahead_forced_open = false;
|
let typeahead_forced_open = false;
|
||||||
|
|
||||||
|
const verona = {
|
||||||
|
subscribed: true,
|
||||||
|
color: "blue",
|
||||||
|
name: "Verona",
|
||||||
|
stream_id: 1,
|
||||||
|
};
|
||||||
|
stream_data.add_sub(verona);
|
||||||
|
|
||||||
run_test("initialize", ({override, override_rewire, mock_template}) => {
|
run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||||
const $search_query_box = $("#search_query");
|
const $search_query_box = $("#search_query");
|
||||||
const $searchbox_form = $("#searchbox_form");
|
const $searchbox_form = $("#searchbox_form");
|
||||||
|
@ -211,7 +220,11 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||||
for (const pill of pills) {
|
for (const pill of pills) {
|
||||||
pill.$element.remove = noop;
|
pill.$element.remove = noop;
|
||||||
}
|
}
|
||||||
search_pill.set_search_bar_contents(terms, search.search_pill_widget);
|
search_pill.set_search_bar_contents(
|
||||||
|
terms,
|
||||||
|
search.search_pill_widget,
|
||||||
|
$search_query_box.text,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
terms = [
|
terms = [
|
||||||
|
|
Loading…
Reference in New Issue