diff --git a/web/src/dropdown_list_widget.js b/web/src/dropdown_list_widget.js deleted file mode 100644 index df6820677d..0000000000 --- a/web/src/dropdown_list_widget.js +++ /dev/null @@ -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($("").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; - } -} diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index b260d8c215..aa54f786c1 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -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) { diff --git a/web/src/stream_settings_ui.js b/web/src/stream_settings_ui.js index 3def45dbf4..724a9bf032 100644 --- a/web/src/stream_settings_ui.js +++ b/web/src/stream_settings_ui.js @@ -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(); diff --git a/web/src/user_groups.ts b/web/src/user_groups.ts index 0cc5d58675..a3ec84d313 100644 --- a/web/src/user_groups.ts +++ b/web/src/user_groups.ts @@ -21,7 +21,7 @@ type UserGroupRaw = Omit & {members: number[]}; type UserGroupForDropdownListWidget = { name: string; - value: string; + unique_id: number; }; let user_group_name_dict: FoldDict; @@ -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]; diff --git a/web/templates/settings/dropdown_list_widget.hbs b/web/templates/settings/dropdown_list_widget.hbs deleted file mode 100644 index 52a3892661..0000000000 --- a/web/templates/settings/dropdown_list_widget.hbs +++ /dev/null @@ -1,32 +0,0 @@ - diff --git a/web/templates/stream_settings/stream_types.hbs b/web/templates/stream_settings/stream_types.hbs index 1766d7b567..bb3bfbac7d 100644 --- a/web/templates/stream_settings/stream_types.hbs +++ b/web/templates/stream_settings/stream_types.hbs @@ -29,14 +29,10 @@ -
- - {{> ../settings/dropdown_list_widget - widget_name=can_remove_subscribers_setting_widget_name - list_placeholder=(t 'Filter roles') - value_type="number" }} -
+{{> ../dropdown_widget_with_label + widget_name=can_remove_subscribers_setting_widget_name + label=(t 'Who can unsubscribe others from this stream?') + value_type="number"}} {{#if (or is_owner is_stream_edit)}}
diff --git a/web/tests/dropdown_list_widget.test.js b/web/tests/dropdown_list_widget.test.js deleted file mode 100644 index b9330d1c8e..0000000000 --- a/web/tests/dropdown_list_widget.test.js +++ /dev/null @@ -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"); -}); diff --git a/web/tests/user_groups.test.js b/web/tests/user_groups.test.js index 658a912aa1..9ba263f267 100644 --- a/web/tests/user_groups.test.js +++ b/web/tests/user_groups.test.js @@ -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({