diff --git a/frontend_tests/node_tests/people.js b/frontend_tests/node_tests/people.js index 4c0ba94520..e94e878693 100644 --- a/frontend_tests/node_tests/people.js +++ b/frontend_tests/node_tests/people.js @@ -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); diff --git a/frontend_tests/node_tests/settings_user_groups.js b/frontend_tests/node_tests/settings_user_groups.js index 4f2ca89c81..242c1f1c7f 100644 --- a/frontend_tests/node_tests/settings_user_groups.js +++ b/frontend_tests/node_tests/settings_user_groups.js @@ -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); })(); diff --git a/frontend_tests/node_tests/stream_edit.js b/frontend_tests/node_tests/stream_edit.js index 9d7c6ea48f..46e6097147 100644 --- a/frontend_tests/node_tests/stream_edit.js +++ b/frontend_tests/node_tests/stream_edit.js @@ -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); diff --git a/static/js/composebox_typeahead.js b/static/js/composebox_typeahead.js index eff57300f5..6343a378fa 100644 --- a/static/js/composebox_typeahead.js +++ b/static/js/composebox_typeahead.js @@ -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, diff --git a/static/js/people.js b/static/js/people.js index bdc031368a..86dc5754e7 100644 --- a/static/js/people.js +++ b/static/js/people.js @@ -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); diff --git a/static/js/pill_typeahead.js b/static/js/pill_typeahead.js index ac2c2a9f7b..f2143412a2 100644 --- a/static/js/pill_typeahead.js +++ b/static/js/pill_typeahead.js @@ -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); } diff --git a/static/js/stream_edit.js b/static/js/stream_edit.js index 8844a3899a..d6baf8c680 100644 --- a/static/js/stream_edit.js +++ b/static/js/stream_edit.js @@ -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); } diff --git a/static/js/user_group_pill.js b/static/js/user_group_pill.js new file mode 100644 index 0000000000..972bf5b06a --- /dev/null +++ b/static/js/user_group_pill.js @@ -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); +}