user groups: Implement edit features in user group settings overlay.

Follow up for #22214.
This commit is contained in:
Purushottam Tiwari 2022-08-31 22:51:19 +05:30 committed by Tim Abbott
parent b1c81e2e02
commit 7879f78917
14 changed files with 444 additions and 5 deletions

View File

@ -234,6 +234,24 @@ export function is_editing_stream(desired_stream_id) {
return stream_id === desired_stream_id;
}
export function is_editing_group(desired_group_id) {
const hash_components = window.location.hash.slice(1).split(/\//);
if (hash_components[0] !== "groups") {
return false;
}
if (!hash_components[2]) {
return false;
}
// if the string casted to a number is valid, and another component
// after exists then it's a stream name/id pair.
const group_id = Number.parseFloat(hash_components[1]);
return group_id === desired_group_id;
}
export function is_create_new_stream_narrow() {
return window.location.hash === "#streams/new";
}

View File

@ -29,7 +29,7 @@ function format_member_list_elem(person) {
user_id: person.user_id,
is_current_user: person.user_id === page_params.user_id,
email: settings_data.email_for_user_settings(person),
displaying_for_admin: page_params.is_admin,
can_edit_subscribers: page_params.is_admin,
show_email: settings_data.show_email(),
});
}
@ -243,7 +243,11 @@ function remove_subscriber({stream_id, target_user_id, $list_entry}) {
}
if (sub.invite_only && people.is_my_user_id(target_user_id)) {
const html_body = render_unsubscribe_private_stream_modal();
const html_body = render_unsubscribe_private_stream_modal({
message: $t({
defaultMessage: "Once you leave this stream, you will not be able to rejoin.",
}),
});
confirm_dialog.launch({
html_heading: $t_html(

View File

@ -99,6 +99,7 @@ import * as ui from "./ui";
import * as unread from "./unread";
import * as unread_ui from "./unread_ui";
import * as user_group_edit from "./user_group_edit";
import * as user_group_edit_members from "./user_group_edit_members";
import * as user_groups from "./user_groups";
import * as user_group_settings_ui from "./user_groups_settings_ui";
import {initialize_user_settings, user_settings} from "./user_settings";
@ -622,6 +623,7 @@ export function initialize_everything() {
user_group_edit.initialize();
stream_edit_subscribers.initialize();
stream_data.initialize(stream_data_params);
user_group_edit_members.initialize();
pm_conversations.recent.initialize(pm_conversations_params);
user_topics.initialize();
muted_users.initialize();

View File

@ -15,6 +15,7 @@ import * as people from "./people";
import * as settings_data from "./settings_data";
import * as settings_ui from "./settings_ui";
import * as ui from "./ui";
import * as user_group_edit_members from "./user_group_edit_members";
import * as user_group_ui_updates from "./user_group_ui_updates";
import * as user_groups from "./user_groups";
import * as user_group_settings_ui from "./user_groups_settings_ui";
@ -68,6 +69,17 @@ export function get_edit_container(group) {
);
}
function show_membership_settings(group) {
const $edit_container = get_edit_container(group);
user_group_ui_updates.update_add_members_elements(group);
const $member_container = $edit_container.find(".edit_members_for_user_group");
user_group_edit_members.enable_member_management({
group,
$parent_container: $member_container,
});
}
export function show_settings_for(node) {
const group = get_user_group_for_target(node);
const html = render_user_group_settings({
@ -83,6 +95,7 @@ export function show_settings_for(node) {
$(".nothing-selected").hide();
$edit_container.show();
show_membership_settings(group);
}
export function setup_group_settings(node) {

View File

@ -0,0 +1,303 @@
import $ from "jquery";
import render_leave_user_group_modal from "../templates/confirm_dialog/confirm_unsubscribe_private_stream.hbs";
import render_user_group_member_list_entry from "../templates/stream_settings/stream_member_list_entry.hbs";
import render_user_group_subscription_request_result from "../templates/stream_settings/stream_subscription_request_result.hbs";
import * as add_subscribers_pill from "./add_subscribers_pill";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import {$t, $t_html} from "./i18n";
import * as ListWidget from "./list_widget";
import {page_params} from "./page_params";
import * as people from "./people";
import * as settings_data from "./settings_data";
import * as ui from "./ui";
import * as user_group_edit from "./user_group_edit";
import * as user_groups from "./user_groups";
export let pill_widget;
let current_group_id;
function get_potential_members() {
const group = user_groups.get_user_group_from_id(current_group_id);
function is_potential_member(person) {
// user verbose style filter to have room
// to add more potential checks easily.
if (group.members.has(person.user_id)) {
return false;
}
return true;
}
return people.filter_all_users(is_potential_member);
}
function format_member_list_elem(person) {
return render_user_group_member_list_entry({
name: person.full_name,
user_id: person.user_id,
is_current_user: person.user_id === page_params.user_id,
email: settings_data.email_for_user_settings(person),
can_edit_subscribers: user_group_edit.can_edit(current_group_id),
show_email: settings_data.show_email(),
});
}
function make_list_widget({$parent_container, name, user_ids}) {
const users = people.get_users_from_ids(user_ids);
people.sort_but_pin_current_user_on_top(users);
const $list_container = $parent_container.find(".member_table");
$list_container.empty();
const $simplebar_container = $parent_container.find(".member_list_container");
return ListWidget.create($list_container, users, {
name,
modifier(item) {
return format_member_list_elem(item);
},
filter: {
$element: $parent_container.find(".search"),
predicate(person, value) {
const matcher = people.build_person_matcher(value);
const match = matcher(person);
return match;
},
},
$simplebar_container,
});
}
export function enable_member_management({group, $parent_container}) {
const group_id = group.id;
const $pill_container = $parent_container.find(".pill-container");
// current_group_id and pill_widget are module-level variables
current_group_id = group_id;
pill_widget = add_subscribers_pill.create({
$pill_container,
get_potential_subscribers: get_potential_members,
});
make_list_widget({
$parent_container,
name: "user_group_members",
user_ids: Array.from(group.members),
});
}
function show_user_group_membership_request_result({
message,
add_class,
remove_class,
subscribed_users,
already_subscribed_users,
ignored_deactivated_users,
}) {
const $user_group_subscription_req_result_elem = $(
".user_group_subscription_request_result",
).expectOne();
const html = render_user_group_subscription_request_result({
message,
subscribed_users,
already_subscribed_users,
ignored_deactivated_users,
});
ui.get_content_element($user_group_subscription_req_result_elem).html(html);
if (add_class) {
$user_group_subscription_req_result_elem.addClass(add_class);
}
if (remove_class) {
$user_group_subscription_req_result_elem.removeClass(remove_class);
}
}
function edit_user_group_membership({group, added = [], removed = [], success, error}) {
channel.post({
url: "/json/user_groups/" + group.id + "/members",
data: {
add: JSON.stringify(added),
delete: JSON.stringify(removed),
},
success,
error,
});
}
function add_new_members({pill_user_ids}) {
const group = user_groups.get_user_group_from_id(current_group_id);
if (!group) {
return;
}
const deactivated_users = new Set();
const already_added_users = new Set();
const active_user_ids = pill_user_ids.filter((user_id) => {
if (!people.is_person_active(user_id)) {
deactivated_users.add(user_id);
return false;
}
if (user_groups.is_user_in_group(group.id, user_id)) {
// we filter out already subscribed users before sending
// add member request as the endpoint is not so robust and
// fails complete request if any already subscribed member
// is present in the request.
already_added_users.add(user_id);
return false;
}
return true;
});
const user_id_set = new Set(active_user_ids);
if (
user_id_set.has(page_params.user_id) &&
user_groups.is_user_in_group(group.id, page_params.user_id)
) {
// We don't want to send a request to add ourselves if we
// are already added to this group. This case occurs
// when creating user pills from a stream or user group.
user_id_set.delete(page_params.user_id);
}
let ignored_deactivated_users;
let ignored_already_added_users;
if (deactivated_users.size > 0) {
ignored_deactivated_users = Array.from(deactivated_users);
ignored_deactivated_users = ignored_deactivated_users.map((user_id) =>
people.get_by_user_id(user_id),
);
}
if (already_added_users.size > 0) {
ignored_already_added_users = Array.from(already_added_users);
ignored_already_added_users = ignored_already_added_users.map((user_id) =>
people.get_by_user_id(user_id),
);
}
if (user_id_set.size === 0) {
show_user_group_membership_request_result({
message: $t({defaultMessage: "No user to subscribe."}),
add_class: "text-error",
remove_class: "text-success",
already_subscribed_users: ignored_already_added_users,
ignored_deactivated_users,
});
return;
}
const user_ids = Array.from(user_id_set);
function invite_success() {
pill_widget.clear();
show_user_group_membership_request_result({
message: $t({defaultMessage: "Added successfully."}),
add_class: "text-success",
remove_class: "text-error",
already_subscribed_users: ignored_already_added_users,
ignored_deactivated_users,
});
}
function invite_failure(xhr) {
const error = JSON.parse(xhr.responseText);
show_user_group_membership_request_result({
message: error.msg,
add_class: "text-error",
remove_class: "text-success",
});
}
edit_user_group_membership({
group,
added: user_ids,
success: invite_success,
error: invite_failure,
});
}
function remove_member({group_id, target_user_id, $list_entry}) {
const group = user_groups.get_user_group_from_id(current_group_id);
if (!group) {
return;
}
function removal_success() {
if (group_id !== current_group_id) {
blueslip.info("Response for subscription removal came too late.");
return;
}
$list_entry.remove();
const message = $t({defaultMessage: "Removed successfully."});
show_user_group_membership_request_result({
message,
add_class: "text-success",
remove_class: "text-remove",
});
}
function removal_failure() {
show_user_group_membership_request_result({
message: $t({defaultMessage: "Error removing user from this group."}),
add_class: "text-error",
remove_class: "text-success",
});
}
function do_remove_user_from_group() {
edit_user_group_membership({
group,
removed: [target_user_id],
success: removal_success,
error: removal_failure,
});
}
if (people.is_my_user_id(target_user_id) && !page_params.is_admin) {
const html_body = render_leave_user_group_modal({
message: $t({
defaultMessage: "Once you leave this group, you will not be able to rejoin.",
}),
});
confirm_dialog.launch({
html_heading: $t_html({defaultMessage: "Leave {group_name}"}, {group_name: group.name}),
html_body,
on_click: do_remove_user_from_group,
});
return;
}
do_remove_user_from_group();
}
export function initialize() {
add_subscribers_pill.set_up_handlers({
get_pill_widget: () => pill_widget,
$parent_container: $("#manage_groups_container"),
pill_selector: ".edit_members_for_user_group .pill-container",
button_selector: ".edit_members_for_user_group .add-subscriber-button",
action: add_new_members,
});
$("#manage_groups_container").on(
"submit",
".edit_members_for_user_group .subscriber_list_remove form",
(e) => {
e.preventDefault();
const $list_entry = $(e.target).closest("tr");
const target_user_id = Number.parseInt($list_entry.attr("data-subscriber-id"), 10);
const group_id = current_group_id;
remove_member({group_id, target_user_id, $list_entry});
},
);
}

View File

@ -1,3 +1,9 @@
import $ from "jquery";
import * as hash_util from "./hash_util";
import {$t} from "./i18n";
import {page_params} from "./page_params";
import * as stream_ui_updates from "./stream_ui_updates";
import * as user_group_edit from "./user_group_edit";
// This module will handle ui updates logic for group settings,
@ -5,3 +11,42 @@ import * as user_group_edit from "./user_group_edit";
export function update_toggler_for_group_setting() {
user_group_edit.toggler.goto(user_group_edit.select_tab);
}
export function update_add_members_elements(group) {
if (!hash_util.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_subscribers_container");
if (page_params.is_guest || page_params.realm_is_zephyr_mirror_realm) {
// For guest users, we just hide the add_members feature.
$add_members_container.hide();
return;
}
// Otherwise, we adjust whether the widgets are disabled based on
// whether this user is authorized to add subscribers.
const $input_element = $add_members_container.find(".input").expectOne();
const $button_element = $add_members_container
.find('button[name="add_subscriber"]')
.expectOne();
if (user_group_edit.can_edit(group.id)) {
$input_element.prop("disabled", false);
$button_element.prop("disabled", false);
$button_element.css("pointer-events", "");
$input_element.popover("destroy");
} else {
$input_element.prop("disabled", true);
$button_element.prop("disabled", true);
stream_ui_updates.initialize_disable_btn_hint_popover(
$add_members_container,
$input_element,
$button_element,
$t({defaultMessage: "Only group members can add users to a group."}),
);
}
}

View File

@ -173,6 +173,13 @@ export function initialize() {
e.preventDefault();
open_create_user_group();
});
$("#manage_groups_container").on("click", ".group-row", show_right_section);
$("#manage_groups_container").on("click", ".fa-chevron-left", () => {
$(".right").removeClass("show");
$(".user-groups-header").removeClass("slide-left");
});
}
export function launch(section) {

View File

@ -142,6 +142,7 @@ body.dark-theme {
#compose,
.column-left .left-sidebar,
.column-right .right-sidebar,
#groups_overlay .right,
#subscription_overlay .right,
#settings_page .right {
background-color: hsl(212, 28%, 18%);

View File

@ -2,10 +2,12 @@
margin: 10px auto;
}
.member_list_loading_indicator,
.subscriber_list_loading_indicator {
margin: 10px auto;
}
.member_list_loading_indicator:empty,
.subscriber_list_loading_indicator:empty {
margin: 0;
}
@ -218,6 +220,7 @@ h4.user_group_setting_subsection_title {
width: 100%;
margin: 0 0 10px;
.user_group_subscription_request_result,
.stream_subscription_request_result {
a {
color: inherit;
@ -225,6 +228,7 @@ h4.user_group_setting_subsection_title {
}
}
.member-search,
.subscriber-search {
margin: 10px 0 0;
@ -279,6 +283,7 @@ h4.user_group_setting_subsection_title {
transition: all 0.3s ease;
}
.user-groups-container .user-groups-header.slide-left .fa-chevron-left,
.subscriptions-container .subscriptions-header.slide-left .fa-chevron-left,
#settings_overlay_container
.settings-header.mobile.slide-left
@ -740,6 +745,7 @@ h4.user_group_setting_subsection_title {
margin: 20px;
}
.group_settings_header,
.stream_settings_header {
white-space: nowrap;
display: flex;
@ -1042,6 +1048,7 @@ h4.user_group_setting_subsection_title {
text-align: center;
}
#groups_overlay .group_settings_header,
#subscription_overlay .stream_settings_header {
flex-wrap: wrap;
}
@ -1155,7 +1162,9 @@ h4.user_group_setting_subsection_title {
}
@media (width <= 500px) {
#groups_overlay,
#subscription_overlay {
.groups_settings_header,
.stream_settings_header {
display: block;
text-align: center;

View File

@ -1 +1 @@
<p>{{t "Once you leave this stream, you will not be able to rejoin." }}</p>
<p>{{message}}</p>

View File

@ -8,7 +8,7 @@
<td class="hidden-subscriber-email">{{t "(hidden)"}}</td>
{{/if}}
<td class="subscriber-user-id">{{user_id}}</td>
{{#if displaying_for_admin}}
{{#if can_edit_subscribers}}
<td class="unsubscribe">
<div class="subscriber_list_remove">
<form class="remove-subscriber-form">

View File

@ -0,0 +1,34 @@
<div class="member_list_settings_container">
<h4 class="user_group_setting_subsection_title">
{{t "Add members" }}
</h4>
<div class="member_list_settings">
<div class="member_list_add float-left">
{{> ../stream_settings/add_subscribers_form}}
<div class="user_group_subscription_request_result"></div>
</div>
<div class="clear-float"></div>
</div>
<div>
<h4 class="inline-block user_group_setting_subsection_title">{{t "Members"}}</h4>
<span class="member-search float-right">
<input type="text" class="search" placeholder="{{t 'Filter subscribers' }}" />
</span>
</div>
<div class="member-list-box">
<div class="member_list_container" data-simplebar>
<div class="member_list_loading_indicator"></div>
<table class="member-list table table-striped">
<thead class="table-sticky-headers">
<th>{{t "Name" }}</th>
<th>{{t "Email" }}</th>
<th>{{t "User ID" }}</th>
{{#if can_edit}}
<th class="actions">{{t "Actions" }}</th>
{{/if}}
</thead>
<tbody class="member_table"></tbody>
</table>
</div>
</div>
</div>

View File

@ -24,7 +24,9 @@
</div>
<div class="group_member_settings group_setting_section">
Group member settings.
<div class="edit_members_for_user_group">
{{> user_group_members}}
</div>
</div>
</div>
</div>

View File

@ -204,6 +204,7 @@ EXEMPT_FILES = make_set(
"static/js/user_group_create_members.js",
"static/js/user_group_create_members_data.js",
"static/js/user_group_edit.js",
"static/js/user_group_edit_members.js",
"static/js/user_group_ui_updates.js",
"static/js/user_groups_settings_ui.js",
"static/js/user_profile.js",