diff --git a/tools/test-js-with-node b/tools/test-js-with-node index b2efca0b4c..130ed5f548 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -273,7 +273,7 @@ EXEMPT_FILES = make_set( "web/src/user_group_create.ts", "web/src/user_group_create_members.ts", "web/src/user_group_create_members_data.ts", - "web/src/user_group_edit.js", + "web/src/user_group_edit.ts", "web/src/user_group_edit_members.ts", "web/src/user_group_popover.ts", "web/src/user_groups.ts", diff --git a/web/src/hashchange.js b/web/src/hashchange.js index 20ffb6be88..c011b4b2ab 100644 --- a/web/src/hashchange.js +++ b/web/src/hashchange.js @@ -30,7 +30,7 @@ import * as spectators from "./spectators.ts"; import {current_user} from "./state_data.ts"; import * as stream_settings_ui from "./stream_settings_ui.ts"; import * as ui_report from "./ui_report.ts"; -import * as user_group_edit from "./user_group_edit.js"; +import * as user_group_edit from "./user_group_edit.ts"; import * as user_profile from "./user_profile.ts"; import {user_settings} from "./user_settings.ts"; diff --git a/web/src/list_widget.ts b/web/src/list_widget.ts index a36ecb356e..29f327064f 100644 --- a/web/src/list_widget.ts +++ b/web/src/list_widget.ts @@ -40,7 +40,7 @@ type ListWidgetOpts = { name?: string; get_item: (key: Key) => Item; modifier_html: (item: Item, filter_value: string) => string; - init_sort?: string | SortingFunction; + init_sort?: string | string[] | SortingFunction; initially_descending_sort?: boolean; html_selector?: (item: Item) => JQuery; callback_after_render?: () => void; @@ -70,7 +70,7 @@ export type ListWidget = BaseListWidget & { clear: () => void; set_filter_value: (value: string) => void; set_reverse_mode: (reverse_mode: boolean) => void; - set_sorting_function: (sorting_function: string | SortingFunction) => void; + set_sorting_function: (sorting_function: string | string[] | SortingFunction) => void; set_up_event_handlers: () => void; increase_rendered_offset: () => void; reduce_rendered_offset: () => void; diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 4c2a28fa95..04791276fd 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -87,7 +87,7 @@ import * as typing_events from "./typing_events.ts"; import * as unread_ops from "./unread_ops.ts"; import * as unread_ui from "./unread_ui.ts"; import * as user_events from "./user_events.js"; -import * as user_group_edit from "./user_group_edit.js"; +import * as user_group_edit from "./user_group_edit.ts"; import * as user_groups from "./user_groups.ts"; import {user_settings} from "./user_settings.ts"; import * as user_status from "./user_status.ts"; diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index 317c9ef97d..35c7babe89 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -1565,7 +1565,7 @@ export const group_setting_name_schema = z.enum([ "can_remove_members_group", ]); -type GroupSettingName = z.infer; +export type GroupSettingName = z.infer; export function create_group_setting_widget({ $pill_container, diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 54b9780567..cbac643e64 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -142,7 +142,7 @@ import * as unread_ops from "./unread_ops.ts"; import * as unread_ui from "./unread_ui.ts"; import * as upload from "./upload.ts"; import * as user_card_popover from "./user_card_popover.ts"; -import * as user_group_edit from "./user_group_edit.js"; +import * as user_group_edit from "./user_group_edit.ts"; import * as user_group_edit_members from "./user_group_edit_members.ts"; import * as user_group_popover from "./user_group_popover.ts"; import * as user_groups from "./user_groups.ts"; diff --git a/web/src/user_events.js b/web/src/user_events.js index bc1698b247..c51030594d 100644 --- a/web/src/user_events.js +++ b/web/src/user_events.js @@ -25,7 +25,7 @@ import * as settings_streams from "./settings_streams.ts"; import * as settings_users from "./settings_users.ts"; import {current_user, realm} from "./state_data.ts"; import * as stream_events from "./stream_events.ts"; -import * as user_group_edit from "./user_group_edit.js"; +import * as user_group_edit from "./user_group_edit.ts"; import * as user_profile from "./user_profile.ts"; export const update_person = function update(person) { diff --git a/web/src/user_group_edit.js b/web/src/user_group_edit.ts similarity index 75% rename from web/src/user_group_edit.js rename to web/src/user_group_edit.ts index 222665ec6d..355d4e1317 100644 --- a/web/src/user_group_edit.js +++ b/web/src/user_group_edit.ts @@ -1,4 +1,7 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; +import type * as tippy from "tippy.js"; +import {z} from "zod"; import render_confirm_delete_user from "../templates/confirm_dialog/confirm_delete_user.hbs"; import render_confirm_join_group_direct_member from "../templates/confirm_dialog/confirm_join_group_direct_member.hbs"; @@ -14,6 +17,7 @@ import * as blueslip from "./blueslip.ts"; import * as browser_history from "./browser_history.ts"; import * as channel from "./channel.ts"; import * as components from "./components.ts"; +import type {Toggle} from "./components.ts"; import * as compose_banner from "./compose_banner.ts"; import * as confirm_dialog from "./confirm_dialog.ts"; import * as dialog_widget from "./dialog_widget.ts"; @@ -25,6 +29,7 @@ import * as overlays from "./overlays.ts"; import * as people from "./people.ts"; import * as scroll_util from "./scroll_util.ts"; import * as settings_components from "./settings_components.ts"; +import {group_setting_name_schema} from "./settings_components.ts"; import * as settings_config from "./settings_config.ts"; import * as settings_data from "./settings_data.ts"; import * as settings_org from "./settings_org.ts"; @@ -36,23 +41,54 @@ import * as user_group_components from "./user_group_components.ts"; import * as user_group_create from "./user_group_create.ts"; import * as user_group_edit_members from "./user_group_edit_members.ts"; import * as user_groups from "./user_groups.ts"; +import type {UserGroup} from "./user_groups.ts"; import * as user_profile from "./user_profile.ts"; import * as util from "./util.ts"; -export let toggler; +type UserGroupPermissionData = + | number + | { + direct_members: number[]; + direct_subgroups: number[]; + }; + +type UserGroupUpdateEvent = { + id: number; + type: string; + group_id: number; + data: { + name: string; + description: string; + can_add_members_group: UserGroupPermissionData; + can_remove_members_group: UserGroupPermissionData; + can_join_group: UserGroupPermissionData; + can_leave_group: UserGroupPermissionData; + can_manage_group: UserGroupPermissionData; + can_mention_group: UserGroupPermissionData; + deactivated: boolean; + }; +}; + +type ActiveData = { + $row: JQuery | undefined; + id: number | undefined; + $tabs: JQuery; +}; + +export let toggler: Toggle; export let select_tab = "general"; -let group_list_widget; -let group_list_toggler; +let group_list_widget: ListWidget.ListWidget; +let group_list_toggler: Toggle; -function get_user_group_id(target) { +function get_user_group_id(target: HTMLElement): number { const $row = $(target).closest( ".group-row, .user_group_settings_wrapper, .save-button, .group_settings_header", ); - return Number.parseInt($row.attr("data-group-id"), 10); + return Number.parseInt($row.attr("data-group-id")!, 10); } -function get_user_group_for_target(target) { +function get_user_group_for_target(target: HTMLElement): UserGroup | undefined { const user_group_id = get_user_group_id(target); if (!user_group_id) { blueslip.error("Cannot find user group id for target"); @@ -67,19 +103,21 @@ function get_user_group_for_target(target) { return group; } -export function get_edit_container(group) { +export function get_edit_container(group: UserGroup): JQuery { return $( - `#groups_overlay .user_group_settings_wrapper[data-group-id='${CSS.escape(group.id)}']`, + `#groups_overlay .user_group_settings_wrapper[data-group-id='${CSS.escape(group.id.toString())}']`, ); } -function update_add_members_elements(group) { +function update_add_members_elements(group: UserGroup): void { if (!is_editing_group(group.id)) { return; } // We are only concerned with the Members tab for editing groups. - const $add_members_container = $(".edit_members_for_user_group .add_members_container"); + const $add_members_container = $( + ".edit_members_for_user_group .add_members_container", + ); if (current_user.is_guest || realm.realm_is_zephyr_mirror_realm) { // For guest users, we just hide the add_members feature. @@ -98,7 +136,7 @@ function update_add_members_elements(group) { $button_element.prop("disabled", false); } $button_element.css("pointer-events", ""); - $add_members_container[0]._tippy?.destroy(); + $add_members_container[0]?._tippy?.destroy(); $add_members_container.removeClass("add_members_disabled"); } else { $input_element.prop("contenteditable", false); @@ -112,7 +150,7 @@ function update_add_members_elements(group) { } } -function update_group_permission_settings_elements(group) { +function update_group_permission_settings_elements(group: UserGroup): void { if (!is_editing_group(group.id)) { return; } @@ -127,8 +165,8 @@ function update_group_permission_settings_elements(group) { $permission_pill_container_elements.find(".input").prop("contenteditable", true); $permission_input_groups.removeClass("group_setting_disabled"); - $permission_input_groups.each(function () { - $(this)[0]._tippy?.destroy(); + $permission_input_groups.each(function (this: tippy.ReferenceElement) { + $(this)[0]?._tippy?.destroy(); }); settings_components.enable_opening_typeahead_on_clicking_label($group_permission_settings); } else { @@ -142,7 +180,7 @@ function update_group_permission_settings_elements(group) { } } -function show_membership_settings(group) { +function show_membership_settings(group: UserGroup): void { const $edit_container = get_edit_container(group); const $member_container = $edit_container.find(".edit_members_for_user_group"); @@ -154,8 +192,10 @@ function show_membership_settings(group) { update_members_panel_ui(group); } -function show_general_settings(group) { - const permission_settings = Object.keys(realm.server_supported_permission_settings.group); +function show_general_settings(group: UserGroup): void { + const permission_settings = z + .array(group_setting_name_schema) + .parse(Object.keys(realm.server_supported_permission_settings.group)); for (const setting_name of permission_settings) { settings_components.create_group_setting_widget({ $pill_container: $(`#id_${CSS.escape(setting_name)}`), @@ -167,21 +207,25 @@ function show_general_settings(group) { update_general_panel_ui(group); } -function update_general_panel_ui(group) { +function update_general_panel_ui(group: UserGroup): void { const $edit_container = get_edit_container(group); if (settings_data.can_manage_user_group(group.id)) { $edit_container.find(".group-header .button-group").show(); - $(`.group_settings_header[data-group-id='${CSS.escape(group.id)}'] .deactivate`).show(); + $( + `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .deactivate`, + ).show(); } else { $edit_container.find(".group-header .button-group").hide(); - $(`.group_settings_header[data-group-id='${CSS.escape(group.id)}'] .deactivate`).hide(); + $( + `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .deactivate`, + ).hide(); } update_group_permission_settings_elements(group); update_group_membership_button(group.id); } -function update_members_panel_ui(group) { +function update_members_panel_ui(group: UserGroup): void { const $edit_container = get_edit_container(group); const $member_container = $edit_container.find(".edit_members_for_user_group"); @@ -192,7 +236,7 @@ function update_members_panel_ui(group) { update_add_members_elements(group); } -export function update_group_management_ui() { +export function update_group_management_ui(): void { if (!overlays.groups_open()) { return; } @@ -209,11 +253,13 @@ export function update_group_management_ui() { update_members_panel_ui(group); } -function group_membership_button(group_id) { - return $(`.group_settings_header[data-group-id='${CSS.escape(group_id)}'] .join_leave_button`); +function group_membership_button(group_id: number): JQuery { + return $( + `.group_settings_header[data-group-id='${CSS.escape(group_id.toString())}'] .join_leave_button`, + ); } -function initialize_tooltip_for_membership_button(group_id) { +function initialize_tooltip_for_membership_button(group_id: number): void { const $tooltip_wrapper = group_membership_button(group_id).closest( ".join_leave_button_wrapper", ); @@ -228,7 +274,7 @@ function initialize_tooltip_for_membership_button(group_id) { } // Group membership button only adds or removes direct membership. -function update_group_membership_button(group_id) { +function update_group_membership_button(group_id: number): void { const $group_settings_button = group_membership_button(group_id); if (!$group_settings_button.length) { @@ -259,25 +305,24 @@ function update_group_membership_button(group_id) { if (can_update_membership) { $group_settings_button.prop("disabled", false); $group_settings_button.css("pointer-events", ""); - const $group_settings_button_wrapper = $group_settings_button.closest( - ".join_leave_button_wrapper", - ); - $group_settings_button_wrapper[0]._tippy?.destroy(); + const $group_settings_button_wrapper: JQuery = + $group_settings_button.closest(".join_leave_button_wrapper"); + $group_settings_button_wrapper[0]?._tippy?.destroy(); } else { $group_settings_button.prop("disabled", true); initialize_tooltip_for_membership_button(group_id); } } -function update_display_checkmark_on_group_edit(group) { +function update_display_checkmark_on_group_edit(group: UserGroup): void { if (is_group_already_present(group)) { const $row = row_for_group_id(group.id); const item = group; - item.is_member = user_groups.is_user_in_group(group.id, people.my_current_user_id()); - item.can_join = settings_data.can_join_user_group(item.id); - item.can_leave = settings_data.can_leave_user_group(item.id); - item.is_direct_member = user_groups.is_direct_member_of( + const is_member = user_groups.is_user_in_group(group.id, people.my_current_user_id()); + const can_join = settings_data.can_join_user_group(item.id); + const can_leave = settings_data.can_leave_user_group(item.id); + const is_direct_member = user_groups.is_direct_member_of( people.my_current_user_id(), item.id, ); @@ -285,8 +330,16 @@ function update_display_checkmark_on_group_edit(group) { item, people.my_current_user_id(), ); - item.associated_subgroup_names = user_groups.format_group_list(associated_subgroups); - const html = render_browse_user_groups_list_item(item); + const associated_subgroup_names = user_groups.format_group_list(associated_subgroups); + const item_render_data = { + ...item, + is_member, + can_join, + can_leave, + is_direct_member, + associated_subgroup_names, + }; + const html = render_browse_user_groups_list_item(item_render_data); const $new_row = $(html); // TODO: Remove this if/when we just handle "active" when rendering templates. @@ -298,7 +351,7 @@ function update_display_checkmark_on_group_edit(group) { } } -function update_your_groups_list_if_needed(group_id) { +function update_your_groups_list_if_needed(group_id: number): void { // update display of group-rows on left panel. // We need this update only if your-groups tab is active // and current user is among the affect users as in that @@ -323,7 +376,7 @@ function update_your_groups_list_if_needed(group_id) { } } -export function handle_subgroup_edit_event(group_id, direct_subgroup_ids) { +export function handle_subgroup_edit_event(group_id: number, direct_subgroup_ids: number[]): void { if (!overlays.groups_open()) { return; } @@ -365,7 +418,7 @@ export function handle_subgroup_edit_event(group_id, direct_subgroup_ids) { } } -function update_status_text_on_member_update(updated_group) { +function update_status_text_on_member_update(updated_group: UserGroup): void { const active_group_id = get_active_data().id; if (active_group_id === undefined) { return; @@ -397,7 +450,7 @@ function update_status_text_on_member_update(updated_group) { update_membership_status_text(active_group); } -function update_settings_for_group_overlay(group_id, user_ids) { +function update_settings_for_group_overlay(group_id: number, user_ids: number[]): void { const group = user_groups.get_user_group_from_id(group_id); // update members list if currently rendered. @@ -421,24 +474,28 @@ function update_settings_for_group_overlay(group_id, user_ids) { } } -export function handle_member_edit_event(group_id, user_ids) { +export function handle_member_edit_event(group_id: number, user_ids: number[]): void { if (overlays.groups_open()) { update_settings_for_group_overlay(group_id, user_ids); } user_profile.update_user_profile_groups_list_for_users(user_ids); } -export function update_group_details(group) { +export function update_group_details(group: UserGroup): void { const $edit_container = get_edit_container(group); $edit_container.find(".group-name").text(group.name); $edit_container.find(".group-description").text(group.description); } -function update_toggler_for_group_setting() { +function update_toggler_for_group_setting(): void { toggler.goto(select_tab); } -function get_membership_status_context(group) { +function get_membership_status_context(group: UserGroup): { + is_direct_member: boolean; + is_member: boolean; + associated_subgroup_names_html: string | undefined; +} { const current_user_id = people.my_current_user_id(); const is_direct_member = user_groups.is_direct_member_of(current_user_id, group.id); @@ -467,19 +524,19 @@ function get_membership_status_context(group) { }; } -function update_membership_status_text(group) { +function update_membership_status_text(group: UserGroup): void { const args = get_membership_status_context(group); const rendered_membership_status = render_user_group_membership_status(args); const $edit_container = get_edit_container(group); $edit_container.find(".membership-status").html(rendered_membership_status); } -export function show_settings_for(group) { +export function show_settings_for(group: UserGroup): void { const html = render_user_group_settings({ group, // We get timestamp in seconds from the API but timerender needs milliseconds. date_created_string: timerender.get_localized_date_or_time_for_format( - new Date(group.date_created * 1000), + new Date((group.date_created ?? 0) * 1000), "dayofyear_year", ), creator: stream_data.maybe_get_creator_details(group.creator_id), @@ -499,7 +556,7 @@ export function show_settings_for(group) { show_general_settings(group); } -export function setup_group_settings(group) { +export function setup_group_settings(group: UserGroup): void { toggler = components.toggle({ child_wants_focus: true, values: [ @@ -518,7 +575,7 @@ export function setup_group_settings(group) { show_settings_for(group); } -export function setup_group_list_tab_hash(tab_key_value) { +export function setup_group_list_tab_hash(tab_key_value: string): void { /* We do not update the hash based on tab switches if a group is currently being edited. @@ -536,55 +593,56 @@ export function setup_group_list_tab_hash(tab_key_value) { } } -function display_membership_toggle_spinner(group_row) { +function display_membership_toggle_spinner($group_row: JQuery): void { /* Prevent sending multiple requests by removing the button class. */ - $(group_row).find(".check").removeClass("join_leave_button"); + $group_row.find(".check").removeClass("join_leave_button"); /* Hide the tick. */ - const $tick = $(group_row).find("svg"); + const $tick = $group_row.find("svg"); $tick.addClass("hide"); /* Add a spinner to show the request is in process. */ - const $spinner = $(group_row).find(".join_leave_status").expectOne(); + const $spinner = $group_row.find(".join_leave_status").expectOne(); $spinner.show(); loading.make_indicator($spinner); } -function hide_membership_toggle_spinner(group_row) { +function hide_membership_toggle_spinner($group_row: JQuery): void { /* Re-enable the button to handle requests. */ - $(group_row).find(".check").addClass("join_leave_button"); + $group_row.find(".check").addClass("join_leave_button"); /* Show the tick. */ - const $tick = $(group_row).find("svg"); + const $tick = $group_row.find("svg"); $tick.removeClass("hide"); /* Destroy the spinner. */ - const $spinner = $(group_row).find(".join_leave_status").expectOne(); + const $spinner = $group_row.find(".join_leave_status").expectOne(); loading.destroy_indicator($spinner); } -function empty_right_panel() { +function empty_right_panel(): void { $(".group-row.active").removeClass("active"); user_group_components.show_user_group_settings_pane.nothing_selected(); } -function open_right_panel_empty() { +function open_right_panel_empty(): void { empty_right_panel(); const tab_key = $(".user-groups-container") .find("div.ind-tab.selected") .first() .attr("data-tab-key"); + assert(tab_key !== undefined); setup_group_list_tab_hash(tab_key); } -export function is_editing_group(desired_group_id) { +export function is_editing_group(desired_group_id: number): boolean { if (!overlays.groups_open()) { return false; } return get_active_data().id === desired_group_id; } -export function handle_deleted_group(group_id) { +export function handle_deleted_group(group_id: number): void { if (!overlays.groups_open()) { return; } @@ -595,15 +653,18 @@ export function handle_deleted_group(group_id) { redraw_user_group_list(); } -export function show_group_settings(group) { +export function show_group_settings(group: UserGroup): void { $(".group-row.active").removeClass("active"); user_group_components.show_user_group_settings_pane.settings(group); row_for_group_id(group.id).addClass("active"); setup_group_settings(group); } -export function open_group_edit_panel_for_row(group_row) { +export function open_group_edit_panel_for_row(group_row: HTMLElement): void { const group = get_user_group_for_target(group_row); + if (group === undefined) { + return; + } show_group_settings(group); } @@ -612,7 +673,7 @@ export function open_group_edit_panel_for_row(group_row) { // `realm.max_stream_description_length` for streams. export const max_user_group_name_length = 100; -export function set_up_click_handlers() { +export function set_up_click_handlers(): void { $("#groups_overlay").on("click", ".left #clear_search_group_name", (e) => { const $input = $("#groups_overlay .left #search_group_name"); $input.val(""); @@ -626,7 +687,7 @@ export function set_up_click_handlers() { }); } -function create_user_group_clicked() { +function create_user_group_clicked(): void { // this changes the tab switcher (settings/preview) which isn't necessary // to a add new stream title. user_group_components.show_user_group_settings_pane.create_user_group(); @@ -636,36 +697,41 @@ function create_user_group_clicked() { $("#create_user_group_name").trigger("focus"); } -export function do_open_create_user_group() { +export function do_open_create_user_group(): void { // Only call this directly for hash changes. // Prefer open_create_user_group(). show_right_section(); create_user_group_clicked(); } -export function open_create_user_group() { +export function open_create_user_group(): void { do_open_create_user_group(); browser_history.update("#groups/new"); } -export function row_for_group_id(group_id) { - return $(`.group-row[data-group-id='${CSS.escape(group_id)}']`); +export function row_for_group_id(group_id: number): JQuery { + return $(`.group-row[data-group-id='${CSS.escape(group_id.toString())}']`); } -export function is_group_already_present(group) { +export function is_group_already_present(group: UserGroup): boolean { return row_for_group_id(group.id).length > 0; } -export function get_active_data() { +export function get_active_data(): ActiveData { const $active_tabs = $(".user-groups-container").find("div.ind-tab.selected"); + const active_group_id = user_group_components.active_group_id; + let $row; + if (active_group_id !== undefined) { + $row = row_for_group_id(active_group_id); + } return { - $row: row_for_group_id(user_group_components.active_group_id), + $row, id: user_group_components.active_group_id, $tabs: $active_tabs, }; } -export function switch_to_group_row(group) { +export function switch_to_group_row(group: UserGroup): void { if (is_group_already_present(group)) { /* It is possible that this function may be called at times @@ -681,7 +747,7 @@ export function switch_to_group_row(group) { const $group_row = row_for_group_id(group.id); const $container = $(".user-groups-list"); - get_active_data().$row.removeClass("active"); + get_active_data().$row?.removeClass("active"); $group_row.addClass("active"); scroll_util.scroll_element_into_container($group_row, $container); @@ -690,12 +756,12 @@ export function switch_to_group_row(group) { show_group_settings(group); } -function show_right_section() { +function show_right_section(): void { $(".right").addClass("show"); $(".user-groups-header").addClass("slide-left"); } -export function add_group_to_table(group) { +export function add_group_to_table(group: UserGroup): void { if (is_group_already_present(group)) { // If a group is already listed/added in groups modal, // then we simply return. @@ -718,7 +784,7 @@ export function add_group_to_table(group) { } } -export function sync_group_permission_setting(property, group) { +export function sync_group_permission_setting(property: string, group: UserGroup): void { const $elem = $(`#id_${CSS.escape(property)}`); const $subsection = $elem.closest(".settings-subsection-parent"); if ($subsection.find(".save-button-controls").hasClass("hide")) { @@ -728,7 +794,7 @@ export function sync_group_permission_setting(property, group) { } } -export function update_group(event) { +export function update_group(event: UserGroupUpdateEvent): void { if (!overlays.groups_open()) { return; } @@ -785,7 +851,11 @@ export function update_group(event) { } } -export function change_state(section, left_side_tab, right_side_tab) { +export function change_state( + section: string, + left_side_tab: string | undefined, + right_side_tab: string, +): void { if (section === "new") { do_open_create_user_group(); redraw_user_group_list(); @@ -827,29 +897,33 @@ export function change_state(section, left_side_tab, right_side_tab) { empty_right_panel(); } -function compare_by_name(a, b) { +function compare_by_name(a: UserGroup, b: UserGroup): number { return util.strcmp(a.name, b.name); } -function redraw_left_panel(tab_name) { +function redraw_left_panel(tab_name: string): void { let groups_list_data; if (tab_name === "all-groups") { groups_list_data = user_groups.get_realm_user_groups(); } else if (tab_name === "your-groups") { groups_list_data = user_groups.get_user_groups_of_user(people.my_current_user_id()); } + if (groups_list_data === undefined) { + return; + } groups_list_data.sort(compare_by_name); group_list_widget.replace_list_data(groups_list_data); update_empty_left_panel_message(); maybe_reset_right_panel(groups_list_data); } -export function redraw_user_group_list() { +export function redraw_user_group_list(): void { const tab_name = get_active_data().$tabs.first().attr("data-tab-key"); + assert(tab_name !== undefined); redraw_left_panel(tab_name); } -export function switch_group_tab(tab_name) { +export function switch_group_tab(tab_name: string): void { /* This switches the groups list tab, but it doesn't update the group_list_toggler widget. You may instead want to @@ -859,11 +933,11 @@ export function switch_group_tab(tab_name) { setup_group_list_tab_hash(tab_name); } -export function add_or_remove_from_group(group, group_row) { +export function add_or_remove_from_group(group: UserGroup, $group_row: JQuery): void { const user_id = people.my_current_user_id(); - function success_callback() { - if (group_row.length) { - hide_membership_toggle_spinner(group_row); + function success_callback(): void { + if ($group_row.length) { + hide_membership_toggle_spinner($group_row); // This should only be triggered when a user is on another group // edit panel and they join a group via the left panel plus button. // In that case, the edit panel of the newly joined group should @@ -873,19 +947,19 @@ export function add_or_remove_from_group(group, group_row) { !is_editing_group(group.id) && user_groups.is_user_in_group(group.id, user_id, true) ) { - open_group_edit_panel_for_row(group_row); + open_group_edit_panel_for_row(util.the($group_row)); } } } - function error_callback() { - if (group_row.length) { - hide_membership_toggle_spinner(group_row); + function error_callback(): void { + if ($group_row.length) { + hide_membership_toggle_spinner($group_row); } } - if (group_row.length) { - display_membership_toggle_spinner(group_row); + if ($group_row.length) { + display_membership_toggle_spinner($group_row); } if (user_groups.is_direct_member_of(user_id, group.id)) { user_group_edit_members.edit_user_group_membership({ @@ -904,7 +978,7 @@ export function add_or_remove_from_group(group, group_row) { } } -export function maybe_reset_right_panel(groups_list_data) { +export function maybe_reset_right_panel(groups_list_data: UserGroup[]): void { if (user_group_components.active_group_id === undefined) { return; } @@ -915,7 +989,7 @@ export function maybe_reset_right_panel(groups_list_data) { } } -export function update_empty_left_panel_message() { +export function update_empty_left_panel_message(): void { // Check if we have any groups in panel to decide whether to // display a notice. let has_groups; @@ -940,7 +1014,7 @@ export function update_empty_left_panel_message() { $(".no-groups-to-show").show(); } -export function remove_deactivated_user_from_all_groups(user_id) { +export function remove_deactivated_user_from_all_groups(user_id: number): void { const all_user_groups = user_groups.get_realm_user_groups(true); for (const user_group of all_user_groups) { @@ -955,8 +1029,8 @@ export function remove_deactivated_user_from_all_groups(user_id) { } } -export function setup_page(callback) { - function initialize_components() { +export function setup_page(callback: () => void): void { + function initialize_components(): void { group_list_toggler = components.toggle({ child_wants_focus: true, values: [ @@ -971,7 +1045,7 @@ export function setup_page(callback) { group_list_toggler.get().prependTo("#groups_overlay_container .list-toggler-container"); } - function populate_and_fill() { + function populate_and_fill(): void { const template_data = { can_create_user_groups: settings_data.user_can_create_user_groups(), zulip_plan_is_not_limited: realm.zulip_plan_is_not_limited, @@ -1013,12 +1087,16 @@ export function setup_page(callback) { immediately after this due to call to change_state. So we call `ListWidget.create` with empty user groups list. */ - group_list_widget = ListWidget.create($container, [], { + const empty_user_group_list: UserGroup[] = []; + group_list_widget = ListWidget.create($container, empty_user_group_list, { name: "user-groups-overlay", get_item: ListWidget.default_get_item, modifier_html(item) { - item.is_member = user_groups.is_user_in_group(item.id, people.my_current_user_id()); - item.is_direct_member = user_groups.is_direct_member_of( + const is_member = user_groups.is_user_in_group( + item.id, + people.my_current_user_id(), + ); + const is_direct_member = user_groups.is_direct_member_of( people.my_current_user_id(), item.id, ); @@ -1026,11 +1104,19 @@ export function setup_page(callback) { item, people.my_current_user_id(), ); - item.associated_subgroup_names = + const associated_subgroup_names = user_groups.format_group_list(associated_subgroups); - item.can_join = settings_data.can_join_user_group(item.id); - item.can_leave = settings_data.can_leave_user_group(item.id); - return render_browse_user_groups_list_item(item); + const can_join = settings_data.can_join_user_group(item.id); + const can_leave = settings_data.can_leave_user_group(item.id); + const item_render_data = { + ...item, + is_member, + is_direct_member, + associated_subgroup_names, + can_join, + can_leave, + }; + return render_browse_user_groups_list_item(item_render_data); }, filter: { $element: $("#groups_overlay_container .left #search_group_name"), @@ -1074,8 +1160,22 @@ export function setup_page(callback) { populate_and_fill(); } -function parse_args_for_deactivation_banner(objections) { - const args = { +type DeactivationBannerArgs = { + streams_using_group_for_setting: { + stream_name: string; + setting_url: string | undefined; + }[]; + groups_using_group_for_setting: { + group_name: string; + setting_url: string; + }[]; + realm_using_group_for_setting: boolean; +}; + +function parse_args_for_deactivation_banner( + objections: Record[], +): DeactivationBannerArgs { + const args: DeactivationBannerArgs = { streams_using_group_for_setting: [], groups_using_group_for_setting: [], realm_using_group_for_setting: false, @@ -1083,6 +1183,7 @@ function parse_args_for_deactivation_banner(objections) { for (const objection of objections) { if (objection.type === "channel") { const stream_id = objection.channel_id; + assert(typeof stream_id === "number"); const sub = stream_data.get_sub_by_id(stream_id); if (sub !== undefined) { args.streams_using_group_for_setting.push({ @@ -1100,6 +1201,7 @@ function parse_args_for_deactivation_banner(objections) { if (objection.type === "user_group") { const group_id = objection.group_id; + assert(typeof group_id === "number"); const group = user_groups.get_user_group_from_id(group_id); const setting_url = hash_util.group_edit_url(group, "general"); args.groups_using_group_for_setting.push({group_name: group.name, setting_url}); @@ -1113,66 +1215,79 @@ function parse_args_for_deactivation_banner(objections) { return args; } -export function initialize() { - $("#groups_overlay_container").on("click", ".group-row", function (e) { - if ($(e.target).closest(".check, .user_group_settings_wrapper").length === 0) { +export function initialize(): void { + $("#groups_overlay_container").on("click", ".group-row", function (this: HTMLElement) { + if ($(this).closest(".check, .user_group_settings_wrapper").length === 0) { open_group_edit_panel_for_row(this); } }); - $("#groups_overlay_container").on("click", "#open_group_info_modal", (e) => { - e.preventDefault(); - e.stopPropagation(); - const user_group_id = get_user_group_id(e.target); - const user_group = user_groups.get_user_group_from_id(user_group_id); - const template_data = { - group_name: user_group.name, - group_description: user_group.description, - max_user_group_name_length, - }; - const change_user_group_info_modal = render_change_user_group_info_modal(template_data); - dialog_widget.launch({ - html_heading: $t_html( - {defaultMessage: "Edit {group_name}"}, - {group_name: user_group.name}, - ), - html_body: change_user_group_info_modal, - id: "change_group_info_modal", - loading_spinner: true, - on_click: save_group_info, - post_render() { - $("#change_group_info_modal .dialog_submit_button") - .addClass("save-button") - .attr("data-group-id", user_group_id); - }, - update_submit_disabled_state_on_change: true, - }); - }); + $("#groups_overlay_container").on( + "click", + "#open_group_info_modal", + function (this: HTMLElement, e) { + e.preventDefault(); + e.stopPropagation(); + const user_group_id = get_user_group_id(this); + const user_group = user_groups.get_user_group_from_id(user_group_id); + const template_data = { + group_name: user_group.name, + group_description: user_group.description, + max_user_group_name_length, + }; + const change_user_group_info_modal = render_change_user_group_info_modal(template_data); + dialog_widget.launch({ + html_heading: $t_html( + {defaultMessage: "Edit {group_name}"}, + {group_name: user_group.name}, + ), + html_body: change_user_group_info_modal, + id: "change_group_info_modal", + loading_spinner: true, + on_click: save_group_info, + post_render() { + $("#change_group_info_modal .dialog_submit_button") + .addClass("save-button") + .attr("data-group-id", user_group_id); + }, + update_submit_disabled_state_on_change: true, + }); + }, + ); $("#groups_overlay_container").on("click", ".group_settings_header .button-danger", () => { const active_group_data = get_active_data(); const group_id = active_group_data.id; + assert(group_id !== undefined); const user_group = user_groups.get_user_group_from_id(group_id); if (!user_group || !settings_data.can_manage_user_group(group_id)) { return; } - function deactivate_user_group() { + function deactivate_user_group(): void { channel.post({ url: "/json/user_groups/" + group_id + "/deactivate", data: {}, success() { dialog_widget.close(); - active_group_data.$row.remove(); + active_group_data.$row?.remove(); }, error(xhr) { dialog_widget.hide_dialog_spinner(); - if (xhr.responseJSON.code === "CANNOT_DEACTIVATE_GROUP_IN_USE") { + const parsed = z + .object({ + code: z.string(), + msg: z.string(), + objections: z.array(z.record(z.string(), z.unknown())), + result: z.string(), + }) + .safeParse(xhr.responseJSON); + if (parsed.success && parsed.data.code === "CANNOT_DEACTIVATE_GROUP_IN_USE") { $("#deactivation-confirm-modal .dialog_submit_button").prop( "disabled", true, ); - const objections = xhr.responseJSON.objections; + const objections = parsed.data.objections; const template_args = parse_args_for_deactivation_banner(objections); const rendered_error_banner = render_cannot_deactivate_group_banner(template_args); @@ -1206,14 +1321,14 @@ export function initialize() { }); }); - function save_group_info(e) { - const group = get_user_group_for_target(e.currentTarget); - + function save_group_info(this: HTMLElement): void { + const group = get_user_group_for_target(this); + assert(group !== undefined); const url = `/json/user_groups/${group.id}`; let name; let description; - const new_name = $("#change_user_group_name").val().trim(); - const new_description = $("#change_user_group_description").val().trim(); + const new_name = $("#change_user_group_name").val()!.trim(); + const new_description = $("#change_user_group_description").val()!.trim(); if (new_name !== group.name) { name = new_name; @@ -1250,16 +1365,13 @@ export function initialize() { $(".user-groups-header").removeClass("slide-left"); }); - $("#groups_overlay_container").on("click", ".join_leave_button", (e) => { - if ( - $(e.currentTarget).hasClass("disabled") || - $(e.currentTarget).hasClass("not-direct-member") - ) { + $("#groups_overlay_container").on("click", ".join_leave_button", function (this: HTMLElement) { + if ($(this).hasClass("disabled") || $(this).hasClass("not-direct-member")) { // We return early if user is not allowed to join or leave a group. return; } - const user_group_id = get_user_group_id(e.target); + const user_group_id = get_user_group_id(this); const user_group = user_groups.get_user_group_from_id(user_group_id); const is_member = user_groups.is_user_in_group(user_group_id, people.my_current_user_id()); const is_direct_member = user_groups.is_direct_member_of( @@ -1294,13 +1406,16 @@ export function initialize() { $("#groups_overlay_container").on( "click", ".subsection-header .subsection-changes-save button", - (e) => { + function (this: HTMLElement, e) { e.preventDefault(); e.stopPropagation(); - const $save_button = $(e.currentTarget); + const $save_button = $(this); const $subsection_elem = $save_button.closest(".settings-subsection-parent"); - const group_id = $save_button.closest(".user_group_settings_wrapper").data("group-id"); + const group_id: unknown = $save_button + .closest(".user_group_settings_wrapper") + .data("group-id"); + assert(typeof group_id === "number"); const group = user_groups.get_user_group_from_id(group_id); const data = settings_components.populate_data_for_group_request( $subsection_elem, @@ -1315,20 +1430,27 @@ export function initialize() { $("#groups_overlay_container").on( "click", ".subsection-header .subsection-changes-discard button", - (e) => { + function (this: HTMLElement, e) { e.preventDefault(); e.stopPropagation(); - const group_id = $(e.target).closest(".user_group_settings_wrapper").data("group-id"); + const group_id: unknown = $(this) + .closest(".user_group_settings_wrapper") + .data("group-id"); + assert(typeof group_id === "number"); const group = user_groups.get_user_group_from_id(group_id); - const $subsection = $(e.target).closest(".settings-subsection-parent"); + const $subsection = $(this).closest(".settings-subsection-parent"); settings_org.discard_group_settings_subsection_changes($subsection, group); }, ); } -export function launch(section, left_side_tab, right_side_tab) { +export function launch( + section: string, + left_side_tab: string | undefined, + right_side_tab: string, +): void { setup_page(() => { overlays.open_overlay({ name: "group_subscriptions",