topic-filter: Replace usage of search_pill.

Add topic_filter_pill to use for topic filtering instead of search_pill.

Fixes part of #35284.
This commit is contained in:
Maneesh Shukla
2025-07-28 16:25:07 +05:30
committed by Tim Abbott
parent 15f2c65986
commit 6e43d34a34
7 changed files with 112 additions and 38 deletions

View File

@@ -176,6 +176,10 @@ IGNORED_PHRASES = [
r"archived", r"archived",
# Used in pills for deactivated users. # Used in pills for deactivated users.
r"deactivated", r"deactivated",
# Used in pills for resolved topics.
r"resolved",
# Used in pills for unresolved topics.
r"unresolved",
# This is a reference to a setting/secret and should be lowercase. # This is a reference to a setting/secret and should be lowercase.
r"zulip_org_id", r"zulip_org_id",
# These are custom time unit options for modal dropdowns # These are custom time unit options for modal dropdowns

View File

@@ -271,6 +271,7 @@ EXEMPT_FILES = make_set(
"web/src/timerender.ts", "web/src/timerender.ts",
"web/src/tippyjs.ts", "web/src/tippyjs.ts",
"web/src/todo_widget.ts", "web/src/todo_widget.ts",
"web/src/topic_filter_pill.ts",
"web/src/topic_list.ts", "web/src/topic_list.ts",
"web/src/topic_popover.ts", "web/src/topic_popover.ts",
"web/src/typing.ts", "web/src/typing.ts",

View File

@@ -0,0 +1,68 @@
import render_input_pill from "../templates/input_pill.hbs";
import {$t} from "./i18n.ts";
import type {InputPillConfig, InputPillContainer} from "./input_pill.ts";
import * as input_pill from "./input_pill.ts";
export type TopicFilterPill = {
type: "topic_filter";
label: string;
syntax: string;
};
export type TopicFilterPillWidget = InputPillContainer<TopicFilterPill>;
export const filter_options: TopicFilterPill[] = [
{
type: "topic_filter",
label: $t({defaultMessage: "unresolved"}),
syntax: "-is:resolved",
},
{
type: "topic_filter",
label: $t({defaultMessage: "resolved"}),
syntax: "is:resolved",
},
];
export function create_item_from_syntax(
syntax: string,
current_items: TopicFilterPill[],
): TopicFilterPill | undefined {
const existing_syntaxes = current_items.map((item) => item.syntax);
if (existing_syntaxes.includes(syntax)) {
return undefined;
}
// Find the matching filter option
const filter_option = filter_options.find((option) => option.syntax === syntax);
if (!filter_option) {
return undefined;
}
return filter_option;
}
export function get_syntax_from_item(item: TopicFilterPill): string {
return item.syntax;
}
export function create_pills(
$pill_container: JQuery,
pill_config?: InputPillConfig,
): TopicFilterPillWidget {
const pill_container = input_pill.create({
$container: $pill_container,
pill_config,
create_item_from_text: create_item_from_syntax,
get_text_from_item: get_syntax_from_item,
get_display_value_from_item: get_syntax_from_item,
generate_pill_html(item: TopicFilterPill, disabled?: boolean) {
return render_input_pill({
display_value: item.label,
disabled,
});
},
});
pill_container.createPillonPaste(() => false);
return pill_container;
}

View File

@@ -10,43 +10,26 @@ import {all_messages_data} from "./all_messages_data.ts";
import * as blueslip from "./blueslip.ts"; import * as blueslip from "./blueslip.ts";
import {Typeahead} from "./bootstrap_typeahead.ts"; import {Typeahead} from "./bootstrap_typeahead.ts";
import type {TypeaheadInputElement} from "./bootstrap_typeahead.ts"; import type {TypeaheadInputElement} from "./bootstrap_typeahead.ts";
import {$t} from "./i18n.ts";
import * as popover_menus from "./popover_menus.ts"; import * as popover_menus from "./popover_menus.ts";
import * as popovers from "./popovers.ts"; import * as popovers from "./popovers.ts";
import * as scroll_util from "./scroll_util.ts"; import * as scroll_util from "./scroll_util.ts";
import type {SearchPillWidget} from "./search_pill.ts";
import * as search_pill from "./search_pill.ts";
import * as sidebar_ui from "./sidebar_ui.ts"; import * as sidebar_ui from "./sidebar_ui.ts";
import * as stream_topic_history from "./stream_topic_history.ts"; import * as stream_topic_history from "./stream_topic_history.ts";
import * as stream_topic_history_util from "./stream_topic_history_util.ts"; import * as stream_topic_history_util from "./stream_topic_history_util.ts";
import type {StreamSubscription} from "./sub_store.ts"; import type {StreamSubscription} from "./sub_store.ts";
import * as sub_store from "./sub_store.ts"; import * as sub_store from "./sub_store.ts";
import * as topic_filter_pill from "./topic_filter_pill.ts";
import type {TopicFilterPill, TopicFilterPillWidget} from "./topic_filter_pill.ts";
import * as topic_list_data from "./topic_list_data.ts"; import * as topic_list_data from "./topic_list_data.ts";
import type {TopicInfo} from "./topic_list_data.ts"; import type {TopicInfo} from "./topic_list_data.ts";
import * as typeahead_helper from "./typeahead_helper.ts"; import * as typeahead_helper from "./typeahead_helper.ts";
import * as vdom from "./vdom.ts"; import * as vdom from "./vdom.ts";
type TopicFilterPill = {
label: string;
syntax: string;
};
const filter_options: TopicFilterPill[] = [
{
label: $t({defaultMessage: "Unresolved topics"}),
syntax: "-is:resolved",
},
{
label: $t({defaultMessage: "Resolved topics"}),
syntax: "is:resolved",
},
];
/* Track all active widgets with a Map by stream_id. We have at max /* Track all active widgets with a Map by stream_id. We have at max
one for now, but we may eventually allow multiple streams to be one for now, but we may eventually allow multiple streams to be
expanded. */ expanded. */
const active_widgets = new Map<number, LeftSidebarTopicListWidget>(); const active_widgets = new Map<number, LeftSidebarTopicListWidget>();
export let search_pill_widget: SearchPillWidget | null = null; export let topic_filter_pill_widget: TopicFilterPillWidget | null = null;
export let topic_state_typeahead: Typeahead<TopicFilterPill> | undefined; export let topic_state_typeahead: Typeahead<TopicFilterPill> | undefined;
// We know whether we're zoomed or not. // We know whether we're zoomed or not.
@@ -366,7 +349,7 @@ function filter_topics_left_sidebar(topic_names: string[]): string[] {
return topic_list_data.filter_topics_by_search_term( return topic_list_data.filter_topics_by_search_term(
topic_names, topic_names,
search_term, search_term,
get_typeahead_search_term(), get_typeahead_search_pills_syntax(),
); );
} }
@@ -386,10 +369,10 @@ export class LeftSidebarTopicListWidget extends TopicListWidget {
export function clear_topic_search(e: JQuery.Event): void { export function clear_topic_search(e: JQuery.Event): void {
e.stopPropagation(); e.stopPropagation();
search_pill_widget?.clear(true); topic_filter_pill_widget?.clear(true);
const $input = $("#topic_filter_query"); const $input = $("#topic_filter_query");
// Since the `clear` function of the search_pill_widget // Since the `clear` function of the topic_filter_pill_widget
// takes care of clearing both the text content and the // takes care of clearing both the text content and the
// pills, we just need to trigger an input event on the // pills, we just need to trigger an input event on the
// contenteditable element to reset the topic list via // contenteditable element to reset the topic list via
@@ -499,10 +482,24 @@ export function get_left_sidebar_topic_search_term(): string {
return $("#topic_filter_query").text().trim(); return $("#topic_filter_query").text().trim();
} }
export function get_typeahead_search_term(): string { export function get_typeahead_search_pills_syntax(): string {
const $pills = $("#left-sidebar-filter-topic-input .pill"); const pills = topic_filter_pill_widget?.items() ?? [];
const value = $pills.find(".pill-value").text().trim();
return value; if (pills.length === 0) {
return "";
}
// For now, there is only one pill in the left sidebar topic search input.
// This is because we only allow one topic filter pill at a time.
// If we allow multiple pills in the future, we may need to
// change this logic to return the syntax of all pills.
if (pills.length > 1) {
blueslip.warn("Multiple pills found in left sidebar topic search input.");
}
// We can remove this assumption once we allow multiple pills and hence update the
// callers of this function to handle multiple pills and implement the search accordingly.
return pills[0]!.syntax;
} }
function set_search_bar_text(text: string): void { function set_search_bar_text(text: string): void {
@@ -519,7 +516,7 @@ export function setup_topic_search_typeahead(): void {
return; return;
} }
search_pill_widget = search_pill.create_pills($pill_container); topic_filter_pill_widget = topic_filter_pill.create_pills($pill_container);
const typeahead_input: TypeaheadInputElement = { const typeahead_input: TypeaheadInputElement = {
$element: $input, $element: $input,
@@ -538,7 +535,7 @@ export function setup_topic_search_typeahead(): void {
if ($pills.length > 0) { if ($pills.length > 0) {
return []; return [];
} }
return [...filter_options]; return [...topic_filter_pill.filter_options];
}, },
item_html(item: TopicFilterPill) { item_html(item: TopicFilterPill) {
return typeahead_helper.render_topic_state(item.label); return typeahead_helper.render_topic_state(item.label);
@@ -559,9 +556,9 @@ export function setup_topic_search_typeahead(): void {
return items; return items;
}, },
updater(item: TopicFilterPill) { updater(item: TopicFilterPill) {
assert(search_pill_widget !== null); assert(topic_filter_pill_widget !== null);
search_pill_widget.clear(true); topic_filter_pill_widget.clear(true);
search_pill_widget.appendValue(item.syntax); topic_filter_pill_widget.appendValue(item.syntax);
set_search_bar_text(""); set_search_bar_text("");
$input.trigger("focus"); $input.trigger("focus");
return get_left_sidebar_topic_search_term(); return get_left_sidebar_topic_search_term();
@@ -585,7 +582,7 @@ export function setup_topic_search_typeahead(): void {
} }
}); });
search_pill_widget.onPillRemove(() => { topic_filter_pill_widget.onPillRemove(() => {
const stream_id = active_stream_id(); const stream_id = active_stream_id();
if (stream_id !== undefined) { if (stream_id !== undefined) {
const widget = active_widgets.get(stream_id); const widget = active_widgets.get(stream_id);

View File

@@ -1,4 +1,8 @@
<div class='pill {{#if deactivated}} deactivated-pill {{/if}}'{{#if user_id}}data-user-id="{{user_id}}"{{/if}}{{#if group_id}}data-user-group-id="{{group_id}}"{{/if}}{{#if stream_id}}data-stream-id="{{stream_id}}"{{/if}} tabindex=0> <div class='pill {{#if deactivated}} deactivated-pill {{/if~}}'
{{~#if user_id}}data-user-id="{{user_id}}"{{/if~}}
{{~#if group_id}}data-user-group-id="{{group_id}}"{{/if~}}
{{~#if stream_id}}data-stream-id="{{stream_id}}"{{/if~}}
{{~#if data_syntax}}data-syntax="{{data_syntax}}"{{/if}} tabindex=0>
{{#if has_image}} {{#if has_image}}
<img class="pill-image" src="{{img_src}}" /> <img class="pill-image" src="{{img_src}}" />
<div class="pill-image-border"></div> <div class="pill-image-border"></div>