stream_settings: Migrate to new DropdownWidget.

Also, remove old DropdownListWidget since it is no longer used.
This commit is contained in:
Aman Agrawal 2023-07-19 19:48:20 +05:30 committed by Tim Abbott
parent aa8e94ca6d
commit 875d564f2d
8 changed files with 73 additions and 922 deletions

View File

@ -1,627 +0,0 @@
import $ from "jquery";
import _ from "lodash";
import tippy from "tippy.js";
import render_inline_decorated_stream_name from "../templates/inline_decorated_stream_name.hbs";
import render_dropdown_list from "../templates/settings/dropdown_list.hbs";
import * as blueslip from "./blueslip";
import {$t} from "./i18n";
import * as keydown_util from "./keydown_util";
import * as ListWidget from "./list_widget";
export class DropdownListWidget {
constructor({
widget_name,
data,
default_text,
render_text = (item_name) => item_name,
null_value = null,
include_current_item = true,
value,
on_update = () => {},
}) {
// Initializing values
this.widget_name = widget_name;
this.data = data;
this.default_text = default_text;
this.render_text = render_text;
this.null_value = null_value;
this.include_current_item = include_current_item;
this.initial_value = value;
this.on_update = on_update;
this.list_widget = null;
this.container_id = `${widget_name}_widget`;
this.value_id = `id_${widget_name}`;
if (value === undefined) {
this.initial_value = null_value;
blueslip.warn("dropdown-list-widget: Called without a default value; using null value");
}
}
render_default_text($elem) {
$elem.text(this.default_text);
$elem.addClass("text-warning");
$elem.closest(".input-group").find(".dropdown_list_reset_button").hide();
}
render(value) {
$(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data("value", value);
const $elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);
if (!value || value === this.null_value) {
this.render_default_text($elem);
return;
}
// Happy path
const item = this.data.find((x) => x.value === value.toString());
if (item === undefined) {
this.render_default_text($elem);
return;
}
if (item.stream !== undefined) {
const stream = item.stream;
const rendered_stream_name_with_privacy_symbol_html =
render_inline_decorated_stream_name({stream, show_colored_icon: true});
$elem.html(rendered_stream_name_with_privacy_symbol_html);
} else {
const text = this.render_text(item.name);
$elem.text(text);
}
$elem.removeClass("text-warning");
$elem.closest(".input-group").find(".dropdown_list_reset_button").show();
}
update(value, e) {
this.render(value);
this.on_update(value, e);
}
register_event_handlers() {
$(`#${CSS.escape(this.container_id)} .dropdown-list-body`).on(
"click",
".list_item",
(e) => {
const value = $(e.currentTarget).attr("data-value");
this.update(value, e);
},
);
$(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => {
this.update(this.null_value, e);
e.preventDefault();
});
}
get_data = (data) => {
if (this.include_current_item) {
return data;
}
return data.filter((x) => x.value !== this.value.toString());
};
setup_dropdown_widget(data) {
const $dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
this.list_widget = ListWidget.create($dropdown_list_body, this.get_data(data), {
name: `${CSS.escape(this.widget_name)}_list`,
get_item: ListWidget.default_get_item,
modifier(item) {
return render_dropdown_list({item});
},
filter: {
$element: $search_input,
predicate(item, value) {
return item.name.toLowerCase().includes(value);
},
},
$simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`),
});
}
replace_data(data) {
this.data = data;
this.list_widget.replace_list_data(this.get_data(data));
}
// Sets the focus to the ListWidget input once the dropdown button is clicked.
dropdown_toggle_click_handler() {
const $dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
$dropdown_toggle.on("click", () => {
$search_input.val("").trigger("input");
});
}
dropdown_keyboard_events() {
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
const $dropdown_menu = $(`.${CSS.escape(this.widget_name)}_setting .dropdown-menu`);
const dropdown_elements = () => {
const $dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
return $dropdown_list_body.children().find("a");
};
// Rest of the key handlers are supported by our
// bootstrap library.
$dropdown_menu.on("keydown", (e) => {
function trigger_element_focus($element) {
e.preventDefault();
e.stopPropagation();
$element.trigger("focus");
}
switch (e.key) {
case "ArrowDown": {
switch (e.target) {
case dropdown_elements().last()[0]:
trigger_element_focus($search_input);
break;
case $search_input[0]:
trigger_element_focus(dropdown_elements().first());
break;
}
break;
}
case "ArrowUp": {
switch (e.target) {
case dropdown_elements().first()[0]:
trigger_element_focus($search_input);
break;
case $search_input[0]:
trigger_element_focus(dropdown_elements().last());
}
break;
}
case "Tab": {
switch (e.target) {
case $search_input[0]:
trigger_element_focus(dropdown_elements().first());
break;
case dropdown_elements().last()[0]:
trigger_element_focus($search_input);
break;
}
break;
}
}
if (keydown_util.is_enter_event(e)) {
e.stopPropagation();
e.preventDefault();
if (e.target === $search_input[0]) {
// Select the first option from the menu on pressing
// "Enter" when focus is on the search input.
dropdown_elements().first().trigger("click");
} else if ($(e.target).parent().hasClass("list_item")) {
$(e.target).trigger("click");
}
}
});
}
setup() {
// populate the dropdown
const $dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
const $dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
this.setup_dropdown_widget(this.data);
$(`#${CSS.escape(this.container_id)} .dropdown-search`).on("click", (e) => {
e.stopPropagation();
});
this.dropdown_toggle_click_handler();
$dropdown_toggle.on("focus", (e) => {
// On opening a Bootstrap Dropdown, the parent element receives focus.
// Here, we want our search input to have focus instead.
e.preventDefault();
// This function gets called twice when focusing the
// dropdown, and only in the second call is the input
// field visible in the DOM; so the following visibility
// check ensures we wait for the second call to focus.
if ($dropdown_list_body.is(":visible")) {
$search_input.trigger("focus");
}
});
this.dropdown_keyboard_events();
this.render(this.initial_value);
this.register_event_handlers();
}
// Returns the updated value
value() {
let val = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data(
"value",
);
if (val === null) {
val = "";
}
return val;
}
}
// A widget mostly similar to `DropdownListWidget` but
// used in cases of multiple dropdown selection.
export class MultiSelectDropdownListWidget extends DropdownListWidget {
constructor({
widget_name,
data,
default_text,
null_value = null,
on_update = () => {},
on_close,
value,
limit,
}) {
super({
widget_name,
data,
default_text,
null_value,
on_update,
value,
});
// Initializing values specific to `MultiSelectDropdownListWidget`.
this.limit = limit;
this.on_close = on_close;
// Important thing to note is that this needs to be maintained as
// a reference type and not to deep clone it/assign it to a
// different variable, so that it can be later referenced within
// `list_widget` as well. The way we manage dropdown elements are
// essentially by just modifying the values in `data_selected` variable.
this.data_selected = []; // Populate the dropdown values selected by user.
if (limit === undefined) {
this.limit = 2;
blueslip.warn(
"Multiselect dropdown-list-widget: Called without limit value; using 2 as the limit",
);
}
}
setup() {
super.setup(this);
this.initialize_dropdown_values();
}
initialize_dropdown_values() {
// Stop the execution if value parameter is undefined and null_value is passed.
if (!this.initial_value || this.initial_value === this.null_value) {
return;
}
const $elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);
// Push values from initial valued array to `data_selected`.
this.data_selected.push(...this.initial_value);
this.render_button_text($elem, this.limit);
}
// Set the button text as per the selected data.
render_button_text($elem, limit) {
const items_selected = this.data_selected.length;
let text = "";
// Destroy the tooltip once the button text reloads.
this.destroy_tooltip();
if (items_selected === 0) {
this.render_default_text($elem);
return;
} else if (limit >= items_selected) {
const data_selected = this.data.filter((data) =>
this.data_selected.includes(data.value),
);
text = data_selected.map((data) => data.name).toString();
} else {
text = $t({defaultMessage: "{items_selected} selected"}, {items_selected});
this.render_tooltip();
}
$elem.text(text);
$elem.removeClass("text-warning");
$elem.closest(".input-group").find(".dropdown_list_reset_button").show();
}
// Override the DrodownListWidget `render` function.
render(value) {
const $elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`);
if (!value || value === this.null_value) {
this.render_default_text($elem);
return;
}
this.render_button_text($elem, this.limit);
}
dropdown_toggle_click_handler() {
const $dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`);
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
$dropdown_toggle.on("click", () => {
this.reset_dropdown_items();
$search_input.val("").trigger("input");
});
}
// Cases where a user presses any dropdown item but accidentally closes
// the dropdown list.
reset_dropdown_items() {
// Clear the data selected and stop the execution once the user has
// pressed the `reset` button.
if (this.is_reset) {
this.data_selected.splice(0, this.data_selected.length);
return;
}
const original_items = this.checked_items ?? this.initial_value;
const items_added = _.difference(this.data_selected, original_items);
// Removing the unnecessary items from dropdown.
for (const val of items_added) {
const index = this.data_selected.indexOf(val);
if (index > -1) {
this.data_selected.splice(index, 1);
}
}
// Items that are removed in dropdown but should have been a part of it
const items_removed = _.difference(original_items, this.data_selected);
this.data_selected.push(...items_removed);
}
// Override the DrodownListWidget `setup_dropdown_widget` function.
setup_dropdown_widget(data) {
const $dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
this.list_widget = ListWidget.create($dropdown_list_body, data, {
name: `${CSS.escape(this.widget_name)}_list`,
get_item: ListWidget.default_get_item,
modifier(item) {
return render_dropdown_list({item});
},
multiselect: {
selected_items: this.data_selected,
},
filter: {
$element: $search_input,
predicate(item, value) {
return item.name.toLowerCase().includes(value);
},
},
$simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`),
});
}
// Add the check mark to dropdown element passed.
add_check_mark($element) {
const value = $element.attr("data-value");
const $link_elem = $element.find("a").expectOne();
$link_elem.prepend($("<i>").addClass(["fa", "fa-check"]));
$element.addClass("checked");
this.data_selected.push(value);
}
// Remove the check mark from dropdown element.
remove_check_mark($element) {
const $icon = $element.find("i").expectOne();
const value = $element.attr("data-value");
const index = this.data_selected.indexOf(value);
if (index > -1) {
$icon.remove();
$element.removeClass("checked");
this.data_selected.splice(index, 1);
}
}
// Render the tooltip once the text changes to `n` selected.
render_tooltip() {
const $elem = $(`#${CSS.escape(this.container_id)}`);
const selected_items = this.data.filter((item) => this.checked_items.includes(item.value));
tippy($elem[0], {
content: selected_items.map((item) => item.name).join(", "),
placement: "top",
});
}
destroy_tooltip() {
const $elem = $(`#${CSS.escape(this.container_id)}`);
const tippy_instance = $elem[0]._tippy;
if (!tippy_instance) {
return;
}
tippy_instance.destroy();
}
dropdown_keyboard_events() {
// Main keydown event handler which transfers the focus from one child element
// to another.
const $search_input = $(
`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`,
);
const $dropdown_menu = $(`.${CSS.escape(this.widget_name)}_setting .dropdown-menu`);
const $filter_button = $(`#${CSS.escape(this.container_id)} .multiselect_btn`);
const dropdown_elements = () => {
const $dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
return $dropdown_list_body.children().find("a");
};
$dropdown_menu.on("keydown", (e) => {
function trigger_element_focus($element) {
e.preventDefault();
e.stopPropagation();
$element.trigger("focus");
}
switch (e.key) {
case "ArrowDown": {
switch (e.target) {
case dropdown_elements().last()[0]:
trigger_element_focus($filter_button);
break;
case $(`#${CSS.escape(this.container_id)} .multiselect_btn`)[0]:
trigger_element_focus($search_input);
break;
case $search_input[0]:
trigger_element_focus(dropdown_elements().first());
break;
}
break;
}
case "ArrowUp": {
switch (e.target) {
case dropdown_elements().first()[0]:
trigger_element_focus($search_input);
break;
case $search_input[0]:
trigger_element_focus($filter_button);
break;
case $(`#${CSS.escape(this.container_id)} .multiselect_btn`)[0]:
trigger_element_focus(dropdown_elements().last());
break;
}
break;
}
case "Tab": {
switch (e.target) {
case $search_input[0]:
trigger_element_focus(dropdown_elements().first());
break;
case $filter_button[0]:
trigger_element_focus($search_input);
break;
}
break;
}
}
if (keydown_util.is_enter_event(e)) {
e.stopPropagation();
e.preventDefault();
if (e.target === $search_input[0]) {
// Select the first option from the menu on pressing
// "Enter" when focus is on the search input.
dropdown_elements().first().trigger("click");
} else if ($(e.target).parent().hasClass("list_item")) {
$(e.target).trigger("click");
}
}
});
}
// Override the `register_event_handlers` function.
register_event_handlers() {
const $dropdown_list_body = $(
`#${CSS.escape(this.container_id)} .dropdown-list-body`,
).expectOne();
$dropdown_list_body.on("click", ".list_item", (e) => {
const $element = e.target.closest("li");
if ($element.hasClass("checked")) {
this.remove_check_mark($element);
} else {
this.add_check_mark($element);
}
e.stopPropagation();
});
$(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => {
// Default back the values.
this.is_reset = true;
this.checked_items = undefined;
this.update(this.null_value);
e.preventDefault();
});
$(`#${CSS.escape(this.container_id)} .multiselect_btn`).on("click", (e) => {
e.preventDefault();
// Set the value to `false` to end the scope of the
// `reset` button.
this.is_reset = false;
// We deep clone the values of `data_selected` to a new
// variable. This is so because arrays are reference types
// and modifying the parent array can change the values
// within the child array. Here, `checked_items` copies over the
// value and not just the reference.
this.checked_items = _.cloneDeep(this.data_selected);
this.update(this.data_selected);
// Cases when the user wants to pass a successful event after
// the dropdown is closed.
if (this.on_close) {
e.stopPropagation();
const $setting_elem = $(e.currentTarget).closest(
`.${CSS.escape(this.widget_name)}_setting`,
);
$setting_elem.find(".dropdown-menu").dropdown("toggle");
this.on_close();
}
});
}
// Returns array of values selected by user.
value() {
let val = this.checked_items;
// Cases taken care of -
// - User never pressed the filter button -> We return the initial value.
// - User pressed the `reset` button -> We return an empty array.
if (val === undefined) {
val = this.is_reset ? [] : this.initial_value;
}
return val;
}
}

View File

@ -14,7 +14,7 @@ import * as channel from "./channel";
import * as components from "./components";
import * as confirm_dialog from "./confirm_dialog";
import * as dialog_widget from "./dialog_widget";
import {DropdownListWidget} from "./dropdown_list_widget";
import * as dropdown_widget from "./dropdown_widget";
import * as hash_util from "./hash_util";
import {$t, $t_html} from "./i18n";
import * as keydown_util from "./keydown_util";
@ -193,6 +193,37 @@ export function stream_settings(sub) {
return settings;
}
function setup_dropdown(sub, slim_sub) {
can_remove_subscribers_group_widget = new dropdown_widget.DropdownWidget({
widget_name: "can_remove_subscribers_group",
get_options: () =>
user_groups.get_realm_user_groups_for_dropdown_list_widget(
"can_remove_subscribers_group",
),
item_click_callback(event, dropdown) {
dropdown.hide();
event.preventDefault();
event.stopPropagation();
can_remove_subscribers_group_widget.render();
settings_org.save_discard_widget_status_handler(
$("#stream_permission_settings"),
false,
slim_sub,
);
},
$events_container: $("#subscription_overlay .subscription_settings"),
tippy_props: {
placement: "bottom-start",
},
default_id: sub.can_remove_subscribers_group,
unique_id_type: dropdown_widget.DATA_TYPES.NUMBER,
on_mount_callback(dropdown) {
$(dropdown.popper).css("min-width", "300px");
},
});
can_remove_subscribers_group_widget.setup();
}
export function show_settings_for(node) {
const stream_id = get_stream_id(node);
const slim_sub = sub_store.get(stream_id);
@ -209,24 +240,6 @@ export function show_settings_for(node) {
return false;
});
const opts = {
widget_name: "can_remove_subscribers_group",
data: user_groups.get_realm_user_groups_for_dropdown_list_widget(
"can_remove_subscribers_group",
),
default_text: $t({defaultMessage: "No user groups"}),
include_current_item: false,
value: sub.can_remove_subscribers_group,
on_update() {
settings_org.save_discard_widget_status_handler(
$("#stream_permission_settings"),
false,
slim_sub,
);
},
};
can_remove_subscribers_group_widget = new DropdownListWidget(opts);
const html = render_stream_settings({
sub,
notification_settings,
@ -257,7 +270,7 @@ export function show_settings_for(node) {
show_subscription_settings(sub);
settings_org.set_message_retention_setting_dropdown(sub);
stream_ui_updates.enable_or_disable_permission_settings_in_edit_panel(sub);
can_remove_subscribers_group_widget.setup();
setup_dropdown(sub, slim_sub);
}
export function setup_stream_settings(node) {

View File

@ -15,7 +15,7 @@ import * as channel from "./channel";
import * as components from "./components";
import * as compose_state from "./compose_state";
import * as confirm_dialog from "./confirm_dialog";
import {DropdownListWidget} from "./dropdown_list_widget";
import * as dropdown_widget from "./dropdown_widget";
import * as hash_util from "./hash_util";
import {$t, $t_html} from "./i18n";
import * as keydown_util from "./keydown_util";
@ -589,6 +589,32 @@ export function switch_stream_sort(tab_name) {
export let new_stream_can_remove_subscribers_group_widget = null;
function dropdown_setup() {
new_stream_can_remove_subscribers_group_widget = new dropdown_widget.DropdownWidget({
widget_name: "new_stream_can_remove_subscribers_group",
get_options: () =>
user_groups.get_realm_user_groups_for_dropdown_list_widget(
"can_remove_subscribers_group",
),
item_click_callback(event, dropdown) {
dropdown.hide();
event.preventDefault();
event.stopPropagation();
new_stream_can_remove_subscribers_group_widget.render();
},
$events_container: $("#subscription_overlay"),
tippy_props: {
placement: "bottom-start",
},
on_mount_callback(dropdown) {
$(dropdown.popper).css("min-width", "300px");
},
default_text: $t({defaultMessage: "No user groups"}),
default_id: user_groups.get_user_group_from_name("role:administrators").id,
unique_id_type: dropdown_widget.DATA_TYPES.NUMBER,
});
}
export function setup_page(callback) {
// We should strongly consider only setting up the page once,
// but I am writing these comments write before a big release,
@ -664,17 +690,6 @@ export function setup_page(callback) {
function populate_and_fill() {
$("#streams_overlay_container").empty();
const opts = {
widget_name: "new_stream_can_remove_subscribers_group",
data: user_groups.get_realm_user_groups_for_dropdown_list_widget(
"can_remove_subscribers_group",
),
default_text: $t({defaultMessage: "No user groups"}),
include_current_item: false,
value: user_groups.get_user_group_from_name("role:administrators").id,
};
new_stream_can_remove_subscribers_group_widget = new DropdownListWidget(opts);
// TODO: Ideally we'd indicate in some way what stream types
// the user can create, by showing other options as disabled.
const stream_privacy_policy = stream_data.stream_privacy_policy_values.public.code;
@ -711,6 +726,7 @@ export function setup_page(callback) {
render_left_panel_superset();
initialize_components();
dropdown_setup();
redraw_left_panel();
stream_create.set_up_handlers();

View File

@ -21,7 +21,7 @@ type UserGroupRaw = Omit<UserGroup, "members"> & {members: number[]};
type UserGroupForDropdownListWidget = {
name: string;
value: string;
unique_id: number;
};
let user_group_name_dict: FoldDict<UserGroup>;
@ -257,7 +257,7 @@ export function get_realm_user_groups_for_dropdown_list_widget(
}
return {
name: group.display_name,
value: user_group.id.toString(),
unique_id: user_group.id,
};
});
@ -267,7 +267,7 @@ export function get_realm_user_groups_for_dropdown_list_widget(
const user_groups_excluding_system_groups = get_realm_user_groups().map((group) => ({
name: group.name,
value: group.id.toString(),
unique_id: group.id,
}));
return [...system_user_groups, ...user_groups_excluding_system_groups];

View File

@ -1,32 +0,0 @@
<div class="input-group dropdown-list-widget" id="{{widget_name}}_widget">
<span class="{{widget_name}}_setting dropup actual-dropdown-menu prop-element" id="id_{{widget_name}}" aria-labelledby="{{widget_name}}_label" data-setting-widget-type="dropdown-list-widget" {{#if value_type}}data-setting-value-type="{{value_type}}"{{/if}}>
{{#if label}}
<label id="{{widget_name}}_label" class="dropdown-title">
{{ label }}
</label>
{{/if}}
<button class="button rounded dropdown-toggle" type="button" data-toggle="dropdown">
<span id="{{widget_name}}_name"></span>
<i class="fa fa-chevron-down"></i>
</button>
<ul class="dropdown-menu modal-bg" role="menu">
<li class="dropdown-search" role="presentation">
<input class="no-input-change-detection filter_text_input" type="text" role="menuitem" placeholder="{{list_placeholder}}" autofocus/>
</li>
<div class="dropdown-list-wrapper" data-simplebar>
<span class="dropdown-list-body"></span>
{{#if filter_button_text }}
<button class="button rounded small sea-green multiselect_btn" tabindex="0">
<i class="fa fa-filter" aria-hidden="true"></i>
{{filter_button_text}}
</button>
{{/if}}
</div>
</ul>
</span>
{{#if reset_button_text}}
<a class="dropdown_list_reset_button" role="button">
{{reset_button_text}}
</a>
{{/if}}
</div>

View File

@ -29,14 +29,10 @@
</select>
</div>
<div class="input-group new-style">
<label class="dropdown-title">{{t 'Who can unsubscribe others from this stream?'}}
</label>
{{> ../settings/dropdown_list_widget
{{> ../dropdown_widget_with_label
widget_name=can_remove_subscribers_setting_widget_name
list_placeholder=(t 'Filter roles')
value_type="number" }}
</div>
label=(t 'Who can unsubscribe others from this stream?')
value_type="number"}}
{{#if (or is_owner is_stream_edit)}}
<div>

View File

@ -1,215 +0,0 @@
"use strict";
const {strict: assert} = require("assert");
const {$t} = require("./lib/i18n");
const {mock_esm, zrequire, set_global} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const blueslip = require("./lib/zblueslip");
const $ = require("./lib/zjquery");
const noop = () => {};
mock_esm("../src/list_widget", {
create: () => ({init: noop}),
});
mock_esm("tippy.js", {
default(arg) {
arg._tippy = {setContent: noop, placement: noop, destroy: noop};
return arg._tippy;
},
});
set_global("document", {});
const {DropdownListWidget, MultiSelectDropdownListWidget} = zrequire("dropdown_list_widget");
// For DropdownListWidget
const setup_dropdown_zjquery_data = (name) => {
const $input_group = $(".input_group");
const $reset_button = $(".dropdown_list_reset_button");
$input_group.set_find_results(".dropdown_list_reset_button", $reset_button);
$(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`).closest = () => $input_group;
const $widget = $(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`);
return {$reset_button, $widget};
};
run_test("basic_functions", () => {
let updated_value;
const opts = {
widget_name: "my_setting",
data: ["one", "two", "three"].map((x) => ({name: x, value: x})),
value: "one",
on_update(val) {
updated_value = val;
},
default_text: $t({defaultMessage: "not set"}),
render_text: (text) => `rendered: ${text}`,
};
const {$reset_button, $widget} = setup_dropdown_zjquery_data(opts.widget_name);
const widget = new DropdownListWidget(opts);
widget.setup();
assert.equal(widget.value(), "one");
assert.equal(updated_value, undefined); // We haven't 'updated' the widget yet.
assert.ok($reset_button.visible());
widget.update("two");
assert.equal($widget.text(), "rendered: two");
assert.equal(widget.value(), "two");
assert.equal(updated_value, "two");
assert.ok($reset_button.visible());
widget.update(null);
assert.equal($widget.text(), "translated: not set");
assert.equal(widget.value(), "");
assert.equal(updated_value, null);
assert.ok(!$reset_button.visible());
widget.update("four");
assert.equal($widget.text(), "translated: not set");
assert.equal(widget.value(), "four");
assert.equal(updated_value, "four");
assert.ok(!$reset_button.visible());
});
run_test("no_default_value", () => {
const opts = {
widget_name: "my_setting",
data: ["one", "two", "three"].map((x) => ({name: x, value: x})),
default_text: $t({defaultMessage: "not set"}),
render_text: /* istanbul ignore next */ (text) => `rendered: ${text}`,
null_value: "null-value",
};
blueslip.expect(
"warn",
"dropdown-list-widget: Called without a default value; using null value",
);
setup_dropdown_zjquery_data(opts.widget_name);
const widget = new DropdownListWidget(opts);
widget.setup();
assert.equal(widget.value(), "null-value");
});
// For MultiSelectDropdownListWidget
const setup_multiselect_dropdown_zjquery_data = function (name) {
$(`#${CSS.escape(name)}_widget`)[0] = {};
return setup_dropdown_zjquery_data(name);
};
run_test("basic MDLW functions", () => {
let updated_value;
const opts = {
widget_name: "my_setting",
data: ["one", "two", "three", "four"].map((x) => ({name: x, value: x})),
value: ["one"],
limit: 2,
on_update(val) {
updated_value = val;
},
default_text: $t({defaultMessage: "not set"}),
};
const {$reset_button, $widget} = setup_multiselect_dropdown_zjquery_data(opts.widget_name);
const widget = new MultiSelectDropdownListWidget(opts);
widget.setup();
function set_dropdown_variables(widget, value) {
widget.data_selected = value;
widget.checked_items = value;
}
assert.deepEqual(widget.value(), ["one"]);
assert.equal(updated_value, undefined);
assert.equal($widget.text(), "one");
assert.ok($reset_button.visible());
set_dropdown_variables(widget, ["one", "two"]);
widget.update(widget.data_selected);
assert.equal($widget.text(), "one,two");
assert.deepEqual(widget.value(), ["one", "two"]);
assert.deepEqual(updated_value, ["one", "two"]);
assert.ok($reset_button.visible());
set_dropdown_variables(widget, ["one", "two", "three"]);
widget.update(widget.data_selected);
assert.equal($widget.text(), "translated: 3 selected");
assert.deepEqual(widget.value(), ["one", "two", "three"]);
assert.deepEqual(updated_value, ["one", "two", "three"]);
assert.ok($reset_button.visible());
set_dropdown_variables(widget, null);
widget.update(widget.data_selected);
assert.equal($widget.text(), "translated: not set");
assert.equal(widget.value(), null);
assert.equal(updated_value, null);
assert.ok(!$reset_button.visible());
set_dropdown_variables(widget, ["one"]);
widget.update(widget.data_selected);
assert.equal($widget.text(), "one");
assert.deepEqual(widget.value(), ["one"]);
assert.deepEqual(updated_value, ["one"]);
assert.ok($reset_button.visible());
});
run_test("MDLW no_default_value", () => {
const opts = {
widget_name: "my_setting",
data: ["one", "two", "three", "four"].map((x) => ({name: x, value: x})),
limit: 2,
null_value: "null-value",
default_text: $t({defaultMessage: "not set"}),
};
blueslip.expect(
"warn",
"dropdown-list-widget: Called without a default value; using null value",
);
setup_multiselect_dropdown_zjquery_data(opts.widget_name);
const widget = new MultiSelectDropdownListWidget(opts);
widget.setup();
assert.equal(widget.value(), "null-value");
});
run_test("MDLW no_limit_set", () => {
const opts = {
widget_name: "my_setting",
data: ["one", "two", "three", "four"].map((x) => ({name: x, value: x})),
value: ["one"],
default_text: $t({defaultMessage: "not set"}),
};
blueslip.expect(
"warn",
"Multiselect dropdown-list-widget: Called without limit value; using 2 as the limit",
);
function set_dropdown_variables(widget, value) {
widget.data_selected = value;
widget.checked_items = value;
}
const {$widget} = setup_multiselect_dropdown_zjquery_data(opts.widget_name);
const widget = new MultiSelectDropdownListWidget(opts);
widget.setup();
set_dropdown_variables(widget, ["one", "two", "three"]);
widget.update(widget.data_selected);
// limit is set to 2 (Default value).
assert.equal($widget.text(), "translated: 3 selected");
set_dropdown_variables(widget, ["one"]);
widget.update(widget.data_selected);
assert.equal($widget.text(), "one");
});

View File

@ -318,11 +318,11 @@ run_test("get_realm_user_groups_for_dropdown_list_widget", () => {
};
const expected_groups_list = [
{name: "translated: Admins, moderators, members and guests", value: "6"},
{name: "translated: Admins, moderators and members", value: "5"},
{name: "translated: Admins, moderators and full members", value: "7"},
{name: "translated: Admins and moderators", value: "4"},
{name: "translated: Admins", value: "3"},
{name: "translated: Admins, moderators, members and guests", unique_id: 6},
{name: "translated: Admins, moderators and members", unique_id: 5},
{name: "translated: Admins, moderators and full members", unique_id: 7},
{name: "translated: Admins and moderators", unique_id: 4},
{name: "translated: Admins", unique_id: 3},
];
user_groups.initialize({