settings: Unite user settings into a single panel.

Previously, there were three different sections for managing active
users, deactivated users and invitations.
This commit combines users section has into a single tabbed panel.

Fixes: #26949.

Co-authored-by: shashank-23002 <21bec103@iiitdmj.ac.in>
This commit is contained in:
Shubham Padia 2024-05-30 09:28:49 +00:00 committed by Tim Abbott
parent e4c89771fd
commit 8ab6e71593
19 changed files with 153 additions and 75 deletions

View File

@ -80,7 +80,7 @@ bots will be deactivated until the user manually
{tab|via-organization-settings} {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 1. Click the **Reactivate** button to the right of the user account that you
want to reactivate. want to reactivate.

View File

@ -84,7 +84,7 @@ for invitations for the organization owners role.
{start_tabs} {start_tabs}
{settings_tab|invites-list-admin} {settings_tab|invitations}
1. From there, you can view pending invitations, **Revoke** email 1. From there, you can view pending invitations, **Revoke** email
invitations and invitation links, or **Resend** email invitations. invitations and invitation links, or **Resend** email invitations.

View File

@ -15,6 +15,7 @@ async function navigate_to_user_list(page: Page): Promise<void> {
await page.waitForSelector("#settings_overlay_container.show", {visible: true}); await page.waitForSelector("#settings_overlay_container.show", {visible: true});
await page.click("li[data-section='users']"); 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<string> { async function user_row(page: Page, name: string): Promise<string> {
@ -80,7 +81,8 @@ async function test_deactivated_users_section(page: Page): Promise<void> {
// "Deactivated users" section doesn't render just deactivated users until reloaded. // "Deactivated users" section doesn't render just deactivated users until reloaded.
await page.reload(); 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.waitForSelector(deactivated_users_section, {visible: true});
await page.click(deactivated_users_section); await page.click(deactivated_users_section);

View File

@ -80,11 +80,12 @@ function insert_tip_box() {
is_admin: current_user.is_admin, is_admin: current_user.is_admin,
}); });
$(".organization-box") $(".organization-box")
.find(".settings-section") .find(".settings-section, .user-settings-section")
.not("#emoji-settings") .not("#emoji-settings")
.not("#organization-auth-settings") .not("#organization-auth-settings")
.not("#admin-bot-list") .not("#admin-bot-list")
.not("#admin-invites-list") .not("#admin-invites-list")
.not("#admin-user-list")
.prepend($(tip_box_html)); .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_sections.reset_sections();
settings.open_settings_overlay(); settings.open_settings_overlay();
if (section !== "") { if (section !== "") {
settings_panel_menu.org_settings.set_current_tab(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"); settings_toggle.goto("organization");
} }

View File

@ -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) { export function set_hash_to_home_view(triggered_by_escape_key = false) {
const current_hash = window.location.hash; const current_hash = window.location.hash;
if (current_hash === "") { if (current_hash === "") {
@ -352,7 +370,10 @@ function do_hashchange_overlay(old_hash) {
// hand-typed a hash. // hand-typed a hash.
blueslip.warn("missing section for organization"); 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; return;
} }
@ -373,6 +394,7 @@ function do_hashchange_overlay(old_hash) {
settings_panel_menu.normal_settings.set_current_tab(section); settings_panel_menu.normal_settings.set_current_tab(section);
} else { } else {
settings_panel_menu.org_settings.set_current_tab(section); 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); settings_toggle.goto(base);
return; return;
@ -440,7 +462,7 @@ function do_hashchange_overlay(old_hash) {
if (base === "organization") { if (base === "organization") {
settings.build_page(); settings.build_page();
admin.build_page(); admin.build_page();
admin.launch(section); admin.launch(section, get_user_settings_tab(section));
return; return;
} }

View File

@ -175,6 +175,10 @@ export function initialize() {
show_uploaded_files_section: realm.max_file_upload_size_mib > 0, show_uploaded_files_section: realm.max_file_upload_size_mib > 0,
show_emoji_settings_lock: !settings_data.user_can_add_custom_emoji(), show_emoji_settings_lock: !settings_data.user_can_add_custom_emoji(),
can_create_new_bots: settings_bots.can_create_new_bots(), 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)); $("#settings_overlay_container").append($(rendered_settings_overlay));

View File

@ -2,7 +2,8 @@ import $ from "jquery";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as browser_history from "./browser_history"; 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 keydown_util from "./keydown_util";
import * as popovers from "./popovers"; import * as popovers from "./popovers";
import * as scroll_util from "./scroll_util"; import * as scroll_util from "./scroll_util";
@ -50,12 +51,30 @@ export class SettingsPanelMenu {
this.hash_prefix = opts.hash_prefix; this.hash_prefix = opts.hash_prefix;
this.$curr_li = this.$main_elem.children("li").eq(0); this.$curr_li = this.$main_elem.children("li").eq(0);
this.current_tab = this.$curr_li.data("section"); 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) => { this.$main_elem.on("click", "li[data-section]", (e) => {
const section = $(e.currentTarget).attr("data-section"); 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, // You generally want to add logic to activate_section,
// not to this click handler. // not to this click handler.
@ -66,13 +85,26 @@ export class SettingsPanelMenu {
show() { show() {
this.$main_elem.show(); this.$main_elem.show();
const section = this.current_tab; const section = this.current_tab;
const user_settings_tab = this.current_user_settings_tab;
if (two_column_mode()) { if (two_column_mode()) {
// In one column mode want to show the settings list, not the first settings section. // 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"); 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() { hide() {
this.$main_elem.hide(); this.$main_elem.hide();
} }
@ -124,7 +156,11 @@ export class SettingsPanelMenu {
this.current_tab = tab; 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(); popovers.hide_all();
if (!section) { if (!section) {
// No section is given so we display the default. // No section is given so we display the default.
@ -153,10 +189,16 @@ export class SettingsPanelMenu {
this.$curr_li.addClass("active"); this.$curr_li.addClass("active");
this.set_current_tab(section); this.set_current_tab(section);
if (section !== "users") {
const settings_section_hash = "#" + this.hash_prefix + section; const settings_section_hash = "#" + this.hash_prefix + section;
// It could be that the hash has already been set. // It could be that the hash has already been set.
browser_history.update_hash_internally_if_required(settings_section_hash); 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"); $(".settings-section").removeClass("show");

View File

@ -35,7 +35,6 @@ export function get_group(section) {
return "org_bots"; return "org_bots";
case "users": case "users":
case "deactivated-users-admin":
return "org_users"; return "org_users";
case "profile": case "profile":
@ -70,7 +69,6 @@ export function initialize() {
load_func_dict.set("default-channels-list", settings_streams.set_up); 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("linkifier-settings", settings_linkifiers.set_up);
load_func_dict.set("playground-settings", settings_playgrounds.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("profile-field-settings", settings_profile_fields.set_up);
load_func_dict.set("data-exports-admin", settings_exports.set_up); load_func_dict.set("data-exports-admin", settings_exports.set_up);
load_func_dict.set( load_func_dict.set(

View File

@ -16,6 +16,7 @@ import * as scroll_util from "./scroll_util";
import * as settings_bots from "./settings_bots"; import * as settings_bots from "./settings_bots";
import * as settings_config from "./settings_config"; import * as settings_config from "./settings_config";
import * as settings_data from "./settings_data"; import * as settings_data from "./settings_data";
import * as setting_invites from "./settings_invites";
import {current_user} from "./state_data"; import {current_user} from "./state_data";
import * as timerender from "./timerender"; import * as timerender from "./timerender";
import * as user_deactivation_ui from "./user_deactivation_ui"; import * as user_deactivation_ui from "./user_deactivation_ui";
@ -329,7 +330,7 @@ section.active.create_table = (active_users) => {
}, },
onupdate: reset_scrollbar($users_table), onupdate: reset_scrollbar($users_table),
}, },
$parent_container: $("#admin-user-list").expectOne(), $parent_container: $("#admin-active-users-list").expectOne(),
init_sort: "full_name_alphabetic", init_sort: "full_name_alphabetic",
sort_fields: { sort_fields: {
email: user_sort.sort_email, email: user_sort.sort_email,
@ -338,7 +339,7 @@ section.active.create_table = (active_users) => {
id: user_sort.sort_user_id, id: user_sort.sort_user_id,
...ListWidget.generic_sort_functions("alphabetic", ["full_name"]), ...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")); loading.destroy_indicator($("#admin_page_users_loading_indicator"));
@ -561,6 +562,7 @@ export function set_up_humans() {
start_data_load(); start_data_load();
section.active.handle_events(); section.active.handle_events();
section.deactivated.handle_events(); section.deactivated.handle_events();
setting_invites.set_up();
} }
export function set_up_bots() { export function set_up_bots() {

View File

@ -385,7 +385,7 @@ select.settings_select {
background-size: 14px; background-size: 14px;
} }
#admin-user-list, #admin-active-users-list,
#admin-bot-list { #admin-bot-list {
.table tr:first-of-type td { .table tr:first-of-type td {
border-top: none; border-top: none;
@ -1582,7 +1582,7 @@ $option_title_width: 180px;
} }
} }
#admin-user-list .last_active { #admin-active-users-list .last_active {
width: 100px; width: 100px;
} }
@ -2013,3 +2013,11 @@ $option_title_width: 180px;
margin-top: -10px; margin-top: -10px;
} }
} }
.tab-switcher.org-user-settings-switcher {
margin-bottom: 12px;
}
#admin-user-list .tab-switcher .ind-tab {
width: 110px;
}

View File

@ -16,7 +16,7 @@
<p id="invitation_admin_message"> <p id="invitation_admin_message">
{{#tr}} {{#tr}}
You can reactivate deactivated users from <z-link>organization settings</z-link>. You can reactivate deactivated users from <z-link>organization settings</z-link>.
{{#*inline "z-link"}}<a href="#organization/deactivated-users-admin">{{> @partial-block}}</a>{{/inline}} {{#*inline "z-link"}}<a href="#organization/deactivated">{{> @partial-block}}</a>{{/inline}}
{{/tr}} {{/tr}}
</p> </p>
{{else}} {{else}}

View File

@ -0,0 +1,28 @@
<div id="admin-active-users-list" class="user-settings-section" data-user-settings-section="active">
<div class="settings_panel_list_header">
<h3>{{t "Users"}}</h3>
<div class="alert-notification" id="user-field-status"></div>
<div class="user_filters">
{{> ../dropdown_widget widget_name=active_user_list_dropdown_widget_name}}
<input type="text" class="search filter_text_input" placeholder="{{t 'Filter users' }}" aria-label="{{t 'Filter users' }}"/>
</div>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-striped wrapped-table">
<thead class="table-sticky-headers">
<th class="active" data-sort="alphabetic" data-sort-prop="full_name">{{t "Name" }}</th>
<th data-sort="email">{{t "Email" }}</th>
<th class="user_role" data-sort="role">{{t "Role" }}</th>
<th class="last_active" data-sort="last_active">{{t "Last active" }}</th>
{{#if is_admin}}
<th class="actions">{{t "Actions" }}</th>
{{/if}}
</thead>
<tbody id="admin_users_table" class="admin_user_table"
data-empty="{{t 'No users match your filters.' }}"></tbody>
</table>
</div>
<div id="admin_page_users_loading_indicator"></div>
</div>

View File

@ -15,8 +15,6 @@
{{> user_list_admin }} {{> user_list_admin }}
{{> deactivated_users_admin }}
{{> bot_list_admin }} {{> bot_list_admin }}
{{> default_streams_list_admin }} {{> default_streams_list_admin }}
@ -27,8 +25,6 @@
{{> playground_settings_admin }} {{> playground_settings_admin }}
{{> invites_list_admin }}
{{> profile_field_settings_admin }} {{> profile_field_settings_admin }}
{{> data_exports_admin }} {{> data_exports_admin }}

View File

@ -1,4 +1,4 @@
<div id="admin-deactivated-users-list" class="settings-section" data-name="deactivated-users-admin"> <div id="admin-deactivated-users-list" class="user-settings-section" data-user-settings-section="deactivated">
<div class="clear-float"></div> <div class="clear-float"></div>
<div class="settings_panel_list_header"> <div class="settings_panel_list_header">

View File

@ -1,4 +1,4 @@
<div id="admin-invites-list" class="settings-section" data-name="invites-list-admin"> <div id="admin-invites-list" class="user-settings-section" data-user-settings-section="invitations">
<div class="tip invite-user-settings-tip"></div> <div class="tip invite-user-settings-tip"></div>
{{#unless is_admin }} {{#unless is_admin }}
<div class="tip">{{t "You can only view or manage invitations that you sent." }}</div> <div class="tip">{{t "You can only view or manage invitations that you sent." }}</div>

View File

@ -1,30 +1,11 @@
<div id="admin-user-list" class="settings-section" data-name="users"> <div id="admin-user-list" class="settings-section" data-name="users">
<div class="clear-float"></div> <div class="tab-container"></div>
<div class="settings_panel_list_header"> {{>active_user_list_admin}}
<h3>{{t "Users"}}</h3>
<div class="alert-notification" id="user-field-status"></div> {{>deactivated_users_admin}}
<div class="user_filters">
{{> ../dropdown_widget widget_name=active_user_list_dropdown_widget_name}} {{>invites_list_admin}}
<input type="text" class="search filter_text_input" placeholder="{{t 'Filter users' }}" aria-label="{{t 'Filter users' }}"/>
</div>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-striped wrapped-table">
<thead class="table-sticky-headers">
<th class="active" data-sort="alphabetic" data-sort-prop="full_name">{{t "Name" }}</th>
<th data-sort="email">{{t "Email" }}</th>
<th class="user_role" data-sort="role">{{t "Role" }}</th>
<th class="last_active" data-sort="last_active">{{t "Last active" }}</th>
{{#if is_admin}}
<th class="actions">{{t "Actions" }}</th>
{{/if}}
</thead>
<tbody id="admin_users_table" class="admin_user_table"
data-empty="{{t 'No users match your filters.' }}"></tbody>
</table>
</div>
<div id="admin_page_users_loading_indicator"></div>
</div> </div>

View File

@ -89,14 +89,7 @@
<li tabindex="0" data-section="users"> <li tabindex="0" data-section="users">
<i class="icon fa fa-user" aria-hidden="true"></i> <i class="icon fa fa-user" aria-hidden="true"></i>
<div class="text">{{t "Users" }}</div> <div class="text">{{t "Users" }}</div>
<i class="locked fa fa-lock tippy-zulip-tooltip" {{#if is_admin}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i> <i class="locked fa fa-lock tippy-zulip-tooltip" {{#if can_edit_user_panel }}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i>
</li>
{{/unless}}
{{#unless is_guest}}
<li class="collapse-org-settings {{#unless is_admin}}hide-org-settings{{/unless}}" tabindex="0" data-section="deactivated-users-admin">
<i class="icon fa fa-user-times" aria-hidden="true"></i>
<div class="text">{{t "Deactivated users" }}</div>
<i class="locked fa fa-lock tippy-zulip-tooltip" {{#if is_admin}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i>
</li> </li>
{{/unless}} {{/unless}}
{{#unless is_guest}} {{#unless is_guest}}
@ -106,12 +99,6 @@
<i class="locked fa fa-lock tippy-zulip-tooltip" {{#if can_create_new_bots}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i> <i class="locked fa fa-lock tippy-zulip-tooltip" {{#if can_create_new_bots}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i>
</li> </li>
{{/unless}} {{/unless}}
{{#unless is_guest}}
<li tabindex="0" data-section="invites-list-admin">
<i class="icon fa fa-user-plus" aria-hidden="true"></i>
<div class="text">{{t "Invitations" }}</div>
</li>
{{/unless}}
{{#if is_admin}} {{#if is_admin}}
<li tabindex="0" data-section="profile-field-settings"> <li tabindex="0" data-section="profile-field-settings">
<i class="icon fa fa-id-card" aria-hidden="true"></i> <i class="icon fa fa-id-card" aria-hidden="true"></i>

View File

@ -361,7 +361,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
[settings, "launch"], [settings, "launch"],
]); ]);
window.location.hash = "#organization/users"; window.location.hash = "#organization/users/active";
helper.clear_events(); helper.clear_events();
$window_stub.trigger("hashchange"); $window_stub.trigger("hashchange");
@ -369,7 +369,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
[overlays, "close_for_hash_change"], [overlays, "close_for_hash_change"],
[settings, "build_page"], [settings, "build_page"],
[admin, "build_page"], [admin, "build_page"],
[admin, "launch", ["users"]], [admin, "launch", ["users", "active"]],
]); ]);
window.location.hash = "#organization/user-list-admin"; window.location.hash = "#organization/user-list-admin";
@ -384,7 +384,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
[overlays, "close_for_hash_change"], [overlays, "close_for_hash_change"],
[settings, "build_page"], [settings, "build_page"],
[admin, "build_page"], [admin, "build_page"],
[admin, "launch", ["users"]], [admin, "launch", ["users", "active"]],
]); ]);
helper.clear_events(); helper.clear_events();

View File

@ -58,11 +58,15 @@ link_mapping = {
"Authentication methods", "Authentication methods",
"/#organization/auth-methods", "/#organization/auth-methods",
], ],
"users": ["Organization settings", "Users", "/#organization/users"], "users": [
"deactivated-users-admin": [ "Organization settings",
"Users",
"/#organization/users/active",
],
"deactivated": [
"Organization settings", "Organization settings",
"Deactivated users", "Deactivated users",
"/#organization/deactivated-users-admin", "/#organization/users/deactivated",
], ],
"bot-list-admin": [ "bot-list-admin": [
"Organization settings", "Organization settings",
@ -89,10 +93,10 @@ link_mapping = {
"Custom profile fields", "Custom profile fields",
"/#organization/profile-field-settings", "/#organization/profile-field-settings",
], ],
"invites-list-admin": [ "invitations": [
"Organization settings", "Organization settings",
"Invitations", "Invitations",
"/#organization/invites-list-admin", "/#organization/users/invitations",
], ],
"data-exports-admin": [ "data-exports-admin": [
"Organization settings", "Organization settings",