user groups: Add new UI support for user group creation.

Add support for creation of user groups using right panel
of new user group settings overlay being developed as part
of https://github.com/zulip/zulip/issues/19526.

In further commits we will add support for editing user
groups using right panel of the overlay.

This commit also introduces a minor bug related hashchange
for #groups which would be a quick fix once we have UI
for group edit on #groups overlay.
This commit is contained in:
Purushottam Tiwari 2022-08-21 21:29:39 +05:30 committed by Tim Abbott
parent c1cb5a6ef1
commit a8c3be7fee
11 changed files with 486 additions and 4 deletions

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_groups from "./user_groups";
import * as user_group_settings_ui from "./user_groups_settings_ui";
import {initialize_user_settings, user_settings} from "./user_settings";
import * as user_status from "./user_status";
import * as user_status_ui from "./user_status_ui";
@ -623,6 +624,7 @@ export function initialize_everything() {
user_topics.initialize();
muted_users.initialize();
stream_settings_ui.initialize();
user_group_settings_ui.initialize();
stream_list.initialize();
condense.initialize();
spoilers.initialize();

View File

@ -0,0 +1,189 @@
import $ from "jquery";
import * as channel from "./channel";
import {$t, $t_html} from "./i18n";
import * as loading from "./loading";
import * as ui_report from "./ui_report";
import * as user_group_create_members from "./user_group_create_members";
import * as user_group_create_members_data from "./user_group_create_members_data";
import * as user_groups from "./user_groups";
import * as user_group_settings_ui from "./user_groups_settings_ui";
class UserGroupMembershipError {
report_no_members_to_user_group() {
$("#user_group_membership_error").text(
$t({defaultMessage: "You cannot create a user_group with no members!"}),
);
$("#user_group_membership_error").show();
}
clear_errors() {
$("#user_group_membership_error").hide();
}
}
const user_group_membership_error = new UserGroupMembershipError();
class UserGroupNameError {
report_already_exists() {
$("#user_group_name_error").text(
$t({defaultMessage: "A user group with this name already exists"}),
);
$("#user_group_name_error").show();
}
clear_errors() {
$("#user_group_name_error").hide();
}
report_empty_user_group() {
$("#user_group_name_error").text($t({defaultMessage: "A user group needs to have a name"}));
$("#user_group_name_error").show();
}
select() {
$("#create_user_group_name").trigger("focus").trigger("select");
}
pre_validate(user_group_name) {
if (user_group_name && user_groups.get_user_group_from_name(user_group_name)) {
this.report_already_exists();
return;
}
this.clear_errors();
}
validate_for_submit(user_group_name) {
if (!user_group_name) {
this.report_empty_user_group();
this.select();
return false;
}
if (user_groups.get_user_group_from_name(user_group_name)) {
this.report_already_exists();
this.select();
return false;
}
return true;
}
}
const user_group_name_error = new UserGroupNameError();
export function create_user_group_clicked() {
// this changes the tab switcher (settings/preview) which isn't necessary
// to a add new stream title.
user_group_settings_ui.show_user_group_settings_pane.create_user_group();
$(".group-row.active").removeClass("active");
show_new_user_group_modal();
$("#create_user_group_name").trigger("focus");
}
function clear_error_display() {
user_group_name_error.clear_errors();
$(".user_group_create_info").hide();
user_group_membership_error.clear_errors();
}
export function show_new_user_group_modal() {
$("#user-group-creation").removeClass("hide");
$(".right .settings").hide();
user_group_create_members.build_widgets();
clear_error_display();
}
function create_user_group() {
const data = {};
const group_name = $("#create_user_group_name").val().trim();
const description = $("#create_user_group_description").val().trim();
// Even though we already check to make sure that while typing the user cannot enter
// newline characters (by pressing the Enter key) it would still be possible to copy
// and paste over a description with newline characters in it. Prevent that.
if (description.includes("\n")) {
ui_report.client_error(
$t_html({defaultMessage: "The group description cannot contain newline characters."}),
$(".user_group_create_info"),
);
return undefined;
}
data.name = group_name;
data.description = description;
const user_ids = user_group_create_members.get_principals();
data.members = JSON.stringify(user_ids);
loading.make_indicator($("#user_group_creating_indicator"), {
text: $t({defaultMessage: "Creating group..."}),
});
return channel.post({
url: "/json/user_groups/create",
data,
success() {
$("#create_user_group_name").val("");
$("#create_user_group_description").val("");
user_group_create_members.clear_member_list();
ui_report.success(
$t_html({defaultMessage: "User group successfully created!"}),
$(".user_group_create_info"),
);
loading.destroy_indicator($("#user_group_creating_indicator"));
// TODO: The rest of the work should be done via the create event we will get for user group.
},
error(xhr) {
ui_report.error(
$t_html({defaultMessage: "Error creating user group."}),
xhr,
$(".user_group_create_info"),
);
loading.destroy_indicator($("#user_group_creating_indicator"));
},
});
}
export function set_up_handlers() {
const $people_to_add_holder = $("#people_to_add_in_group").expectOne();
user_group_create_members.create_handlers($people_to_add_holder);
const $container = $("#user-group-creation").expectOne();
$container.on("click", ".finalize_create_user_group", (e) => {
e.preventDefault();
clear_error_display();
const group_name = $("#create_user_group_name").val().trim();
const name_ok = user_group_name_error.validate_for_submit(group_name);
if (!name_ok) {
return;
}
const principals = user_group_create_members_data.get_principals();
if (principals.length === 0) {
user_group_membership_error.report_no_members_to_user_group();
return;
}
create_user_group();
});
$container.on("input", "#create_user_group_name", () => {
const user_group_name = $("#create_user_group_name").val().trim();
// This is an inexpensive check.
user_group_name_error.pre_validate(user_group_name);
});
// Do not allow the user to enter newline characters while typing out the
// group's description during it's creation.
$container.on("keydown", "#create_user_group_description", (e) => {
if (e.key === "Enter") {
e.preventDefault();
}
});
}

View File

@ -0,0 +1,121 @@
import $ from "jquery";
import render_new_user_group_user from "../templates/stream_settings/new_stream_user.hbs";
import render_new_user_group_users from "../templates/user_group_settings/new_user_group_users.hbs";
import * as add_subscribers_pill from "./add_subscribers_pill";
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 user_group_create_members_data from "./user_group_create_members_data";
let pill_widget;
let all_users_list_widget;
export function get_principals() {
return user_group_create_members_data.get_principals();
}
function redraw_member_list() {
all_users_list_widget.replace_list_data(user_group_create_members_data.sorted_user_ids());
}
function add_user_ids(user_ids) {
user_group_create_members_data.add_user_ids(user_ids);
redraw_member_list();
}
function add_all_users() {
const user_ids = user_group_create_members_data.get_all_user_ids();
add_user_ids(user_ids);
}
function remove_user_ids(user_ids) {
user_group_create_members_data.remove_user_ids(user_ids);
redraw_member_list();
}
export function clear_member_list() {
user_group_create_members_data.initialize_with_current_user();
redraw_member_list();
}
function build_pill_widget({$parent_container}) {
const $pill_container = $parent_container.find(".pill-container");
const get_potential_members = user_group_create_members_data.get_potential_members;
pill_widget = add_subscribers_pill.create({
$pill_container,
get_potential_subscribers: get_potential_members,
});
}
export function create_handlers($container) {
$container.on("click", ".add_all_users_to_user_group", (e) => {
e.preventDefault();
add_all_users();
$(".add-user-list-filter").trigger("focus");
});
$container.on("click", ".remove_potential_subscriber", (e) => {
e.preventDefault();
const $elem = $(e.target);
const user_id = Number.parseInt($elem.attr("data-user-id"), 10);
remove_user_ids([user_id]);
});
function add_users({pill_user_ids}) {
add_user_ids(pill_user_ids);
pill_widget.clear();
}
add_subscribers_pill.set_up_handlers({
get_pill_widget: () => pill_widget,
$parent_container: $container,
pill_selector: ".add_subscribers_container .input",
button_selector: ".add_subscribers_container button.add-subscriber-button",
action: add_users,
});
}
export function build_widgets() {
const $add_people_container = $("#people_to_add_in_group");
$add_people_container.html(render_new_user_group_users({}));
const $simplebar_container = $add_people_container.find(".member_list_container");
build_pill_widget({$parent_container: $add_people_container});
user_group_create_members_data.initialize_with_current_user();
const current_user_id = page_params.user_id;
all_users_list_widget = ListWidget.create($("#create_user_group_members"), [current_user_id], {
name: "new_user_group_add_users",
$parent_container: $add_people_container,
modifier(user_id) {
const user = people.get_by_user_id(user_id);
const item = {
show_email: settings_data.show_email(),
email: people.get_visible_email(user),
user_id,
full_name: user.full_name,
is_current_user: user_id === current_user_id,
disabled: user_id === current_user_id,
};
return render_new_user_group_user(item);
},
filter: {
$element: $("#people_to_add_in_group .add-user-list-filter"),
predicate(user_id, search_term) {
const user = people.get_by_user_id(user_id);
return people.build_person_matcher(search_term)(user);
},
},
$simplebar_container,
html_selector: (user_id) => {
const user = people.get_by_user_id(user_id);
return $(`#${CSS.escape("user_checkbox_" + user.user_id)}`);
},
});
}

View File

@ -0,0 +1,51 @@
import {page_params} from "./page_params";
import * as people from "./people";
let user_id_set;
export function initialize_with_current_user() {
const current_user_id = page_params.user_id;
user_id_set = new Set();
user_id_set.add(current_user_id);
}
export function sorted_user_ids() {
const users = people.get_users_from_ids(Array.from(user_id_set));
people.sort_but_pin_current_user_on_top(users);
return users.map((user) => user.user_id);
}
export function get_all_user_ids() {
const potential_members = people.get_realm_users();
const user_ids = potential_members.map((user) => user.user_id);
// sort for determinism
user_ids.sort((a, b) => a - b);
return user_ids;
}
export function get_principals() {
// Return list of user ids which were selected by user.
return Array.from(user_id_set);
}
export function get_potential_members() {
const potential_members = people.get_realm_users();
return potential_members.filter((user) => !user_id_set.has(user.user_id));
}
export function add_user_ids(user_ids) {
for (const user_id of user_ids) {
if (!user_id_set.has(user_id)) {
const user = people.get_by_user_id(user_id);
if (user) {
user_id_set.add(user_id);
}
}
}
}
export function remove_user_ids(user_ids) {
for (const user_id of user_ids) {
user_id_set.delete(user_id);
}
}

View File

@ -4,11 +4,13 @@ import render_browse_user_groups_list_item from "../templates/user_group_setting
import render_user_group_settings_overlay from "../templates/user_group_settings/user_group_settings_overlay.hbs";
import * as browser_history from "./browser_history";
import {$t} from "./i18n";
import * as ListWidget from "./list_widget";
import * as overlays from "./overlays";
import * as people from "./people";
import * as settings_data from "./settings_data";
import * as ui from "./ui";
import * as user_group_create from "./user_group_create";
import * as user_groups from "./user_groups";
export function set_up_click_handlers() {
@ -25,6 +27,26 @@ export function set_up_click_handlers() {
});
}
export const show_user_group_settings_pane = {
nothing_selected() {
$(".settings, #user-group-creation").hide();
$(".nothing-selected").show();
$("#groups_overlay .user-group-info-title").text(
$t({defaultMessage: "User group settings"}),
);
},
create_user_group() {
$(".nothing-selected, .settings, #user-group-creation").hide();
$("#user-group-creation").show();
$("#groups_overlay .user-group-info-title").text($t({defaultMessage: "Create user group"}));
},
};
export function open_create_user_group() {
user_group_create.create_user_group_clicked();
browser_history.update("#groups/new");
}
export function setup_page(callback) {
function populate_and_fill() {
const template_data = {
@ -63,6 +85,7 @@ export function setup_page(callback) {
});
set_up_click_handlers();
user_group_create.set_up_handlers();
// show the "User group settings" header by default.
$(".display-type #user_group_settings_title").show();
@ -75,6 +98,13 @@ export function setup_page(callback) {
populate_and_fill();
}
export function initialize() {
$("#manage_groups_container").on("click", ".create_user_group_button", (e) => {
e.preventDefault();
open_create_user_group();
});
}
export function launch() {
setup_page(() => {
overlays.open_overlay({

View File

@ -424,6 +424,10 @@ body.dark-theme {
#stream-creation
#stream_creation_form
#stream_creating_indicator:not(:empty),
#groups_overlay
#user-group-creation
#user_group_creation_form
#user_group_creating_indicator:not(:empty),
.emoji-info-popover
.emoji-popover
.emoji-popover-emoji:not(.reacted):focus {
@ -663,6 +667,8 @@ body.dark-theme {
.user-groups-container .left,
.subscriber-list-box,
.subscriber-list-box .subscriber_list_container .subscriber-list tr,
.member-list-box,
.member-list-box .member_list_container .member-list tr,
#subscription_overlay .subsection-parent div,
#subscription_overlay .radio-input-parent,
#stream_privacy_modal .radio-input-parent,

View File

@ -96,10 +96,12 @@
}
}
#create_user_group_description,
#create_stream_description {
width: calc(100% - 15px);
}
.user_group_creation_error,
.stream_creation_error {
display: none;
margin-left: 2px;
@ -107,26 +109,30 @@
}
/* TODO: Unify with settings.css definition */
h3.stream_setting_subsection_title {
h3.stream_setting_subsection_title,
h3.user_group_setting_subsection_title {
display: inline-block;
font-size: 1.5em;
font-weight: normal;
line-height: 1.5;
}
h4.stream_setting_subsection_title {
h4.stream_setting_subsection_title,
h4.user_group_setting_subsection_title {
display: inline-block;
font-size: 1.35em;
font-weight: normal;
line-height: 1.5;
}
.member-list-box,
.subscriber-list-box {
text-align: center;
border-left: 1px solid hsl(0, 0%, 87%);
border-right: 1px solid hsl(0, 0%, 87%);
border-radius: 4px;
.member_list_container,
.subscriber_list_container {
position: relative;
/* 2*45px (settings header) + 38px(tab-container row) + 20px (margin for .inner-box) + 134px (add user input and search widget area) = 282px */
@ -135,6 +141,7 @@ h4.stream_setting_subsection_title {
text-align: left;
-webkit-overflow-scrolling: touch;
.member-list,
.subscriber-list {
width: 100%;
margin: auto;
@ -206,7 +213,8 @@ h4.stream_setting_subsection_title {
padding-right: 8px;
}
.subscriber_list_add {
.subscriber_list_add,
.member_list_add {
width: 100%;
margin: 0 0 10px;
@ -225,7 +233,8 @@ h4.stream_setting_subsection_title {
}
}
.subscriber_list_add .form-inline {
.subscriber_list_add .form-inline,
.member_list_add .form-inline {
margin-bottom: 0;
}
@ -485,6 +494,7 @@ h4.stream_setting_subsection_title {
font-weight: 400;
}
.user-group-creation-body,
.stream-creation-body {
section.block {
margin-bottom: 20px;
@ -661,7 +671,9 @@ h4.stream_setting_subsection_title {
color: hsl(0, 0%, 67%);
}
#groups_overlay,
#subscription_overlay {
#user-group-creation,
#stream-creation {
max-height: calc(100% - 102px);
overflow: auto;
@ -675,14 +687,17 @@ h4.stream_setting_subsection_title {
padding-top: 9px;
}
.user-group-creation-body,
.stream-creation-body {
padding: 15px;
}
.add_all_users_to_user_group,
.add_all_users_to_stream {
margin-left: 10px;
}
.create_user_group_member_list_header,
.create_stream_subscriber_list_header {
margin-top: 10px;
margin-bottom: 3px;
@ -697,9 +712,11 @@ h4.stream_setting_subsection_title {
float: right;
}
#user_group_creation_form,
#stream_creation_form {
margin: 0;
#user_group_creating_indicator,
#stream_creating_indicator {
&:not(:empty) {
position: absolute;

View File

@ -0,0 +1,27 @@
<div class="member_list_add float-left">
{{> ../stream_settings/add_subscribers_form}}
</div>
<br />
{{t "Do you want to add everyone?"}}
<button class="add_all_users_to_user_group small button rounded sea-green">{{t 'Add all users'}}</button>
<div class="create_member_list_header">
<h4 class="user_group_setting_subsection_title">{{t 'Members' }}</h4>
<input class="add-user-list-filter" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter members' }}" />
</div>
<div class="member-list-box">
<div class="member_list_container" data-simplebar>
<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>
<th>{{t "Action" }}</th>
</thead>
<tbody id="create_user_group_members" class="member_table"></tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,35 @@
<div class="hide" id="user-group-creation" tabindex="-1" role="dialog"
aria-label="{{t 'User group creation' }}">
<form id="user_group_creation_form">
<div class="alert user_group_create_info"></div>
<div id="user_group_creating_indicator"></div>
<div class="user-group-creation-body">
<section class="block">
<label for="create_user_group_name">
{{t "User group name" }}
</label>
<input type="text" name="user_group_name" id="create_user_group_name"
placeholder="{{t 'User group name' }}" value="" autocomplete="off" />
<div id="user_group_name_error" class="user_group_creation_error"></div>
</section>
<section class="block">
<label for="create_user_group_description">
{{t "User group description" }}
</label>
<input type="text" name="user_group_description" id="create_user_group_description"
placeholder="{{t 'User group description' }}" value="" autocomplete="off" />
</section>
<section class="block">
<label for="people_to_add_in_group">
<h4 class="user_group_setting_subsection_title">{{t "Choose members" }}</h4>
</label>
<div id="user_group_membership_error" class="user_group_creation_error"></div>
<div class="controls" id="people_to_add_in_group"></div>
</section>
</div>
<div class="modal-footer">
<button class="button small white rounded" data-dismiss="modal">{{t "Cancel" }}</button>
<button class="finalize_create_user_group button small sea-green rounded" type="submit">{{t "Create" }}</button>
</div>
</form>
</div>

View File

@ -37,6 +37,7 @@
<div id="user_group_settings" class="settings" data-simplebar data-simplebar-auto-hide="false">
{{!-- edit user group here --}}
</div>
{{> user_group_creation_form }}
</div>
</div>
</div>

View File

@ -200,6 +200,9 @@ EXEMPT_FILES = make_set(
"static/js/unread_ops.js",
"static/js/unread_ui.js",
"static/js/upload_widget.ts",
"static/js/user_group_create.js",
"static/js/user_group_create_members.js",
"static/js/user_group_create_members_data.js",
"static/js/user_groups_settings_ui.js",
"static/js/user_profile.js",
"static/js/user_settings.ts",