groups-ui: Add option to copy membership from group.

Instead of adding group as a subgroup, we now provide option
to add direct members and direct subgroups of a group to a
user group by providing an expand button in the group pill.

Fixes #32335.
This commit is contained in:
Sahil Batra
2025-04-17 12:13:01 +05:30
committed by Tim Abbott
parent 592ee2de1c
commit 53cdfddf5b
10 changed files with 91 additions and 10 deletions

View File

@@ -283,6 +283,11 @@ Source: https://lucide.dev/icons/plus
Copyright: 2013-2022 Cole Bemis Copyright: 2013-2022 Cole Bemis
License: ISC License License: ISC License
Files: web/shared/icons/expand-both-diagonals.svg
Source: https://lucide.dev/icons/expand
Copyright: 2013-2022 Cole Bemis
License: ISC License
Files: web/third/bootstrap/css/bootstrap.app.css Files: web/third/bootstrap/css/bootstrap.app.css
Copyright: 2012 Twitter, Inc. Copyright: 2012 Twitter, Inc.
License: Apache-2.0 License: Apache-2.0

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m 15,14 a 1,1 0 0 0 -0.707031,0.292969 1,1 0 0 0 0,1.414062 l 6,6 a 1,1 0 0 0 1.414062,0 1,1 0 0 0 0,-1.414062 l -6,-6 A 1,1 0 0 0 15,14 Z" />
<path d="m 20.292969,2.2929688 -6,6 a 1,1 0 0 0 0,1.4140625 1,1 0 0 0 1.414062,0 l 6,-6 a 1,1 0 0 0 0,-1.4140625 1,1 0 0 0 -1.414062,0 z" />
<path d="m 21,15.199219 a 1,1 0 0 0 -1,1 V 20 h -3.800781 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 H 21 a 1.0001,1.0001 0 0 0 1,-1 v -4.800781 a 1,1 0 0 0 -1,-1 z" />
<path d="m 16.199219,2 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 H 20 v 3.8007812 a 1,1 0 0 0 1,1.0000001 1,1 0 0 0 1,-1.0000001 V 3 A 1.0001,1.0001 0 0 0 21,2 Z" />
<path d="m 3,15.199219 a 1,1 0 0 0 -1,1 V 21 a 1.0001,1.0001 0 0 0 1,1 H 7.8007812 A 1,1 0 0 0 8.8007813,21 1,1 0 0 0 7.8007812,20 H 4 v -3.800781 a 1,1 0 0 0 -1,-1 z" />
<path d="m 9,14 a 1,1 0 0 0 -0.7070312,0.292969 l -6,6 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140625,0 l 6,-6 a 1,1 0 0 0 0,-1.414062 A 1,1 0 0 0 9,14 Z" />
<path d="M 3,2 A 1.0001,1.0001 0 0 0 2,3 V 7.8007812 A 1,1 0 0 0 3,8.8007813 1,1 0 0 0 4,7.8007812 V 4 H 7.8007812 A 1,1 0 0 0 8.8007813,3 1,1 0 0 0 7.8007812,2 Z" />
<path d="m 3,2 a 1,1 0 0 0 -0.7070312,0.2929688 1,1 0 0 0 0,1.4140625 l 6,6 a 1,1 0 0 0 1.4140625,0 1,1 0 0 0 0,-1.4140625 l -6,-6 A 1,1 0 0 0 3,2 Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -3,6 +3,7 @@ import assert from "minimalistic-assert";
import * as add_subscribers_pill from "./add_subscribers_pill.ts"; import * as add_subscribers_pill from "./add_subscribers_pill.ts";
import * as input_pill from "./input_pill.ts"; import * as input_pill from "./input_pill.ts";
import * as keydown_util from "./keydown_util.ts"; import * as keydown_util from "./keydown_util.ts";
import * as people from "./people.ts";
import type {User} from "./people.ts"; import type {User} from "./people.ts";
import * as stream_pill from "./stream_pill.ts"; import * as stream_pill from "./stream_pill.ts";
import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper.ts"; import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper.ts";
@@ -24,6 +25,30 @@ function get_pill_group_ids(pill_widget: CombinedPillContainer): number[] {
return group_user_ids; return group_user_ids;
} }
function expand_group_pill($pill_elem: JQuery, pill_widget: CombinedPillContainer): void {
const group_id = Number.parseInt($pill_elem.attr("data-user-group-id")!, 10);
const group = user_groups.get_user_group_from_id(group_id);
const direct_subgroup_ids = group.direct_subgroup_ids;
const direct_member_ids = group.members;
const taken_group_ids = user_group_pill.get_group_ids(pill_widget);
const taken_user_ids = user_pill.get_user_ids(pill_widget);
for (const member_id of direct_member_ids) {
if (!taken_user_ids.includes(member_id)) {
const user = people.get_by_user_id(member_id);
user_pill.append_user(user, pill_widget, false);
}
}
for (const group_id of direct_subgroup_ids) {
if (!taken_group_ids.includes(group_id)) {
const subgroup = user_groups.get_user_group_from_id(group_id);
user_group_pill.append_user_group(subgroup, pill_widget, false, true);
}
}
}
export function create_item_from_text( export function create_item_from_text(
text: string, text: string,
current_items: CombinedPill[], current_items: CombinedPill[],
@@ -42,6 +67,8 @@ export function create_item_from_text(
const group_item = user_group_pill.create_item_from_group_name(text, current_items); const group_item = user_group_pill.create_item_from_group_name(text, current_items);
if (group_item) { if (group_item) {
const subgroup = user_groups.get_user_group_from_id(group_item.group_id); const subgroup = user_groups.get_user_group_from_id(group_item.group_id);
group_item.show_expand_button =
subgroup.members.size > 0 || subgroup.direct_subgroup_ids.size > 0;
const current_group_id = user_group_components.active_group_id; const current_group_id = user_group_components.active_group_id;
assert(current_group_id !== undefined); assert(current_group_id !== undefined);
const current_group = user_groups.get_user_group_from_id(current_group_id); const current_group = user_groups.get_user_group_from_id(current_group_id);
@@ -88,6 +115,10 @@ export function create({
return user_group_pill.filter_taken_groups(potential_groups, pill_widget); return user_group_pill.filter_taken_groups(potential_groups, pill_widget);
} }
pill_widget.onPillExpand((pill) => {
expand_group_pill(pill, pill_widget);
});
add_subscribers_pill.set_up_pill_typeahead({ add_subscribers_pill.set_up_pill_typeahead({
pill_widget, pill_widget,
$pill_container, $pill_container,
@@ -136,6 +167,10 @@ export function create_without_add_button({
onPillRemoveAction(get_pill_user_ids(pill_widget), get_pill_group_ids(pill_widget)); onPillRemoveAction(get_pill_user_ids(pill_widget), get_pill_group_ids(pill_widget));
}); });
pill_widget.onPillExpand((pill) => {
expand_group_pill(pill, pill_widget);
});
add_subscribers_pill.set_up_pill_typeahead({ add_subscribers_pill.set_up_pill_typeahead({
pill_widget, pill_widget,
$pill_container, $pill_container,

View File

@@ -58,6 +58,7 @@ type InputPillStore<ItemType> = {
on_pill_exit: InputPillCreateOptions<ItemType>["on_pill_exit"]; on_pill_exit: InputPillCreateOptions<ItemType>["on_pill_exit"];
onPillCreate?: () => void; onPillCreate?: () => void;
onPillRemove?: (pill: InputPill<ItemType>, trigger: RemovePillTrigger) => void; onPillRemove?: (pill: InputPill<ItemType>, trigger: RemovePillTrigger) => void;
onPillExpand?: (pill: JQuery) => void;
createPillonPaste?: () => void; createPillonPaste?: () => void;
split_text_on_comma: boolean; split_text_on_comma: boolean;
convert_to_pill_on_enter: boolean; convert_to_pill_on_enter: boolean;
@@ -78,6 +79,7 @@ export type InputPillContainer<ItemType> = {
onPillRemove: ( onPillRemove: (
callback: (pill: InputPill<ItemType>, trigger: RemovePillTrigger) => void, callback: (pill: InputPill<ItemType>, trigger: RemovePillTrigger) => void,
) => void; ) => void;
onPillExpand: (callback: (pill: JQuery) => void) => void;
onTextInputHook: (callback: () => void) => void; onTextInputHook: (callback: () => void) => void;
createPillonPaste: (callback: () => void) => void; createPillonPaste: (callback: () => void) => void;
clear: (quiet?: boolean) => void; clear: (quiet?: boolean) => void;
@@ -469,6 +471,15 @@ export function create<ItemType extends {type: string}>(
store.$input.trigger("focus"); store.$input.trigger("focus");
}); });
store.$parent.on("click", ".expand", function (this: HTMLElement, e) {
assert(store.onPillExpand !== undefined);
e.stopPropagation();
store.onPillExpand($(this).closest(".pill"));
const pill = util.the($(this).closest(".pill"));
funcs.removePill(pill, "close");
store.$input.trigger("focus");
});
store.$parent.on("click", function (e) { store.$parent.on("click", function (e) {
if ($(e.target).is(".pill-container")) { if ($(e.target).is(".pill-container")) {
$(this).find(".input").trigger("focus"); $(this).find(".input").trigger("focus");
@@ -501,6 +512,10 @@ export function create<ItemType extends {type: string}>(
store.onPillRemove = callback; store.onPillRemove = callback;
}, },
onPillExpand(callback) {
store.onPillExpand = callback;
},
onTextInputHook(callback) { onTextInputHook(callback) {
store.onTextInputHook = callback; store.onTextInputHook = callback;
}, },

View File

@@ -394,7 +394,10 @@ export function set_up_combined(
if (include_streams(query) && item.type === "stream") { if (include_streams(query) && item.type === "stream") {
stream_pill.append_stream(item, pills); stream_pill.append_stream(item, pills);
} else if (include_user_groups && item.type === "user_group") { } else if (include_user_groups && item.type === "user_group") {
user_group_pill.append_user_group(item, pills); const show_expand_button =
!opts.for_stream_subscribers &&
(item.members.size > 0 || item.direct_subgroup_ids.size > 0);
user_group_pill.append_user_group(item, pills, true, show_expand_button);
} else if ( } else if (
include_users && include_users &&
item.type === "user" && item.type === "user" &&

View File

@@ -17,6 +17,7 @@ export type UserGroupPill = {
type: "user_group"; type: "user_group";
group_id: number; group_id: number;
group_name: string; group_name: string;
show_expand_button?: boolean;
}; };
export type UserGroupPillWidget = InputPillContainer<UserGroupPill>; export type UserGroupPillWidget = InputPillContainer<UserGroupPill>;
@@ -34,6 +35,7 @@ export function generate_pill_html(item: UserGroupPill): string {
group_id: item.group_id, group_id: item.group_id,
show_group_members_count: true, show_group_members_count: true,
group_members_count: group_members.length, group_members_count: group_members.length,
show_expand_button: item.show_expand_button ?? false,
}); });
} }
@@ -87,12 +89,14 @@ export function append_user_group(
group: UserGroup, group: UserGroup,
pill_widget: CombinedPillContainer | GroupSettingPillContainer | UserGroupPillWidget, pill_widget: CombinedPillContainer | GroupSettingPillContainer | UserGroupPillWidget,
execute_oncreate_callback = true, execute_oncreate_callback = true,
show_expand_button = false,
): void { ): void {
pill_widget.appendValidatedData( pill_widget.appendValidatedData(
{ {
type: "user_group", type: "user_group",
group_id: group.id, group_id: group.id,
group_name: group.name, group_name: group.name,
show_expand_button,
}, },
false, false,
!execute_oncreate_callback, !execute_oncreate_callback,

View File

@@ -70,7 +70,8 @@
line-height: 1.5; line-height: 1.5;
} }
.pill-close-button { .pill-close-button,
.pill-expand-button {
font-size: 0.7142em; /* 10px at 14px em */ font-size: 0.7142em; /* 10px at 14px em */
text-decoration: none; text-decoration: none;
/* Let the close button's box stretch, /* Let the close button's box stretch,
@@ -92,20 +93,22 @@
opacity: 0.7; opacity: 0.7;
} }
.exit { .exit,
.expand {
width: var(--length-input-pill-exit); width: var(--length-input-pill-exit);
height: var(--length-input-pill-exit); height: var(--length-input-pill-exit);
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-right: 2px; margin-right: 2px;
border-radius: 2px; border-radius: 2px;
}
.exit:hover { &:hover {
background: var(--color-background-input-pill-exit-hover); background: var(--color-background-input-pill-exit-hover);
.pill-close-button { .pill-close-button,
opacity: 1; .pill-expand-button {
opacity: 1;
}
} }
} }

View File

@@ -33,6 +33,11 @@
&nbsp;<span class="group-members-count">({{group_members_count}})</span> &nbsp;<span class="group-members-count">({{group_members_count}})</span>
{{~/if~}} {{~/if~}}
</span> </span>
{{#if show_expand_button}}
<div class="expand">
<a role="button" class="zulip-icon zulip-icon-expand-both-diagonals pill-expand-button"></a>
</div>
{{/if}}
{{#unless disabled}} {{#unless disabled}}
<div class="exit"> <div class="exit">
<a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a> <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>

View File

@@ -94,14 +94,14 @@ const admins = {
name: "Admins", name: "Admins",
description: "foo", description: "foo",
id: 1, id: 1,
members: [jill.user_id, mark.user_id, me.user_id], members: new Set([jill.user_id, mark.user_id, me.user_id]),
}; };
const admins_item = user_group_item(admins); const admins_item = user_group_item(admins);
const testers = { const testers = {
name: "Testers", name: "Testers",
description: "bar", description: "bar",
id: 2, id: 2,
members: [mark.user_id, fred.user_id, me.user_id], members: new Set([mark.user_id, fred.user_id, me.user_id]),
}; };
const testers_item = user_group_item(testers); const testers_item = user_group_item(testers);

View File

@@ -77,6 +77,7 @@ const testers_pill = {
group_id: testers.id, group_id: testers.id,
group_name: testers.name, group_name: testers.name,
type: "user_group", type: "user_group",
show_expand_button: false,
}; };
const everyone_pill = { const everyone_pill = {
group_id: everyone.id, group_id: everyone.id,