mirror of https://github.com/zulip/zulip.git
user groups: Implement edit features in user group settings overlay.
Follow up for #22214.
This commit is contained in:
parent
b1c81e2e02
commit
7879f78917
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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});
|
||||
},
|
||||
);
|
||||
}
|
|
@ -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."}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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%);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1 +1 @@
|
|||
<p>{{t "Once you leave this stream, you will not be able to rejoin." }}</p>
|
||||
<p>{{message}}</p>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue