stream create: Overhaul create-stream add-subscribers UI.

The most notable change here is that when you are adding
subscribers to a stream as part of creating the stream,
you can now use the same essential pill-based UI for
adding users as we do when you edit subscribers for an
existing stream.

We don't try to exactly mimic the edit-stream UI or
implementation, since when you are adding subscribers
during create-stream, we are just updating a list in
memory, whereas in the edit-stream UI, we immediately
send info to the server.

Fixes #20499
This commit is contained in:
Steve Howell 2022-02-08 17:56:40 +00:00 committed by Tim Abbott
parent 43ee1f7b93
commit c43d48b22f
16 changed files with 353 additions and 399 deletions

View File

@ -163,13 +163,6 @@ const bob = {
full_name: "Bob van Roberts", full_name: "Bob van Roberts",
}; };
const alice2 = {
email: "alice2@example.com",
delivery_email: "alice2-delivery@example.com",
user_id: 204,
full_name: "Alice",
};
const charles = { const charles = {
email: "charles@example.com", email: "charles@example.com",
user_id: 301, user_id: 301,
@ -582,77 +575,6 @@ test_people("set_custom_profile_field_data", () => {
assert.equal(person.profile_data[field.id].rendered_value, "<p>Field value</p>"); assert.equal(person.profile_data[field.id].rendered_value, "<p>Field value</p>");
}); });
test_people("get_people_for_stream_create", () => {
people.add_active_user(alice1);
people.add_active_user(bob);
people.add_active_user(alice2);
assert.equal(people.get_active_human_count(), 4);
page_params.is_admin = true;
page_params.realm_email_address_visibility = admins_only;
let others = people.get_people_for_stream_create();
let expected = [
{
email: "alice1-delivery@example.com",
user_id: alice1.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: true,
},
{
email: "alice2-delivery@example.com",
user_id: alice2.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: true,
},
{
email: "bob-delivery@example.com",
user_id: bob.user_id,
full_name: "Bob van Roberts",
checked: false,
disabled: false,
show_email: true,
},
];
assert.deepEqual(others, expected);
page_params.is_admin = false;
alice1.delivery_email = undefined;
alice2.delivery_email = undefined;
bob.delivery_email = undefined;
others = people.get_people_for_stream_create();
expected = [
{
email: "alice1@example.com",
user_id: alice1.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: false,
},
{
email: "alice2@example.com",
user_id: alice2.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: false,
},
{
email: "bob@example.com",
user_id: bob.user_id,
full_name: "Bob van Roberts",
checked: false,
disabled: false,
show_email: false,
},
];
assert.deepEqual(others, expected);
});
test_people("recipient_counts", () => { test_people("recipient_counts", () => {
const user_id = 99; const user_id = 99;
assert.equal(people.get_recipient_count({user_id}), 0); assert.equal(people.get_recipient_count({user_id}), 0);
@ -672,7 +594,7 @@ test_people("filtered_users", () => {
people.add_active_user(plain_noah); people.add_active_user(plain_noah);
const search_term = "a"; const search_term = "a";
const users = people.get_people_for_stream_create(); const users = people.get_realm_users();
let filtered_people = people.filter_people_by_search_terms(users, [search_term]); let filtered_people = people.filter_people_by_search_terms(users, [search_term]);
assert.equal(filtered_people.size, 2); assert.equal(filtered_people.size, 2);
assert.ok(filtered_people.has(ashton.user_id)); assert.ok(filtered_people.has(ashton.user_id));

View File

@ -0,0 +1,80 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const {page_params} = require("../zjsunit/zpage_params");
const people = zrequire("people");
const stream_create_subscribers_data = zrequire("stream_create_subscribers_data");
const me = {
email: "me@zulip.com",
full_name: "Zed", // Zed will sort to the top by virtue of being the current user.
user_id: 400,
};
const test_user101 = {
email: "test101@zulip.com",
full_name: "Test User 101",
user_id: 101,
};
const test_user102 = {
email: "test102@zulip.com",
full_name: "Test User 102",
user_id: 102,
};
const test_user103 = {
email: "test102@zulip.com",
full_name: "Test User 103",
user_id: 103,
};
function test(label, f) {
run_test(label, ({override, override_rewire}) => {
page_params.is_admin = false;
people.init();
people.add_active_user(me);
people.add_active_user(test_user101);
people.add_active_user(test_user102);
people.add_active_user(test_user103);
page_params.user_id = me.user_id;
people.initialize_current_user(me.user_id);
f({override, override_rewire});
});
}
test("basics", () => {
stream_create_subscribers_data.initialize_with_current_user();
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [me.user_id]);
assert.deepEqual(stream_create_subscribers_data.get_principals(), [me.user_id]);
const all_user_ids = stream_create_subscribers_data.get_all_user_ids();
assert.deepEqual(all_user_ids, [101, 102, 103, 400]);
stream_create_subscribers_data.add_user_ids(all_user_ids);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [400, 101, 102, 103]);
stream_create_subscribers_data.remove_user_ids([101, 103]);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [400, 102]);
assert.deepEqual(stream_create_subscribers_data.get_potential_subscribers(), [
test_user101,
test_user103,
]);
assert.ok(stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
});
test("must_be_subscribed", () => {
page_params.is_admin = false;
assert.ok(stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
page_params.is_admin = true;
assert.ok(!stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
});

View File

@ -4,33 +4,29 @@ import type {ElementHandle, Page} from "puppeteer";
import common from "../puppeteer_lib/common"; import common from "../puppeteer_lib/common";
async function user_checkbox(page: Page, name: string): Promise<string> { async function user_row_selector(page: Page, name: string): Promise<string> {
const user_id = await common.get_user_id_from_name(page, name); const user_id = await common.get_user_id_from_name(page, name);
return `#user_checkbox_${CSS.escape(user_id.toString())}`; const selector = `.remove_potential_subscriber[data-user-id="${user_id}"]`;
return selector;
} }
async function user_span(page: Page, name: string): Promise<string> { async function await_user_visible(page: Page, name: string): Promise<void> {
return (await user_checkbox(page, name)) + " span"; const selector = await user_row_selector(page, name);
await page.waitForSelector(selector, {visible: true});
} }
async function stream_checkbox(page: Page, stream_name: string): Promise<string> { async function await_user_hidden(page: Page, name: string): Promise<void> {
const stream_id = await common.get_stream_id(page, stream_name); const selector = await user_row_selector(page, name);
return `#stream-checkboxes [data-stream-id="${CSS.escape(stream_id.toString())}"]`; await page.waitForSelector(selector, {hidden: true});
} }
async function stream_span(page: Page, stream_name: string): Promise<string> { async function add_user_to_stream(page: Page, name: string): Promise<void> {
return (await stream_checkbox(page, stream_name)) + " input ~ span"; const user_id = await common.get_user_id_from_name(page, name);
} await page.evaluate(
(user_id: Number) => zulip_test.add_user_id_to_new_stream(user_id),
async function wait_for_checked(page: Page, user_name: string, is_checked: boolean): Promise<void> { user_id,
const selector = await user_checkbox(page, user_name);
await page.waitForFunction(
(selector: string, is_checked: boolean) =>
$(selector).find("input").prop("checked") === is_checked,
{},
selector,
is_checked,
); );
await await_user_visible(page, name);
} }
async function stream_name_error(page: Page): Promise<string> { async function stream_name_error(page: Page): Promise<string> {
@ -83,29 +79,9 @@ async function test_subscription_button(page: Page): Promise<void> {
button = await subscribed(); button = await subscribed();
} }
async function click_create_new_stream( async function click_create_new_stream(page: Page): Promise<void> {
page: Page,
cordelia_checkbox: string,
othello_checkbox: string,
): Promise<void> {
await page.click("#add_new_subscription .create_stream_button"); await page.click("#add_new_subscription .create_stream_button");
await page.waitForSelector(cordelia_checkbox, {visible: true}); await await_user_visible(page, "desdemona");
await page.waitForSelector(othello_checkbox, {visible: true});
}
async function open_copy_from_stream_dropdown(
page: Page,
scotland_checkbox: string,
rome_checkbox: string,
): Promise<void> {
await page.click("#copy-from-stream-expand-collapse .control-label");
await page.waitForSelector(scotland_checkbox, {visible: true});
await page.waitForSelector(rome_checkbox, {visible: true});
}
async function verify_check_all_only_affects_visible_users(page: Page): Promise<void> {
await wait_for_checked(page, "cordelia", false);
await wait_for_checked(page, "othello", true);
} }
async function clear_ot_filter_with_backspace(page: Page): Promise<void> { async function clear_ot_filter_with_backspace(page: Page): Promise<void> {
@ -114,52 +90,33 @@ async function clear_ot_filter_with_backspace(page: Page): Promise<void> {
await page.keyboard.press("Backspace"); await page.keyboard.press("Backspace");
} }
async function verify_filtered_users_are_visible_again( async function test_user_filter_ui(page: Page): Promise<void> {
page: Page,
cordelia_checkbox: string,
othello_checkbox: string,
): Promise<void> {
await page.waitForSelector(cordelia_checkbox, {visible: true});
await page.waitForSelector(othello_checkbox, {visible: true});
}
async function test_user_filter_ui(
page: Page,
cordelia_checkbox: string,
othello_checkbox: string,
scotland_checkbox: string,
rome_checkbox: string,
): Promise<void> {
await page.waitForSelector("form#stream_creation_form", {visible: true}); await page.waitForSelector("form#stream_creation_form", {visible: true});
// Desdemona should be checked by default // Desdemona should be there by default
await wait_for_checked(page, "desdemona", true); await await_user_visible(page, "desdemona");
await add_user_to_stream(page, "cordelia");
await add_user_to_stream(page, "othello");
await page.type(`form#stream_creation_form [name="user_list_filter"]`, "ot", {delay: 100}); await page.type(`form#stream_creation_form [name="user_list_filter"]`, "ot", {delay: 100});
await page.waitForSelector("#user-checkboxes", {visible: true}); await page.waitForSelector("#create_stream_subscribers", {visible: true});
// Wait until filtering is completed. // Wait until filtering is completed.
await page.waitForFunction( await page.waitForFunction(
() => document.querySelectorAll("#user-checkboxes label").length === 1, () =>
document.querySelectorAll("#create_stream_subscribers .remove_potential_subscriber")
.length === 1,
); );
await page.waitForSelector(cordelia_checkbox, {hidden: true}); await await_user_hidden(page, "cordelia");
await page.waitForSelector(othello_checkbox, {visible: true}); await await_user_hidden(page, "desdemona");
await await_user_visible(page, "othello");
// Filter shouldn't affect streams. // Clear the filter.
await page.waitForSelector(scotland_checkbox, {visible: true});
await page.waitForSelector(rome_checkbox, {visible: true});
// Test check all
await page.click(".subs_set_all_users");
await wait_for_checked(page, "othello", true);
await clear_ot_filter_with_backspace(page); await clear_ot_filter_with_backspace(page);
await verify_filtered_users_are_visible_again(page, cordelia_checkbox, othello_checkbox);
await verify_check_all_only_affects_visible_users(page);
// Test unset all await await_user_visible(page, "cordelia");
await page.click(".subs_unset_all_users"); await await_user_visible(page, "desdemona");
await verify_filtered_users_are_visible_again(page, cordelia_checkbox, othello_checkbox); await await_user_visible(page, "othello");
await wait_for_checked(page, "cordelia", false);
await wait_for_checked(page, "othello", false);
} }
async function create_stream(page: Page): Promise<void> { async function create_stream(page: Page): Promise<void> {
@ -168,13 +125,6 @@ async function create_stream(page: Page): Promise<void> {
stream_name: "Puppeteer", stream_name: "Puppeteer",
stream_description: "Everything Puppeteer", stream_description: "Everything Puppeteer",
}); });
await page.click(await stream_span(page, "Scotland")); // Subscribes all users from Scotland
await page.click(await user_span(page, "cordelia")); // Add cordelia.
await page.click(await user_span(page, "desdemona")); // Add cordelia.
await page.click(await user_span(page, "othello")); // Remove othello who was selected from Scotland.
await wait_for_checked(page, "cordelia", true);
await wait_for_checked(page, "desdemona", true); // Add desdemona back as we did unset all in last test.
await wait_for_checked(page, "othello", false);
await page.click("form#stream_creation_form .finalize_create_stream"); await page.click("form#stream_creation_form .finalize_create_stream");
await page.waitForFunction(() => $(".stream-name").is(':contains("Puppeteer")')); await page.waitForFunction(() => $(".stream-name").is(':contains("Puppeteer")'));
const stream_name = await common.get_text_from_selector( const stream_name = await common.get_text_from_selector(
@ -189,9 +139,9 @@ async function create_stream(page: Page): Promise<void> {
assert.strictEqual(stream_name, "Puppeteer"); assert.strictEqual(stream_name, "Puppeteer");
assert.strictEqual(stream_description, "Everything Puppeteer"); assert.strictEqual(stream_description, "Everything Puppeteer");
// Assert subscriber count becomes 6(scotland(+5), cordelia(+1), othello(-1), Desdemona(+1)). // Assert subscriber count becomes 3 (cordelia, desdemona, othello)
await page.waitForFunction( await page.waitForFunction(
(subscriber_count_selector: string) => $(subscriber_count_selector).text().trim() === "6", (subscriber_count_selector: string) => $(subscriber_count_selector).text().trim() === "3",
{}, {},
subscriber_count_selector, subscriber_count_selector,
); );
@ -201,13 +151,13 @@ async function test_streams_with_empty_names_cannot_be_created(page: Page): Prom
await page.click("#add_new_subscription .create_stream_button"); await page.click("#add_new_subscription .create_stream_button");
await page.waitForSelector("form#stream_creation_form", {visible: true}); await page.waitForSelector("form#stream_creation_form", {visible: true});
await common.fill_form(page, "form#stream_creation_form", {stream_name: " "}); await common.fill_form(page, "form#stream_creation_form", {stream_name: " "});
await page.click("form#stream_creation_form button.button.sea-green"); await page.click("form#stream_creation_form button.finalize_create_stream");
assert.strictEqual(await stream_name_error(page), "A stream needs to have a name"); assert.strictEqual(await stream_name_error(page), "A stream needs to have a name");
} }
async function test_streams_with_duplicate_names_cannot_be_created(page: Page): Promise<void> { async function test_streams_with_duplicate_names_cannot_be_created(page: Page): Promise<void> {
await common.fill_form(page, "form#stream_creation_form", {stream_name: "Puppeteer"}); await common.fill_form(page, "form#stream_creation_form", {stream_name: "Puppeteer"});
await page.click("form#stream_creation_form button.button.sea-green"); await page.click("form#stream_creation_form button.finalize_create_stream");
assert.strictEqual(await stream_name_error(page), "A stream with this name already exists"); assert.strictEqual(await stream_name_error(page), "A stream with this name already exists");
const cancel_button_selector = "form#stream_creation_form button.button.white"; const cancel_button_selector = "form#stream_creation_form button.button.white";
@ -215,20 +165,8 @@ async function test_streams_with_duplicate_names_cannot_be_created(page: Page):
} }
async function test_stream_creation(page: Page): Promise<void> { async function test_stream_creation(page: Page): Promise<void> {
const cordelia_checkbox = await user_checkbox(page, "cordelia"); await click_create_new_stream(page);
const othello_checkbox = await user_checkbox(page, "othello"); await test_user_filter_ui(page);
const scotland_checkbox = await stream_checkbox(page, "Scotland");
const rome_checkbox = await stream_checkbox(page, "Rome");
await click_create_new_stream(page, cordelia_checkbox, othello_checkbox);
await open_copy_from_stream_dropdown(page, scotland_checkbox, rome_checkbox);
await test_user_filter_ui(
page,
cordelia_checkbox,
othello_checkbox,
scotland_checkbox,
rome_checkbox,
);
await create_stream(page); await create_stream(page);
await test_streams_with_empty_names_cannot_be_created(page); await test_streams_with_empty_names_cannot_be_created(page);
await test_streams_with_duplicate_names_cannot_be_created(page); await test_streams_with_duplicate_names_cannot_be_created(page);

View File

@ -1067,44 +1067,6 @@ export function get_user_id_from_name(full_name) {
return person.user_id; return person.user_id;
} }
function people_cmp(person1, person2) {
const name_cmp = util.strcmp(person1.full_name, person2.full_name);
if (name_cmp < 0) {
return -1;
} else if (name_cmp > 0) {
return 1;
}
return util.strcmp(person1.email, person2.email);
}
export function get_people_for_stream_create() {
/*
If you are thinking of reusing this function,
a better option in most cases is to just
call `get_realm_users()` and then filter out
the "me" user yourself as part of any other
filtering that you are doing.
In particular, this function does a sort
that is kinda expensive and may not apply
to your use case.
*/
const people_minus_you = [];
for (const person of active_user_dict.values()) {
if (!is_my_user_id(person.user_id)) {
people_minus_you.push({
email: get_visible_email(person),
show_email: settings_data.show_email(),
user_id: person.user_id,
full_name: person.full_name,
checked: false,
disabled: false,
});
}
}
return people_minus_you.sort(people_cmp);
}
export function track_duplicate_full_name(full_name, user_id, to_remove) { export function track_duplicate_full_name(full_name, user_id, to_remove) {
let ids; let ids;
if (duplicate_full_name_data.has(full_name)) { if (duplicate_full_name_data.has(full_name)) {

View File

@ -2,26 +2,19 @@ import $ from "jquery";
import render_announce_stream_docs from "../templates/announce_stream_docs.hbs"; import render_announce_stream_docs from "../templates/announce_stream_docs.hbs";
import render_subscription_invites_warning_modal from "../templates/confirm_dialog/confirm_subscription_invites_warning.hbs"; import render_subscription_invites_warning_modal from "../templates/confirm_dialog/confirm_subscription_invites_warning.hbs";
import render_new_stream_user from "../templates/new_stream_user.hbs";
import render_new_stream_users from "../templates/stream_settings/new_stream_users.hbs";
import * as channel from "./channel"; import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog"; import * as confirm_dialog from "./confirm_dialog";
import {$t, $t_html} from "./i18n"; import {$t, $t_html} from "./i18n";
import * as ListWidget from "./list_widget";
import * as loading from "./loading"; import * as loading from "./loading";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
import * as peer_data from "./peer_data";
import * as people from "./people"; import * as people from "./people";
import * as settings_data from "./settings_data"; import * as stream_create_subscribers from "./stream_create_subscribers";
import * as stream_data from "./stream_data"; import * as stream_data from "./stream_data";
import * as stream_settings_data from "./stream_settings_data";
import * as stream_settings_ui from "./stream_settings_ui"; import * as stream_settings_ui from "./stream_settings_ui";
import * as ui_report from "./ui_report"; import * as ui_report from "./ui_report";
let created_stream; let created_stream;
let all_users;
let all_users_list_widget;
export function reset_created_stream() { export function reset_created_stream() {
created_stream = undefined; created_stream = undefined;
@ -146,11 +139,6 @@ function update_announce_stream_state() {
$("#announce-new-stream").show(); $("#announce-new-stream").show();
} }
function get_principals() {
// Return list of user ids which were selected by user.
return all_users.filter((user) => user.checked === true).map((user) => user.user_id);
}
function create_stream() { function create_stream() {
const data = {}; const data = {};
const stream_name = $("#create_stream_name").val().trim(); const stream_name = $("#create_stream_name").val().trim();
@ -233,7 +221,7 @@ function create_stream() {
// TODO: We can eliminate the user_ids -> principals conversion // TODO: We can eliminate the user_ids -> principals conversion
// once we upgrade the backend to accept user_ids. // once we upgrade the backend to accept user_ids.
const user_ids = get_principals(); const user_ids = stream_create_subscribers.get_principals();
data.principals = JSON.stringify(user_ids); data.principals = JSON.stringify(user_ids);
loading.make_indicator($("#stream_creating_indicator"), { loading.make_indicator($("#stream_creating_indicator"), {
@ -303,40 +291,7 @@ export function show_new_stream_modal() {
$(".right .settings").hide(); $(".right .settings").hide();
stream_settings_ui.hide_or_disable_stream_privacy_options_if_required($("#stream-creation")); stream_settings_ui.hide_or_disable_stream_privacy_options_if_required($("#stream-creation"));
const add_people_container = $("#people_to_add"); stream_create_subscribers.build_widgets();
add_people_container.html(
render_new_stream_users({
streams: stream_settings_data.get_streams_for_settings_page(),
}),
);
all_users = people.get_people_for_stream_create();
// Add current user on top of list
const current_user = people.get_by_user_id(page_params.user_id);
all_users.unshift({
show_email: settings_data.show_email(),
email: people.get_visible_email(current_user),
user_id: current_user.user_id,
full_name: current_user.full_name,
checked: true,
disabled: !page_params.is_admin,
});
all_users_list_widget = ListWidget.create($("#user-checkboxes"), all_users, {
name: "new_stream_add_users",
parent_container: add_people_container,
modifier(item) {
return render_new_stream_user(item);
},
filter: {
element: $("#people_to_add .add-user-list-filter"),
predicate(user, search_term) {
return people.build_person_matcher(search_term)(user);
},
},
simplebar_container: $("#user-checkboxes-simplebar-wrapper"),
html_selector: (user) => $(`#${CSS.escape("user_checkbox_" + user.user_id)}`),
});
// Select the first visible and enabled choice for stream privacy. // Select the first visible and enabled choice for stream privacy.
$("#make-invite-only input:visible:not([disabled]):first").prop("checked", true); $("#make-invite-only input:visible:not([disabled]):first").prop("checked", true);
@ -365,82 +320,9 @@ export function show_new_stream_modal() {
clear_error_display(); clear_error_display();
} }
function create_handlers_for_users(container) {
// container should be $('#people_to_add')...see caller to verify
function update_checked_state_for_users(value, users) {
// Update the all_users backing data structure for
// which users will be submitted should the user click save,
// and also ensure that any visible checkboxes reflect
// the state of that data structure.
// If we have to rerender a very large number of users, it's
// eventually faster to just do a full redraw rather than
// many hundreds of single-item rerenders.
const full_redraw = !users || users.length > 250;
for (const user of all_users) {
// We don't want to uncheck the user creating the stream if it is not admin.
if (user.user_id === page_params.user_id && value === false && !page_params.is_admin) {
continue;
}
// We update for all users if `users` parameter is empty.
if (users === undefined || users.includes(user.user_id)) {
user.checked = value;
if (!full_redraw) {
all_users_list_widget.render_item(user);
}
}
}
if (full_redraw) {
all_users_list_widget.hard_redraw();
}
}
container.on("change", "#user-checkboxes input", (e) => {
const elem = $(e.target);
const user_id = Number.parseInt(elem.attr("data-user-id"), 10);
const checked = elem.prop("checked");
update_checked_state_for_users(checked, [user_id]);
});
// 'Check all' and 'Uncheck all' visible users
container.on("click", ".subs_set_all_users, .subs_unset_all_users", (e) => {
e.preventDefault();
// Only `check / uncheck` users who are displayed.
const mark_checked = e.target.classList.contains("subs_set_all_users");
const users_displayed = all_users_list_widget.get_current_list();
if (all_users.length !== users_displayed.length) {
update_checked_state_for_users(
mark_checked,
users_displayed.map((user) => user.user_id),
);
} else {
update_checked_state_for_users(mark_checked);
}
});
container.on("click", "#copy-from-stream-expand-collapse", (e) => {
e.preventDefault();
$("#stream-checkboxes").toggle();
$("#copy-from-stream-expand-collapse .toggle").toggleClass("fa-caret-right fa-caret-down");
});
container.on("change", "#stream-checkboxes label.checkbox", (e) => {
e.preventDefault();
const elem = $(e.target).closest("[data-stream-id]");
const stream_id = Number.parseInt(elem.attr("data-stream-id"), 10);
const checked = elem.find("input").prop("checked");
const subscriber_ids = peer_data.get_subscribers(stream_id);
update_checked_state_for_users(checked, subscriber_ids);
});
}
export function set_up_handlers() { export function set_up_handlers() {
// Sets up all the event handlers concerning the `People to add`
// section in Create stream UI.
const people_to_add_holder = $("#people_to_add").expectOne(); const people_to_add_holder = $("#people_to_add").expectOne();
create_handlers_for_users(people_to_add_holder); stream_create_subscribers.create_handlers(people_to_add_holder);
const container = $("#stream-creation").expectOne(); const container = $("#stream-creation").expectOne();
@ -457,7 +339,7 @@ export function set_up_handlers() {
return; return;
} }
const principals = get_principals(); const principals = stream_create_subscribers.get_principals();
if (principals.length === 0) { if (principals.length === 0) {
stream_subscription_error.report_no_subs_to_stream(); stream_subscription_error.report_no_subs_to_stream();
return; return;

View File

@ -0,0 +1,118 @@
import $ from "jquery";
import render_new_stream_user from "../templates/stream_settings/new_stream_user.hbs";
import render_new_stream_users from "../templates/stream_settings/new_stream_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 stream_create_subscribers_data from "./stream_create_subscribers_data";
let pill_widget;
let all_users_list_widget;
export function get_principals() {
return stream_create_subscribers_data.get_principals();
}
function redraw_subscriber_list() {
all_users_list_widget.replace_list_data(stream_create_subscribers_data.sorted_user_ids());
}
function add_user_ids(user_ids) {
stream_create_subscribers_data.add_user_ids(user_ids);
redraw_subscriber_list();
}
function add_all_users() {
const user_ids = stream_create_subscribers_data.get_all_user_ids();
add_user_ids(user_ids);
}
function remove_user_ids(user_ids) {
stream_create_subscribers_data.remove_user_ids(user_ids);
redraw_subscriber_list();
}
function build_pill_widget({parent_container}) {
const pill_container = parent_container.find(".pill-container");
const get_potential_subscribers = stream_create_subscribers_data.get_potential_subscribers;
pill_widget = add_subscribers_pill.create({pill_container, get_potential_subscribers});
}
export function create_handlers(container) {
container.on("click", ".add_all_users_to_stream", (e) => {
e.preventDefault();
add_all_users();
$(".add-user-list-filter").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");
add_people_container.html(render_new_stream_users({}));
const simplebar_container = add_people_container.find(".subscriber_list_container");
build_pill_widget({parent_container: add_people_container});
stream_create_subscribers_data.initialize_with_current_user();
const current_user_id = page_params.user_id;
all_users_list_widget = ListWidget.create($("#create_stream_subscribers"), [current_user_id], {
name: "new_stream_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: stream_create_subscribers_data.must_be_subscribed(user_id),
};
return render_new_stream_user(item);
},
filter: {
element: $("#people_to_add .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)}`);
},
});
}
export function add_user_id_to_new_stream(user_id) {
// This is only used by puppeteer tests.
add_user_ids([user_id]);
}

View File

@ -0,0 +1,55 @@
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_subscribers = people.get_realm_users();
const user_ids = potential_subscribers.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_subscribers() {
const potential_subscribers = people.get_realm_users();
return potential_subscribers.filter((user) => !user_id_set.has(user.user_id));
}
export function must_be_subscribed(user_id) {
return !page_params.is_admin && user_id === page_params.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

@ -14,3 +14,4 @@ export {last_visible as last_visible_row, id as row_id} from "./rows";
export {cancel as cancel_compose} from "./compose_actions"; export {cancel as cancel_compose} from "./compose_actions";
export {page_params, page_params_parse_time} from "./page_params"; export {page_params, page_params_parse_time} from "./page_params";
export {initiate as initiate_reload} from "./reload"; export {initiate as initiate_reload} from "./reload";
export {add_user_id_to_new_stream} from "./stream_create_subscribers";

View File

@ -805,8 +805,22 @@ h4.stream_setting_subsection_title {
margin: 8px 0; margin: 8px 0;
} }
.add_all_users_to_stream {
margin-left: 10px;
}
.create_stream_subscriber_list_header {
margin-top: 10px;
margin-bottom: 3px;
h5 {
display: inline-block;
}
}
.add-user-list-filter { .add-user-list-filter {
width: calc(100% - 10px); width: 140px;
float: right;
} }
#stream_creation_form { #stream_creation_form {

View File

@ -2298,12 +2298,7 @@ div.floating_recipient {
color: hsl(0, 0%, 100%); color: hsl(0, 0%, 100%);
} }
#user-checkboxes-simplebar-wrapper { #create_stream_subscribers {
max-height: 500px;
overflow-y: auto;
}
#user-checkboxes {
margin-top: 10px; margin-top: 10px;
.checkbox { .checkbox {
@ -2316,24 +2311,6 @@ div.floating_recipient {
} }
} }
#stream-checkboxes {
margin-top: 10px;
display: none;
.checkbox {
display: block;
}
input[type="checkbox"] {
margin: 5px 0;
float: none;
}
}
#copy-from-stream-expand-collapse {
cursor: pointer;
}
.sub_button_row { .sub_button_row {
text-align: center; text-align: center;
} }

View File

@ -1,5 +0,0 @@
<label class="checkbox add-user-label" id="user_checkbox_{{user_id}}">
<input type="checkbox" name="user" {{#if checked}}checked="checked"{{#if disabled}} disabled="disabled"{{/if}}{{/if}} data-user-id="{{user_id}}"/>
<span></span>
{{full_name}} {{#if show_email}}({{email}}){{else}}({{#tr}}User ID: {user_id}; <em>email hidden</em>{{/tr}}){{/if}}
</label>

View File

@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="add_subscriber_btn_wrapper inline-block"> <div class="add_subscriber_btn_wrapper inline-block">
<button type="submit" name="add_subscriber" class="button add-subscriber-button small rounded" tabindex="0"> <button type="submit" name="add_subscriber" class="button add-subscriber-button small rounded sea-green" tabindex="0">
{{t 'Add' }} {{t 'Add' }}
</button> </button>
</div> </div>

View File

@ -0,0 +1,14 @@
<tr>
<td>
{{full_name}}{{#if is_current_user}} <span class="my_user_status">{{t "(you)"}}</span>{{/if}}
</td>
{{#if show_email}}
<td class="subscriber-email">{{email}}</td>
{{else}}
<td class="hidden-subscriber-email">{{t "(hidden)"}}</td>
{{/if}}
<td>{{user_id}} </td>
<td>
<button {{#if disabled}} disabled="disabled"{{/if}} data-user-id="{{user_id}}" class="remove_potential_subscriber button small rounded btn-danger">Remove</button>
</td>
</tr>

View File

@ -1,32 +1,27 @@
{{! Client-side Mustache template for rendering users in the stream creation modal.}} <div class="subscriber_list_add float-left">
{{> add_subscribers_form}}
<div id="copy-from-stream-expand-collapse" class="add-user-label">
<i class="toggle fa fa-caret-right" aria-hidden="true"></i>
<span class="control-label">
{{t "Copy from stream" }}
</span>
</div> </div>
<div id="stream-checkboxes">
{{#each streams}}
<label class="checkbox add-user-label" data-stream-id="{{this.stream_id}}">
<input type="checkbox" name="stream" />
<span></span>
{{this.name}} ( <i class="fa fa-user" aria-hidden="true"></i> {{this.subscriber_count}})
</label>
{{/each}}
</div>
<br /> <br />
<input class="add-user-list-filter" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter' }}" />
{{t "Do you want to add everyone?"}}
<button class="add_all_users_to_stream small button rounded sea-green">{{t 'Add all users'}}</button>
<div> <div class="create_stream_subscriber_list_header">
<a draggable="false" class="subs_set_all_users" tabindex="0">{{t "Check all" }}</a> | <h5>Subscribers</h5>
<a draggable="false" class="subs_unset_all_users" tabindex="0">{{t "Uncheck all" }}</a> <input class="add-user-list-filter" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter subscribers' }}" />
</div> </div>
<div id="user-checkboxes-simplebar-wrapper" data-simplebar> <div class="subscriber-list-box">
<div id="user-checkboxes"></div> <div class="subscriber_list_container" data-simplebar>
<table class="subscriber-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_stream_subscribers" class="subscriber_table"></tbody>
</table>
</div>
</div> </div>

View File

@ -28,7 +28,7 @@
</section> </section>
<section class="block"> <section class="block">
<label class="stream-title" for="people_to_add"> <label class="stream-title" for="people_to_add">
{{t "People to add" }} <h4>{{t "Choose subscribers" }}</h4>
</label> </label>
<div id="stream_subscription_error" class="stream_creation_error"></div> <div id="stream_subscription_error" class="stream_creation_error"></div>
<div class="controls" id="people_to_add"></div> <div class="controls" id="people_to_add"></div>

View File

@ -167,6 +167,7 @@ EXEMPT_FILES = make_set(
"static/js/stream_bar.js", "static/js/stream_bar.js",
"static/js/stream_color.js", "static/js/stream_color.js",
"static/js/stream_create.js", "static/js/stream_create.js",
"static/js/stream_create_subscribers.js",
"static/js/stream_edit.js", "static/js/stream_edit.js",
"static/js/stream_edit_subscribers.js", "static/js/stream_edit_subscribers.js",
"static/js/stream_list.js", "static/js/stream_list.js",