setting(streams): Add pill support to subscribe all members of group.

We update the pills typeahead logic to also include user group
results and pass the "user_group" key in `opts` to enable this
option for Add subscriber form.

The changes includes:
* Exporting the `query_matches_name_description` function, to
  deduplicate the typeahead `matcher` logic.
* Creating a `people.is_known_user` function, to deduplicate
  the typeahead `sorter` logic.
* Add a new `user_group_pill.js`, to allow adding user group
  pills in input widgets that support pills.

This has been tested manually as well as by adding some new
node tests.
This commit is contained in:
m-e-l-u-h-a-n 2021-04-05 16:24:38 +05:30 committed by Tim Abbott
parent e4d5ea69a0
commit ce4cf66f3f
8 changed files with 197 additions and 31 deletions

View File

@ -242,6 +242,8 @@ test_people("basics", () => {
const email = "isaac@example.com";
assert(!people.is_known_user_id(32));
assert(!people.is_known_user(isaac));
assert(!people.is_known_user(undefined));
assert(!people.is_valid_full_name_and_user_id(full_name, 32));
assert.equal(people.get_user_id_from_name(full_name), undefined);
@ -251,6 +253,7 @@ test_people("basics", () => {
assert(people.is_valid_full_name_and_user_id(full_name, 32));
assert(people.is_known_user_id(32));
assert(people.is_known_user(isaac));
assert.equal(people.get_active_human_count(), 2);
assert.equal(people.get_user_id_from_name(full_name), 32);

View File

@ -150,6 +150,9 @@ test_ui("populate_user_groups", (override) => {
if (user_id === iago.user_id) {
return iago;
}
if (user_id === alice.user_id) {
return alice;
}
if (user_id === undefined) {
return noop;
}
@ -158,6 +161,9 @@ test_ui("populate_user_groups", (override) => {
get_by_user_id_called = true;
return undefined;
};
people.is_known_user = function () {
return people.get_by_user_id !== undefined && people.get_by_user_id !== noop;
};
override(settings_user_groups, "can_edit", () => true);
@ -236,7 +242,7 @@ test_ui("populate_user_groups", (override) => {
typeahead_helper.sort_recipients = function () {
sort_recipients_typeahead_called = true;
};
config.sorter.call(fake_context);
config.sorter.call(fake_context, []);
assert(sort_recipients_typeahead_called);
})();

View File

@ -34,6 +34,8 @@ const people = zrequire("people");
const stream_data = zrequire("stream_data");
const stream_edit = zrequire("stream_edit");
const stream_pill = zrequire("stream_pill");
const user_groups = zrequire("user_groups");
const user_group_pill = zrequire("user_group_pill");
const user_pill = zrequire("user_pill");
const jill = {
@ -62,6 +64,24 @@ for (const person of persons) {
people.add_active_user(person);
}
const admins = {
name: "Admins",
description: "foo",
id: 1,
members: [jill.user_id, mark.user_id],
};
const testers = {
name: "Testers",
description: "bar",
id: 2,
members: [mark.user_id, fred.user_id, me.user_id],
};
const groups = [admins, testers];
for (const group of groups) {
user_groups.add(group);
}
const denmark = {
stream_id: 1,
name: "Denmark",
@ -144,21 +164,44 @@ test_ui("subscriber_pills", (override) => {
assert.equal(typeof config.sorter, "function");
assert.equal(typeof config.updater, "function");
const fake_this = {
const fake_stream_this = {
query: "#Denmark",
};
const fake_person_this = {
query: "me",
};
const fake_group_this = {
query: "test",
};
(function test_highlighter() {
const fake_stream = $.create("fake-stream");
typeahead_helper.render_stream = () => fake_stream;
assert.equal(config.highlighter.call(fake_this, denmark), fake_stream);
const fake_html = $.create("fake-html");
typeahead_helper.render_stream = function () {
return fake_html;
};
assert.equal(config.highlighter.call(fake_stream_this, denmark), fake_html);
typeahead_helper.render_person_or_user_group = function () {
return fake_html;
};
assert.equal(config.highlighter.call(fake_group_this, testers), fake_html);
assert.equal(config.highlighter.call(fake_person_this, me), fake_html);
})();
(function test_matcher() {
let result = config.matcher.call(fake_this, denmark);
let result = config.matcher.call(fake_stream_this, denmark);
assert(result);
result = config.matcher.call(fake_stream_this, sweden);
assert(!result);
result = config.matcher.call(fake_this, sweden);
result = config.matcher.call(fake_group_this, testers);
assert(result);
result = config.matcher.call(fake_group_this, admins);
assert(!result);
result = config.matcher.call(fake_person_this, me);
assert(result);
result = config.matcher.call(fake_person_this, jill);
assert(!result);
})();
@ -167,8 +210,19 @@ test_ui("subscriber_pills", (override) => {
typeahead_helper.sort_streams = () => {
sort_streams_called = true;
};
config.sorter.call(fake_this);
config.sorter.call(fake_stream_this);
assert(sort_streams_called);
let sort_recipients_called = false;
typeahead_helper.sort_recipients = function () {
sort_recipients_called = true;
};
config.sorter.call(fake_group_this, [testers]);
assert(sort_recipients_called);
sort_recipients_called = false;
config.sorter.call(fake_person_this, [me]);
assert(sort_recipients_called);
})();
(function test_updater() {
@ -178,21 +232,29 @@ test_ui("subscriber_pills", (override) => {
}
assert.equal(number_of_pills(), 0);
config.updater.call(fake_this, denmark);
config.updater.call(fake_stream_this, denmark);
assert.equal(number_of_pills(), 1);
fake_this.query = me.email;
config.updater.call(fake_this, me);
config.updater.call(fake_person_this, me);
assert.equal(number_of_pills(), 2);
fake_this.query = "#Denmark";
config.updater.call(fake_group_this, testers);
assert.equal(number_of_pills(), 3);
})();
(function test_source() {
const result = config.source.call(fake_this);
const taken_ids = stream_pill.get_stream_ids(stream_edit.pill_widget);
const stream_ids = Array.from(result, (stream) => stream.stream_id).sort();
let expected_ids = Array.from(subs, (stream) => stream.stream_id).sort();
expected_ids = expected_ids.filter((id) => !taken_ids.includes(id));
assert.deepEqual(stream_ids, expected_ids);
let result = config.source.call(fake_stream_this);
const stream_ids = result.map((stream) => stream.stream_id);
const expected_stream_ids = [sweden.stream_id];
assert.deepEqual(stream_ids, expected_stream_ids);
result = config.source.call(fake_group_this);
const group_ids = result.map((group) => group.id).filter(Boolean);
const expected_group_ids = [admins.id];
assert.deepEqual(group_ids, expected_group_ids);
result = config.source.call(fake_person_this);
const user_ids = result.map((user) => user.user_id).filter(Boolean);
const expected_user_ids = [jill.user_id, fred.user_id];
assert.deepEqual(user_ids, expected_user_ids);
})();
input_typeahead_called = true;
@ -228,9 +290,10 @@ test_ui("subscriber_pills", (override) => {
peer_data.get_subscribers(denmark.stream_id),
).filter((id) => id !== me.user_id);
// denmark.stream_id is stubbed. Thus request is
// sent to add all subscribers of stream Denmark.
expected_user_ids = potential_denmark_stream_subscribers;
// `denmark` stream pill, `me` user pill and
// `testers` user group pill are stubbed.
// Thus request is sent to add all the users.
expected_user_ids = [mark.user_id, fred.user_id];
add_subscribers_handler(event);
add_subscribers_handler = $(subscriptions_table_selector).get_on_handler(
@ -242,6 +305,7 @@ test_ui("subscriber_pills", (override) => {
// Only Denmark stream pill is created and a
// request is sent to add all it's subscribers.
override(user_pill, "get_user_ids", () => []);
override(user_group_pill, "get_user_ids", () => []);
expected_user_ids = potential_denmark_stream_subscribers;
add_subscribers_handler(event);

View File

@ -89,7 +89,7 @@ export function query_matches_person(query, person) {
return typeahead.query_matches_source_attrs(query, person, ["full_name", email_attr], " ");
}
function query_matches_name_description(query, user_group_or_stream) {
export function query_matches_name_description(query, user_group_or_stream) {
return typeahead.query_matches_source_attrs(
query,
user_group_or_stream,

View File

@ -163,6 +163,10 @@ export function is_known_user_id(user_id) {
return people_by_user_id_dict.has(user_id);
}
export function is_known_user(user) {
return user && is_known_user_id(user.user_id);
}
function sort_numerically(user_ids) {
user_ids.sort((a, b) => a - b);

View File

@ -1,15 +1,32 @@
import * as composebox_typeahead from "./composebox_typeahead";
import * as people from "./people";
import * as settings_data from "./settings_data";
import * as stream_pill from "./stream_pill";
import * as typeahead_helper from "./typeahead_helper";
import * as user_group_pill from "./user_group_pill";
import * as user_groups from "./user_groups";
import * as user_pill from "./user_pill";
function person_matcher(query, item) {
if (people.is_known_user(item)) {
return composebox_typeahead.query_matches_person(query, item);
}
return undefined;
}
function group_matcher(query, item) {
if (user_groups.is_user_group(item)) {
return composebox_typeahead.query_matches_name_description(query, item);
}
return undefined;
}
export function set_up(input, pills, opts) {
let source = opts.source;
if (!opts.source) {
source = () => user_pill.typeahead_source(pills);
}
const include_streams = (query) => opts.stream && query.trim().startsWith("#");
const include_user_groups = opts.user_group;
input.typeahead({
items: 5,
@ -20,6 +37,10 @@ export function set_up(input, pills, opts) {
return stream_pill.typeahead_source(pills);
}
if (include_user_groups) {
return user_group_pill.typeahead_source(pills).concat(source());
}
return source();
},
highlighter(item) {
@ -27,6 +48,10 @@ export function set_up(input, pills, opts) {
return typeahead_helper.render_stream(item);
}
if (include_user_groups) {
return typeahead_helper.render_person_or_user_group(item);
}
return typeahead_helper.render_person(item);
},
matcher(item) {
@ -38,24 +63,36 @@ export function set_up(input, pills, opts) {
return item.name.toLowerCase().includes(query);
}
if (!settings_data.show_email()) {
return item.full_name.toLowerCase().includes(query);
if (include_user_groups) {
return group_matcher(query, item) || person_matcher(query, item);
}
const email = people.get_visible_email(item);
return (
email.toLowerCase().includes(query) || item.full_name.toLowerCase().includes(query)
);
return person_matcher(query, item);
},
sorter(matches) {
if (include_streams(this.query)) {
return typeahead_helper.sort_streams(matches, this.query.trim().slice(1));
}
return typeahead_helper.sort_recipients(matches, this.query, "");
const users = matches.filter((ele) => people.is_known_user(ele));
let groups;
if (include_user_groups) {
groups = matches.filter((ele) => user_groups.is_user_group(ele));
}
return typeahead_helper.sort_recipients(
users,
this.query,
"",
undefined,
groups,
undefined,
);
},
updater(item) {
if (include_streams(this.query)) {
stream_pill.append_stream(item, pills);
} else if (include_user_groups && user_groups.is_user_group(item)) {
user_group_pill.append_user_group(item, pills);
} else {
user_pill.append_user(item, pills);
}

View File

@ -33,6 +33,7 @@ import * as sub_store from "./sub_store";
import * as subs from "./subs";
import * as ui from "./ui";
import * as ui_report from "./ui_report";
import * as user_group_pill from "./user_group_pill";
import * as user_pill from "./user_pill";
import * as util from "./util";
@ -243,6 +244,8 @@ function submit_add_subscriber_form(e) {
const stream_subscription_info_elem = $(".stream_subscription_info").expectOne();
let user_ids = user_pill.get_user_ids(pill_widget);
user_ids = user_ids.concat(stream_pill.get_user_ids(pill_widget));
user_ids = user_ids.concat(user_group_pill.get_user_ids(pill_widget));
user_ids = new Set(user_ids);
if (user_ids.has(page_params.user_id) && sub.subscribed) {
@ -400,7 +403,7 @@ function show_subscription_settings(sub) {
simplebar_container: $(".subscriber_list_container"),
});
const opts = {source: get_users_for_subscriber_typeahead, stream: true};
const opts = {source: get_users_for_subscriber_typeahead, stream: true, user_group: true};
pill_typeahead.set_up(sub_settings.find(".input"), pill_widget, opts);
}

View File

@ -0,0 +1,49 @@
import * as user_groups from "./user_groups";
function get_user_ids_from_user_groups(items) {
let user_ids = [];
const group_ids = items.map((item) => item.id).filter(Boolean);
for (const group_id of group_ids) {
const user_group = user_groups.get_user_group_from_id(group_id);
user_ids = user_ids.concat(Array.from(user_group.members));
}
return user_ids;
}
export function get_user_ids(pill_widget) {
const items = pill_widget.items();
let user_ids = get_user_ids_from_user_groups(items);
user_ids = Array.from(new Set(user_ids));
user_ids = user_ids.filter(Boolean);
return user_ids;
}
export function append_user_group(group, pill_widget) {
if (group !== undefined && group !== null) {
pill_widget.appendValidatedData({
display_value: group.name + ": " + group.members.size + " users",
id: group.id,
});
pill_widget.clear_text();
}
}
export function get_group_ids(pill_widget) {
const items = pill_widget.items();
let group_ids = items.map((item) => item.id);
group_ids = group_ids.filter(Boolean);
return group_ids;
}
export function filter_taken_groups(items, pill_widget) {
const taken_group_ids = get_group_ids(pill_widget);
items = items.filter((item) => !taken_group_ids.includes(item.id));
return items;
}
export function typeahead_source(pill_widget) {
const groups = user_groups.get_realm_user_groups();
return filter_taken_groups(groups, pill_widget);
}