diff --git a/web/src/add_group_members_pill.ts b/web/src/add_group_members_pill.ts index 78c9cd24ae..b6aa065a2e 100644 --- a/web/src/add_group_members_pill.ts +++ b/web/src/add_group_members_pill.ts @@ -1,6 +1,10 @@ +import * as add_subscribers_pill from "./add_subscribers_pill"; +import * as input_pill from "./input_pill"; import * as keydown_util from "./keydown_util"; +import type {User} from "./people"; import * as stream_pill from "./stream_pill"; -import type {CombinedPillContainer} from "./typeahead_helper"; +import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper"; +import * as user_group_create_members_data from "./user_group_create_members_data"; import * as user_group_pill from "./user_group_pill"; import * as user_pill from "./user_pill"; @@ -15,6 +19,39 @@ function get_pill_group_ids(pill_widget: CombinedPillContainer): number[] { return group_user_ids; } +export function create_without_add_button({ + $pill_container, + onPillCreateAction, + onPillRemoveAction, +}: { + $pill_container: JQuery; + onPillCreateAction: (pill_user_ids: number[], pill_subgroup_ids: number[]) => void; + onPillRemoveAction: (pill_user_ids: number[], pill_subgroup_ids: number[]) => void; +}): CombinedPillContainer { + const pill_widget = input_pill.create({ + $container: $pill_container, + create_item_from_text: add_subscribers_pill.create_item_from_text, + get_text_from_item: add_subscribers_pill.get_text_from_item, + get_display_value_from_item: add_subscribers_pill.get_display_value_from_item, + generate_pill_html: add_subscribers_pill.generate_pill_html, + }); + function get_users(): User[] { + const potential_members = user_group_create_members_data.get_potential_members(); + return user_pill.filter_taken_users(potential_members, pill_widget); + } + + pill_widget.onPillCreate(() => { + onPillCreateAction(get_pill_user_ids(pill_widget), get_pill_group_ids(pill_widget)); + }); + pill_widget.onPillRemove(() => { + onPillRemoveAction(get_pill_user_ids(pill_widget), get_pill_group_ids(pill_widget)); + }); + + add_subscribers_pill.set_up_pill_typeahead({pill_widget, $pill_container, get_users}); + + return pill_widget; +} + export function set_up_handlers({ get_pill_widget, $parent_container, diff --git a/web/src/add_subscribers_pill.ts b/web/src/add_subscribers_pill.ts index cee35cbc95..452085007d 100644 --- a/web/src/add_subscribers_pill.ts +++ b/web/src/add_subscribers_pill.ts @@ -13,7 +13,7 @@ import * as user_group_pill from "./user_group_pill"; import * as user_groups from "./user_groups"; import * as user_pill from "./user_pill"; -function create_item_from_text( +export function create_item_from_text( text: string, current_items: CombinedPill[], ): CombinedPill | undefined { @@ -31,7 +31,7 @@ function create_item_from_text( return undefined; } -function get_text_from_item(item: CombinedPill): string { +export function get_text_from_item(item: CombinedPill): string { let text: string; switch (item.type) { case "stream": @@ -47,7 +47,7 @@ function get_text_from_item(item: CombinedPill): string { return text; } -function set_up_pill_typeahead({ +export function set_up_pill_typeahead({ pill_widget, $pill_container, get_users, @@ -65,7 +65,7 @@ function set_up_pill_typeahead({ pill_typeahead.set_up_combined($pill_container.find(".input"), pill_widget, opts); } -function get_display_value_from_item(item: CombinedPill): string { +export function get_display_value_from_item(item: CombinedPill): string { if (item.type === "user_group") { return user_group_pill.display_pill(user_groups.get_user_group_from_id(item.group_id)); } else if (item.type === "stream") { @@ -75,7 +75,7 @@ function get_display_value_from_item(item: CombinedPill): string { return user_pill.get_display_value_from_item(item); } -function generate_pill_html(item: CombinedPill): string { +export function generate_pill_html(item: CombinedPill): string { if (item.type === "user_group") { return render_input_pill({ display_value: get_display_value_from_item(item), diff --git a/web/src/user_group_create.ts b/web/src/user_group_create.ts index 51b9c2e7e9..5741cc23b9 100644 --- a/web/src/user_group_create.ts +++ b/web/src/user_group_create.ts @@ -36,7 +36,7 @@ let can_mention_group_widget: GroupSettingPillContainer | undefined; class UserGroupMembershipError { report_no_members_to_user_group(): void { $("#user_group_membership_error").text( - $t({defaultMessage: "You cannot create a user group with no members."}), + $t({defaultMessage: "You cannot create a user group with no members or subgroups."}), ); $("#user_group_membership_error").show(); } @@ -151,6 +151,7 @@ function create_user_group(): void { return; } const user_ids = user_group_create_members.get_principals(); + const subgroup_ids = user_group_create_members.get_subgroups(); assert(can_add_members_group_widget !== undefined); const can_add_members_group = settings_components.get_group_setting_widget_value( @@ -177,6 +178,7 @@ function create_user_group(): void { name: group_name, description, members: JSON.stringify(user_ids), + subgroups: JSON.stringify(subgroup_ids), can_add_members_group: JSON.stringify(can_add_members_group), can_join_group: JSON.stringify(can_join_group), can_leave_group: JSON.stringify(can_leave_group), @@ -230,7 +232,8 @@ export function set_up_handlers(): void { } const principals = user_group_create_members_data.get_principals(); - if (principals.length === 0) { + const subgroups = user_group_create_members_data.get_subgroups(); + if (principals.length === 0 && subgroups.length === 0) { user_group_membership_error.report_no_members_to_user_group(); return; } diff --git a/web/src/user_group_create_members.ts b/web/src/user_group_create_members.ts index 54ca7ef436..f946cdfc17 100644 --- a/web/src/user_group_create_members.ts +++ b/web/src/user_group_create_members.ts @@ -1,36 +1,44 @@ import $ from "jquery"; import render_new_user_group_user from "../templates/stream_settings/new_stream_user.hbs"; +import render_new_user_group_subgroup from "../templates/user_group_settings/new_user_group_subgroup.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 add_group_members_pill from "./add_group_members_pill"; import * as ListWidget from "./list_widget"; import type {ListWidget as ListWidgetType} from "./list_widget"; import * as people from "./people"; +import type {User} from "./people"; import {current_user} from "./state_data"; import type {CombinedPillContainer} from "./typeahead_helper"; +import * as user_group_components from "./user_group_components"; import * as user_group_create_members_data from "./user_group_create_members_data"; -import * as user_sort from "./user_sort"; +import type {UserGroup} from "./user_groups"; let pill_widget: CombinedPillContainer; -let all_users_list_widget: ListWidgetType; +let all_users_list_widget: ListWidgetType; export function get_principals(): number[] { return user_group_create_members_data.get_principals(); } -function redraw_member_list(): void { - all_users_list_widget.replace_list_data(user_group_create_members_data.sorted_user_ids()); +export function get_subgroups(): number[] { + return user_group_create_members_data.get_subgroups(); } -function add_user_ids(user_ids: number[]): void { +function redraw_member_list(): void { + all_users_list_widget.replace_list_data(user_group_create_members_data.sorted_members()); +} + +function add_members(user_ids: number[], subgroup_ids: number[]): void { user_group_create_members_data.add_user_ids(user_ids); + user_group_create_members_data.add_subgroup_ids(subgroup_ids); redraw_member_list(); } function add_all_users(): void { const user_ids = user_group_create_members_data.get_all_user_ids(); - add_user_ids(user_ids); + add_members(user_ids, []); } function soft_remove_user_id(user_id: number): void { @@ -43,30 +51,40 @@ function undo_soft_remove_user_id(user_id: number): void { redraw_member_list(); } -export function clear_member_list(): void { - user_group_create_members_data.initialize_with_current_user(); +function soft_remove_subgroup_id(subgroup_id: number): void { + user_group_create_members_data.soft_remove_subgroup_id(subgroup_id); redraw_member_list(); } -function sync_user_ids(user_ids: number[]): void { +function undo_soft_remove_subgroup_id(subgroup_id: number): void { + user_group_create_members_data.undo_soft_remove_subgroup_id(subgroup_id); + redraw_member_list(); +} + +export function clear_member_list(): void { + user_group_create_members_data.initialize_with_current_user(); + user_group_create_members_data.reset_subgroups_data(); + redraw_member_list(); +} + +function sync_members(user_ids: number[], subgroup_ids: number[]): void { user_group_create_members_data.sync_user_ids(user_ids); + user_group_create_members_data.sync_subgroup_ids(subgroup_ids); redraw_member_list(); } function build_pill_widget({$parent_container}: {$parent_container: JQuery}): void { 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_without_add_button({ + pill_widget = add_group_members_pill.create_without_add_button({ $pill_container, - get_potential_subscribers: get_potential_members, - onPillCreateAction: add_user_ids, - // It is better to sync the current set of user ids in the input - // instead of removing user_ids from the user_ids_set, otherwise - // we'll have to have more complex logic of when to remove - // a user and when not to depending upon their group, channel - // and individual pills. - onPillRemoveAction: sync_user_ids, + onPillCreateAction: add_members, + // It is better to sync the current set of user and subgroup ids + // in the input instead of removing them from the user_ids_set + // and subgroup_id_set, otherwise we'll have to have more complex + // logic of when to remove a user and when not to depending upon + // their channel and individual pills. + onPillRemoveAction: sync_members, }); } @@ -90,6 +108,20 @@ export function create_handlers($container: JQuery): void { const user_id = Number.parseInt($elem.attr("data-user-id")!, 10); undo_soft_remove_user_id(user_id); }); + + $container.on("click", ".remove_potential_subgroup", (e) => { + e.preventDefault(); + const $elem = $(e.target); + const subgroup_id = Number.parseInt($elem.attr("data-group-id")!, 10); + soft_remove_subgroup_id(subgroup_id); + }); + + $container.on("click", ".undo_soft_removed_potential_subgroup", (e) => { + e.preventDefault(); + const $elem = $(e.target); + const user_id = Number.parseInt($elem.attr("data-group-id")!, 10); + undo_soft_remove_subgroup_id(user_id); + }); } export function build_widgets(): void { @@ -102,33 +134,45 @@ export function build_widgets(): void { user_group_create_members_data.initialize_with_current_user(); const current_user_id = current_user.user_id; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const initial_members = [people.get_by_user_id(current_user_id)] as (User | UserGroup)[]; - all_users_list_widget = ListWidget.create($("#create_user_group_members"), [current_user_id], { + all_users_list_widget = ListWidget.create($("#create_user_group_members"), initial_members, { name: "new_user_group_add_users", $parent_container: $add_people_container, - get_item: people.get_by_user_id, + get_item: ListWidget.default_get_item, sort_fields: { - email: user_sort.sort_email, - id: user_sort.sort_user_id, - ...ListWidget.generic_sort_functions("alphabetic", ["full_name"]), + email: user_group_components.sort_group_member_email, + name: user_group_components.sort_group_member_name, }, - modifier_html(user) { + modifier_html(member: User | UserGroup) { + if ("user_id" in member) { + const item = { + email: member.delivery_email, + user_id: member.user_id, + full_name: member.full_name, + is_current_user: member.user_id === current_user_id, + img_src: people.small_avatar_url_for_person(member), + soft_removed: user_group_create_members_data.user_id_in_soft_remove_list( + member.user_id, + ), + }; + return render_new_user_group_user(item); + } + const item = { - email: user.delivery_email, - user_id: user.user_id, - full_name: user.full_name, - is_current_user: user.user_id === current_user_id, - img_src: people.small_avatar_url_for_person(user), - soft_removed: user_group_create_members_data.user_id_in_soft_remove_list( - user.user_id, + group_id: member.id, + display_value: member.name, + soft_removed: user_group_create_members_data.subgroup_id_in_soft_remove_list( + member.id, ), }; - return render_new_user_group_user(item); + return render_new_user_group_subgroup(item); }, filter: { $element: $("#people_to_add_in_group .add-user-list-filter"), - predicate(user, search_term) { - return people.build_person_matcher(search_term)(user); + predicate(member, search_term) { + return user_group_components.build_group_member_matcher(search_term)(member); }, }, $simplebar_container, diff --git a/web/src/user_group_create_members_data.ts b/web/src/user_group_create_members_data.ts index a546f03ae7..dede574c0a 100644 --- a/web/src/user_group_create_members_data.ts +++ b/web/src/user_group_create_members_data.ts @@ -3,19 +3,34 @@ import _ from "lodash"; import * as people from "./people"; import type {User} from "./people"; import {current_user} from "./state_data"; +import * as user_group_components from "./user_group_components"; +import * as user_groups from "./user_groups"; +import type {UserGroup} from "./user_groups"; let user_id_set: Set; let soft_remove_user_id_set: Set; +let subgroup_id_set = new Set([]); +let soft_remove_subgroup_id_set = new Set([]); export function initialize_with_current_user(): void { user_id_set = new Set([current_user.user_id]); soft_remove_user_id_set = new Set(); } -export function sorted_user_ids(): number[] { +export function reset_subgroups_data(): void { + subgroup_id_set = new Set([]); + soft_remove_subgroup_id_set = new Set([]); +} + +export function sorted_members(): (User | UserGroup)[] { const users = people.get_users_from_ids([...user_id_set]); people.sort_but_pin_current_user_on_top(users); - return users.map((user) => user.user_id); + + const subgroups = [...subgroup_id_set] + .map((group_id) => user_groups.get_user_group_from_id(group_id)) + .sort(user_group_components.sort_group_member_name); + + return [...subgroups, ...users]; } export function get_all_user_ids(): number[] { @@ -31,6 +46,10 @@ export function get_principals(): number[] { return _.difference([...user_id_set], [...soft_remove_user_id_set]); } +export function get_subgroups(): number[] { + return _.difference([...subgroup_id_set], [...soft_remove_subgroup_id_set]); +} + export function get_potential_members(): User[] { const potential_members = people.get_realm_users(); return potential_members.filter((user) => !user_id_set.has(user.user_id)); @@ -72,3 +91,30 @@ export function undo_soft_remove_user_id(user_id: number): void { export function user_id_in_soft_remove_list(user_id: number): boolean { return soft_remove_user_id_set.has(user_id); } + +export function add_subgroup_ids(subgroup_ids: number[]): void { + for (const subgroup_id of subgroup_ids) { + if (!subgroup_id_set.has(subgroup_id)) { + const group = user_groups.get_user_group_from_id(subgroup_id); + if (group) { + subgroup_id_set.add(subgroup_id); + } + } + } +} + +export function sync_subgroup_ids(subgroup_ids: number[]): void { + subgroup_id_set = new Set(subgroup_ids); +} + +export function soft_remove_subgroup_id(subgroup_id: number): void { + soft_remove_subgroup_id_set.add(subgroup_id); +} + +export function undo_soft_remove_subgroup_id(subgroup_id: number): void { + soft_remove_subgroup_id_set.delete(subgroup_id); +} + +export function subgroup_id_in_soft_remove_list(subgroup_id: number): boolean { + return soft_remove_subgroup_id_set.has(subgroup_id); +} diff --git a/web/templates/user_group_settings/new_user_group_subgroup.hbs b/web/templates/user_group_settings/new_user_group_subgroup.hbs new file mode 100644 index 0000000000..28a79d4a3c --- /dev/null +++ b/web/templates/user_group_settings/new_user_group_subgroup.hbs @@ -0,0 +1,12 @@ + + + {{> ../user_group_display_only_pill strikethrough=soft_removed}} + + + {{#if soft_removed}} + + {{else}} + + {{/if}} + +