diff --git a/help/deactivate-or-reactivate-a-user.md b/help/deactivate-or-reactivate-a-user.md index 7dd68d156d..674935d898 100644 --- a/help/deactivate-or-reactivate-a-user.md +++ b/help/deactivate-or-reactivate-a-user.md @@ -80,7 +80,7 @@ bots will be deactivated until the user manually {tab|via-organization-settings} -{settings_tab|deactivated-users-admin} +{settings_tab|deactivated} 1. Click the **Reactivate** button to the right of the user account that you want to reactivate. diff --git a/help/invite-new-users.md b/help/invite-new-users.md index 050fe6ec1e..18181c8f7b 100644 --- a/help/invite-new-users.md +++ b/help/invite-new-users.md @@ -84,7 +84,7 @@ for invitations for the organization owners role. {start_tabs} -{settings_tab|invites-list-admin} +{settings_tab|invitations} 1. From there, you can view pending invitations, **Revoke** email invitations and invitation links, or **Resend** email invitations. diff --git a/web/e2e-tests/user-deactivation.test.ts b/web/e2e-tests/user-deactivation.test.ts index 912f345e5f..543a791970 100644 --- a/web/e2e-tests/user-deactivation.test.ts +++ b/web/e2e-tests/user-deactivation.test.ts @@ -15,6 +15,7 @@ async function navigate_to_user_list(page: Page): Promise { await page.waitForSelector("#settings_overlay_container.show", {visible: true}); await page.click("li[data-section='users']"); + await page.waitForSelector("#admin-user-list.show", {visible: true}); } async function user_row(page: Page, name: string): Promise { @@ -80,7 +81,8 @@ async function test_deactivated_users_section(page: Page): Promise { // "Deactivated users" section doesn't render just deactivated users until reloaded. await page.reload(); - const deactivated_users_section = "li[data-section='deactivated-users-admin']"; + await page.waitForSelector("#admin-user-list.show", {visible: true}); + const deactivated_users_section = ".tab-container .ind-tab[data-tab-key='deactivated']"; await page.waitForSelector(deactivated_users_section, {visible: true}); await page.click(deactivated_users_section); diff --git a/web/src/admin.js b/web/src/admin.js index 47e655bea3..dfd1ec54fa 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -80,11 +80,12 @@ function insert_tip_box() { is_admin: current_user.is_admin, }); $(".organization-box") - .find(".settings-section") + .find(".settings-section, .user-settings-section") .not("#emoji-settings") .not("#organization-auth-settings") .not("#admin-bot-list") .not("#admin-invites-list") + .not("#admin-user-list") .prepend($(tip_box_html)); } @@ -276,12 +277,15 @@ export function build_page() { } } -export function launch(section) { +export function launch(section, user_settings_tab) { settings_sections.reset_sections(); settings.open_settings_overlay(); if (section !== "") { settings_panel_menu.org_settings.set_current_tab(section); } + if (section === "users") { + settings_panel_menu.org_settings.set_user_settings_tab(user_settings_tab); + } settings_toggle.goto("organization"); } diff --git a/web/src/hashchange.js b/web/src/hashchange.js index 7a3649fd2c..31f4cfa9d4 100644 --- a/web/src/hashchange.js +++ b/web/src/hashchange.js @@ -72,6 +72,24 @@ function is_somebody_else_profile_open() { ); } +function handle_invalid_users_section_url(user_settings_tab) { + const valid_user_settings_tab_values = new Set(["active", "deactivated", "invitations"]); + if (!valid_user_settings_tab_values.has(user_settings_tab)) { + const valid_users_section_url = "#organization/users/active"; + browser_history.update(valid_users_section_url); + return "active"; + } + return user_settings_tab; +} + +function get_user_settings_tab(section) { + if (section === "users") { + const current_user_settings_tab = hash_parser.get_current_nth_hash_section(2); + return handle_invalid_users_section_url(current_user_settings_tab); + } + return undefined; +} + export function set_hash_to_home_view(triggered_by_escape_key = false) { const current_hash = window.location.hash; if (current_hash === "") { @@ -352,7 +370,10 @@ function do_hashchange_overlay(old_hash) { // hand-typed a hash. blueslip.warn("missing section for organization"); } - settings_panel_menu.org_settings.activate_section_or_default(section); + settings_panel_menu.org_settings.activate_section_or_default( + section, + get_user_settings_tab(section), + ); return; } @@ -373,6 +394,7 @@ function do_hashchange_overlay(old_hash) { settings_panel_menu.normal_settings.set_current_tab(section); } else { settings_panel_menu.org_settings.set_current_tab(section); + settings_panel_menu.org_settings.set_user_settings_tab(get_user_settings_tab(section)); } settings_toggle.goto(base); return; @@ -440,7 +462,7 @@ function do_hashchange_overlay(old_hash) { if (base === "organization") { settings.build_page(); admin.build_page(); - admin.launch(section); + admin.launch(section, get_user_settings_tab(section)); return; } diff --git a/web/src/settings.js b/web/src/settings.js index d89097de37..8fdd7427c8 100644 --- a/web/src/settings.js +++ b/web/src/settings.js @@ -175,6 +175,10 @@ export function initialize() { show_uploaded_files_section: realm.max_file_upload_size_mib > 0, show_emoji_settings_lock: !settings_data.user_can_add_custom_emoji(), can_create_new_bots: settings_bots.can_create_new_bots(), + can_edit_user_panel: + current_user.is_admin || + settings_data.user_can_create_multiuse_invite() || + settings_data.user_can_invite_users_by_email(), }); $("#settings_overlay_container").append($(rendered_settings_overlay)); diff --git a/web/src/settings_panel_menu.js b/web/src/settings_panel_menu.js index 9edd34dddb..54a8041627 100644 --- a/web/src/settings_panel_menu.js +++ b/web/src/settings_panel_menu.js @@ -2,7 +2,8 @@ import $ from "jquery"; import * as blueslip from "./blueslip"; import * as browser_history from "./browser_history"; -import {$t_html} from "./i18n"; +import * as components from "./components"; +import {$t, $t_html} from "./i18n"; import * as keydown_util from "./keydown_util"; import * as popovers from "./popovers"; import * as scroll_util from "./scroll_util"; @@ -50,12 +51,30 @@ export class SettingsPanelMenu { this.hash_prefix = opts.hash_prefix; this.$curr_li = this.$main_elem.children("li").eq(0); this.current_tab = this.$curr_li.data("section"); + this.current_user_settings_tab = "active"; + this.org_user_settings_toggler = components.toggle({ + html_class: "org-user-settings-switcher", + child_wants_focus: true, + values: [ + {label: $t({defaultMessage: "Users"}), key: "active"}, + { + label: $t({defaultMessage: "Deactivated users"}), + key: "deactivated", + }, + {label: $t({defaultMessage: "Invitations"}), key: "invitations"}, + ], + callback: (_name, key) => { + browser_history.update(`#organization/users/${key}`); + this.set_user_settings_tab(key); + $(".user-settings-section").hide(); + $(`[data-user-settings-section="${CSS.escape(key)}"]`).show(); + }, + }); this.$main_elem.on("click", "li[data-section]", (e) => { const section = $(e.currentTarget).attr("data-section"); - this.activate_section_or_default(section); - + this.activate_section_or_default(section, this.current_user_settings_tab); // You generally want to add logic to activate_section, // not to this click handler. @@ -66,13 +85,26 @@ export class SettingsPanelMenu { show() { this.$main_elem.show(); const section = this.current_tab; + const user_settings_tab = this.current_user_settings_tab; + if (two_column_mode()) { // In one column mode want to show the settings list, not the first settings section. - this.activate_section_or_default(section); + this.activate_section_or_default(section, user_settings_tab); } this.$curr_li.trigger("focus"); } + show_org_user_settings_toggler() { + if ($("#admin-user-list").find(".tab-switcher").length === 0) { + const toggler_html = this.org_user_settings_toggler.get(); + $("#admin-user-list .tab-container").html(toggler_html); + + // We need to re-register these handlers since they are + // destroyed once the settings modal closes. + this.org_user_settings_toggler.register_event_handlers(); + } + } + hide() { this.$main_elem.hide(); } @@ -124,7 +156,11 @@ export class SettingsPanelMenu { this.current_tab = tab; } - activate_section_or_default(section) { + set_user_settings_tab(tab) { + this.current_user_settings_tab = tab; + } + + activate_section_or_default(section, user_settings_tab) { popovers.hide_all(); if (!section) { // No section is given so we display the default. @@ -153,10 +189,16 @@ export class SettingsPanelMenu { this.$curr_li.addClass("active"); this.set_current_tab(section); - const settings_section_hash = "#" + this.hash_prefix + section; + if (section !== "users") { + const settings_section_hash = "#" + this.hash_prefix + section; - // It could be that the hash has already been set. - browser_history.update_hash_internally_if_required(settings_section_hash); + // It could be that the hash has already been set. + browser_history.update_hash_internally_if_required(settings_section_hash); + } + if (section === "users" && this.org_user_settings_toggler !== undefined) { + this.show_org_user_settings_toggler(); + this.org_user_settings_toggler.goto(user_settings_tab); + } $(".settings-section").removeClass("show"); diff --git a/web/src/settings_sections.js b/web/src/settings_sections.js index 2e5d567ff7..f35a4327cd 100644 --- a/web/src/settings_sections.js +++ b/web/src/settings_sections.js @@ -35,7 +35,6 @@ export function get_group(section) { return "org_bots"; case "users": - case "deactivated-users-admin": return "org_users"; case "profile": @@ -70,7 +69,6 @@ export function initialize() { load_func_dict.set("default-channels-list", settings_streams.set_up); load_func_dict.set("linkifier-settings", settings_linkifiers.set_up); load_func_dict.set("playground-settings", settings_playgrounds.set_up); - load_func_dict.set("invites-list-admin", settings_invites.set_up); load_func_dict.set("profile-field-settings", settings_profile_fields.set_up); load_func_dict.set("data-exports-admin", settings_exports.set_up); load_func_dict.set( diff --git a/web/src/settings_users.js b/web/src/settings_users.js index d7741c2789..7730f42031 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -16,6 +16,7 @@ import * as scroll_util from "./scroll_util"; import * as settings_bots from "./settings_bots"; import * as settings_config from "./settings_config"; import * as settings_data from "./settings_data"; +import * as setting_invites from "./settings_invites"; import {current_user} from "./state_data"; import * as timerender from "./timerender"; import * as user_deactivation_ui from "./user_deactivation_ui"; @@ -329,7 +330,7 @@ section.active.create_table = (active_users) => { }, onupdate: reset_scrollbar($users_table), }, - $parent_container: $("#admin-user-list").expectOne(), + $parent_container: $("#admin-active-users-list").expectOne(), init_sort: "full_name_alphabetic", sort_fields: { email: user_sort.sort_email, @@ -338,7 +339,7 @@ section.active.create_table = (active_users) => { id: user_sort.sort_user_id, ...ListWidget.generic_sort_functions("alphabetic", ["full_name"]), }, - $simplebar_container: $("#admin-user-list .progressive-table-wrapper"), + $simplebar_container: $("#admin-active-users-list .progressive-table-wrapper"), }); loading.destroy_indicator($("#admin_page_users_loading_indicator")); @@ -561,6 +562,7 @@ export function set_up_humans() { start_data_load(); section.active.handle_events(); section.deactivated.handle_events(); + setting_invites.set_up(); } export function set_up_bots() { diff --git a/web/styles/settings.css b/web/styles/settings.css index 66e870e048..a0055b445f 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -385,7 +385,7 @@ select.settings_select { background-size: 14px; } -#admin-user-list, +#admin-active-users-list, #admin-bot-list { .table tr:first-of-type td { border-top: none; @@ -1582,7 +1582,7 @@ $option_title_width: 180px; } } -#admin-user-list .last_active { +#admin-active-users-list .last_active { width: 100px; } @@ -2013,3 +2013,11 @@ $option_title_width: 180px; margin-top: -10px; } } + +.tab-switcher.org-user-settings-switcher { + margin-bottom: 12px; +} + +#admin-user-list .tab-switcher .ind-tab { + width: 110px; +} diff --git a/web/templates/invitation_failed_error.hbs b/web/templates/invitation_failed_error.hbs index d89bcfcccd..c708979708 100644 --- a/web/templates/invitation_failed_error.hbs +++ b/web/templates/invitation_failed_error.hbs @@ -16,7 +16,7 @@

{{#tr}} You can reactivate deactivated users from organization settings. - {{#*inline "z-link"}}{{> @partial-block}}{{/inline}} + {{#*inline "z-link"}}{{> @partial-block}}{{/inline}} {{/tr}}

{{else}} diff --git a/web/templates/settings/active_user_list_admin.hbs b/web/templates/settings/active_user_list_admin.hbs new file mode 100644 index 0000000000..30ff599391 --- /dev/null +++ b/web/templates/settings/active_user_list_admin.hbs @@ -0,0 +1,28 @@ + diff --git a/web/templates/settings/admin_tab.hbs b/web/templates/settings/admin_tab.hbs index 6cfed7300b..daf65b312a 100644 --- a/web/templates/settings/admin_tab.hbs +++ b/web/templates/settings/admin_tab.hbs @@ -15,8 +15,6 @@ {{> user_list_admin }} -{{> deactivated_users_admin }} - {{> bot_list_admin }} {{> default_streams_list_admin }} @@ -27,8 +25,6 @@ {{> playground_settings_admin }} -{{> invites_list_admin }} - {{> profile_field_settings_admin }} {{> data_exports_admin }} diff --git a/web/templates/settings/deactivated_users_admin.hbs b/web/templates/settings/deactivated_users_admin.hbs index bb442d8cbc..04d1928b6f 100644 --- a/web/templates/settings/deactivated_users_admin.hbs +++ b/web/templates/settings/deactivated_users_admin.hbs @@ -1,4 +1,4 @@ -
+