diff --git a/static/js/ui_init.js b/static/js/ui_init.js index eb4cf31cca..1718e983e5 100644 --- a/static/js/ui_init.js +++ b/static/js/ui_init.js @@ -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(); diff --git a/static/js/user_group_create.js b/static/js/user_group_create.js new file mode 100644 index 0000000000..27f5b00778 --- /dev/null +++ b/static/js/user_group_create.js @@ -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(); + } + }); +} diff --git a/static/js/user_group_create_members.js b/static/js/user_group_create_members.js new file mode 100644 index 0000000000..b168d0019c --- /dev/null +++ b/static/js/user_group_create_members.js @@ -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)}`); + }, + }); +} diff --git a/static/js/user_group_create_members_data.js b/static/js/user_group_create_members_data.js new file mode 100644 index 0000000000..171f24ee59 --- /dev/null +++ b/static/js/user_group_create_members_data.js @@ -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); + } +} diff --git a/static/js/user_groups_settings_ui.js b/static/js/user_groups_settings_ui.js index 76e65f1500..e633b6088a 100644 --- a/static/js/user_groups_settings_ui.js +++ b/static/js/user_groups_settings_ui.js @@ -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({ diff --git a/static/styles/dark_theme.css b/static/styles/dark_theme.css index 4a94f2fd67..99243257a0 100644 --- a/static/styles/dark_theme.css +++ b/static/styles/dark_theme.css @@ -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, diff --git a/static/styles/subscriptions.css b/static/styles/subscriptions.css index 4ca3db5a01..5cf6986eb4 100644 --- a/static/styles/subscriptions.css +++ b/static/styles/subscriptions.css @@ -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; diff --git a/static/templates/user_group_settings/new_user_group_users.hbs b/static/templates/user_group_settings/new_user_group_users.hbs new file mode 100644 index 0000000000..b7bd449b3e --- /dev/null +++ b/static/templates/user_group_settings/new_user_group_users.hbs @@ -0,0 +1,27 @@ +
{{t "Name" }} | +{{t "Email" }} | +{{t "User ID" }} | +{{t "Action" }} | + + +
---|