mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
user_groups: Allow adding a user to groups via user profile.
Fixes: #32488.
This commit is contained in:
committed by
Tim Abbott
parent
e33fe6779b
commit
faa8b0d4a5
@@ -101,12 +101,11 @@ export function generate_pill_html(item: CombinedPill): string {
|
||||
}
|
||||
|
||||
export function set_up_handlers_for_add_button_state(
|
||||
pill_widget: CombinedPillContainer,
|
||||
pill_widget: CombinedPillContainer | user_group_pill.UserGroupPillWidget,
|
||||
$pill_container: JQuery,
|
||||
): void {
|
||||
const $pill_widget_input = $pill_container.find(".input");
|
||||
const $pill_widget_button = $pill_container.parent().find(".add-users-button");
|
||||
|
||||
const $pill_widget_button = $pill_container.closest(".add-button-container").find(".button");
|
||||
// Disable the add button first time the pill container is created.
|
||||
$pill_widget_button.prop("disabled", true);
|
||||
|
||||
@@ -114,6 +113,8 @@ export function set_up_handlers_for_add_button_state(
|
||||
pill_widget.onPillRemove(() =>
|
||||
$pill_widget_button.prop("disabled", pill_widget.items().length === 0),
|
||||
);
|
||||
// If a pill is added, enable the add button.
|
||||
pill_widget.onPillCreate(() => $pill_widget_button.prop("disabled", false));
|
||||
// Disable the add button when there is no pending text that can be converted
|
||||
// into a pill and the number of existing pills is zero.
|
||||
$pill_widget_input.on("input", () =>
|
||||
|
@@ -15,6 +15,7 @@ export type InputPillConfig = {
|
||||
exclude_inaccessible_users?: boolean;
|
||||
setting_name?: string;
|
||||
setting_type?: "realm" | "stream" | "group";
|
||||
user_id?: number;
|
||||
};
|
||||
|
||||
type InputPillCreateOptions<ItemType> = {
|
||||
@@ -68,6 +69,10 @@ export type InputPillContainer<ItemType> = {
|
||||
appendValidatedData: (item: ItemType) => void;
|
||||
getByElement: (element: HTMLElement) => InputPill<ItemType> | undefined;
|
||||
items: () => ItemType[];
|
||||
removePill: (
|
||||
element: HTMLElement,
|
||||
trigger: RemovePillTrigger,
|
||||
) => InputPill<ItemType> | undefined;
|
||||
onPillCreate: (callback: () => void) => void;
|
||||
onPillRemove: (
|
||||
callback: (pill: InputPill<ItemType>, trigger: RemovePillTrigger) => void,
|
||||
@@ -480,6 +485,7 @@ export function create<ItemType extends {type: string}>(
|
||||
getByElement: funcs.getByElement.bind(funcs),
|
||||
getCurrentText: funcs.getCurrentText.bind(funcs),
|
||||
items: funcs.items.bind(funcs),
|
||||
removePill: funcs.removePill.bind(funcs),
|
||||
|
||||
onPillCreate(callback) {
|
||||
store.onPillCreate = callback;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import render_input_pill from "../templates/input_pill.hbs";
|
||||
|
||||
import {set_up_handlers_for_add_button_state} from "./add_subscribers_pill.ts";
|
||||
import * as input_pill from "./input_pill.ts";
|
||||
import {set_up_user_group} from "./pill_typeahead.ts";
|
||||
import * as settings_data from "./settings_data.ts";
|
||||
@@ -16,6 +17,7 @@ type SetUpPillTypeaheadConfig = {
|
||||
function create_item_from_group_name(
|
||||
group_name: string,
|
||||
current_items: UserGroupPill[],
|
||||
pill_config?: input_pill.InputPillConfig,
|
||||
): UserGroupPill | undefined {
|
||||
group_name = group_name.trim();
|
||||
const group = user_groups.get_user_group_from_name(group_name);
|
||||
@@ -31,6 +33,12 @@ function create_item_from_group_name(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if the user is already a direct member of the user group.
|
||||
const user_id = pill_config?.user_id;
|
||||
if (user_id !== undefined && user_groups.is_direct_member_of(user_id, group.id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "user_group",
|
||||
group_id: group.id,
|
||||
@@ -43,9 +51,17 @@ export function get_user_groups_allowed_to_add_members(): UserGroup[] {
|
||||
return all_user_groups.filter((group) => settings_data.can_add_members_to_user_group(group.id));
|
||||
}
|
||||
|
||||
function set_up_pill_typeahead({pill_widget, $pill_container}: SetUpPillTypeaheadConfig): void {
|
||||
function set_up_pill_typeahead(
|
||||
{pill_widget, $pill_container}: SetUpPillTypeaheadConfig,
|
||||
user_id?: number,
|
||||
): void {
|
||||
const user_group_source: () => UserGroup[] = () => {
|
||||
const groups_with_permission = get_user_groups_allowed_to_add_members();
|
||||
let groups_with_permission = get_user_groups_allowed_to_add_members();
|
||||
if (user_id !== undefined) {
|
||||
groups_with_permission = groups_with_permission.filter(
|
||||
(group) => !user_groups.is_direct_member_of(user_id, group.id),
|
||||
);
|
||||
}
|
||||
return user_group_pill.filter_taken_groups(groups_with_permission, pill_widget);
|
||||
};
|
||||
set_up_user_group($pill_container.find(".input"), pill_widget, {user_group_source});
|
||||
@@ -62,15 +78,24 @@ function generate_pill_html(item: UserGroupPill): string {
|
||||
});
|
||||
}
|
||||
|
||||
export function create($user_group_pill_container: JQuery): user_group_pill.UserGroupPillWidget {
|
||||
export function create(
|
||||
$user_group_pill_container: JQuery,
|
||||
user_id?: number,
|
||||
): user_group_pill.UserGroupPillWidget {
|
||||
const pill_config = user_id ? {user_id} : undefined;
|
||||
|
||||
const pill_widget = input_pill.create({
|
||||
$container: $user_group_pill_container,
|
||||
pill_config,
|
||||
create_item_from_text: create_item_from_group_name,
|
||||
get_text_from_item: user_group_pill.get_group_name_from_item,
|
||||
generate_pill_html,
|
||||
get_display_value_from_item,
|
||||
});
|
||||
|
||||
set_up_pill_typeahead({pill_widget, $pill_container: $user_group_pill_container});
|
||||
set_up_pill_typeahead({pill_widget, $pill_container: $user_group_pill_container}, user_id);
|
||||
if (user_id) {
|
||||
set_up_handlers_for_add_button_state(pill_widget, $user_group_pill_container);
|
||||
}
|
||||
return pill_widget;
|
||||
}
|
||||
|
@@ -57,6 +57,8 @@ import * as ui_report from "./ui_report.ts";
|
||||
import type {UploadWidget} from "./upload_widget.ts";
|
||||
import * as user_deactivation_ui from "./user_deactivation_ui.ts";
|
||||
import * as user_group_edit_members from "./user_group_edit_members.ts";
|
||||
import * as user_group_picker_pill from "./user_group_picker_pill.ts";
|
||||
import * as user_group_pill from "./user_group_pill.ts";
|
||||
import * as user_groups from "./user_groups.ts";
|
||||
import type {UserGroup} from "./user_groups.ts";
|
||||
import * as user_pill from "./user_pill.ts";
|
||||
@@ -80,6 +82,7 @@ export type CustomProfileFieldData = {
|
||||
let user_streams_list_widget: ListWidgetType<StreamSubscription> | undefined;
|
||||
let user_groups_list_widget: ListWidgetType<UserGroup> | undefined;
|
||||
let user_profile_subscribe_widget: DropdownWidget | undefined;
|
||||
let user_group_pill_widget: user_group_pill.UserGroupPillWidget;
|
||||
let toggler: components.Toggle;
|
||||
let bot_owner_dropdown_widget: DropdownWidget | undefined;
|
||||
let original_values: (Record<string, unknown> & {user_id?: string | undefined}) | undefined;
|
||||
@@ -548,6 +551,82 @@ export function show_user_profile_access_error_modal(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function add_user_to_groups(group_ids: number[], user_id: number, $alert_box: JQuery): void {
|
||||
const group_ids_successfully_added: number[] = [];
|
||||
|
||||
function add_user_to_next_group(): void {
|
||||
if (group_ids_successfully_added.length >= group_ids.length) {
|
||||
if (group_ids_successfully_added.length > 0) {
|
||||
ui_report.success(
|
||||
$t_html({
|
||||
defaultMessage: "Added successfully!",
|
||||
}),
|
||||
$alert_box,
|
||||
1200,
|
||||
);
|
||||
clear_successful_pills();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const group_id = group_ids[group_ids_successfully_added.length]!;
|
||||
const target_user_group = user_groups.get_user_group_from_id(group_id);
|
||||
|
||||
if (!target_user_group) {
|
||||
return;
|
||||
}
|
||||
|
||||
user_group_edit_members.edit_user_group_membership({
|
||||
group: target_user_group,
|
||||
added: [user_id],
|
||||
success(): void {
|
||||
group_ids_successfully_added.push(group_id);
|
||||
add_user_to_next_group();
|
||||
},
|
||||
error(xhr): void {
|
||||
const parsed = z
|
||||
.object({
|
||||
result: z.literal("error"),
|
||||
msg: z.string(),
|
||||
code: z.string(),
|
||||
})
|
||||
.safeParse(xhr?.responseJSON);
|
||||
|
||||
const error_message = people.is_my_user_id(user_id)
|
||||
? $t(
|
||||
{defaultMessage: "Error joining {group_name}: {error}"},
|
||||
{
|
||||
group_name: target_user_group.name,
|
||||
error: parsed.success ? parsed.data.msg : "Unknown error",
|
||||
},
|
||||
)
|
||||
: $t(
|
||||
{defaultMessage: "Error adding user to {group_name}: {error}"},
|
||||
{
|
||||
group_name: target_user_group.name,
|
||||
error: parsed.success ? parsed.data.msg : "Unknown error",
|
||||
},
|
||||
);
|
||||
|
||||
ui_report.client_error(error_message, $alert_box);
|
||||
clear_successful_pills();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function clear_successful_pills(): void {
|
||||
for (const id of group_ids_successfully_added) {
|
||||
const $pill = $(`#user-group-to-add .pill-container .pill[data-user-group-id="${id}"]`);
|
||||
if ($pill.length > 0) {
|
||||
user_group_pill_widget.removePill($pill[0]!, "close");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the process
|
||||
add_user_to_next_group();
|
||||
}
|
||||
|
||||
export function show_user_profile(user: User, default_tab_key = "profile-tab"): void {
|
||||
const field_types = realm.custom_profile_field_types;
|
||||
const profile_data = realm.custom_profile_fields
|
||||
@@ -565,6 +644,9 @@ export function show_user_profile(user: User, default_tab_key = "profile-tab"):
|
||||
(people.can_admin_user(user) || user_unsub_streams.length > 0) &&
|
||||
!user.is_system_bot &&
|
||||
people.is_person_active(user.user_id);
|
||||
const show_user_group_container =
|
||||
user_group_picker_pill.get_user_groups_allowed_to_add_members().length > 0 &&
|
||||
people.is_person_active(user.user_id);
|
||||
// We currently have the main UI for editing your own profile in
|
||||
// settings, so can_manage_profile is artificially false for those.
|
||||
const can_manage_profile =
|
||||
@@ -586,6 +668,7 @@ export function show_user_profile(user: User, default_tab_key = "profile-tab"):
|
||||
profile_data,
|
||||
should_add_guest_user_indicator: people.should_add_guest_user_indicator(user.user_id),
|
||||
show_user_subscribe_widget,
|
||||
show_user_group_container,
|
||||
user_avatar: people.medium_avatar_url_for_person(user),
|
||||
user_circle_class: buddy_data.get_user_circle_class(user.user_id),
|
||||
user_id: user.user_id,
|
||||
@@ -695,6 +778,13 @@ export function show_user_profile(user: User, default_tab_key = "profile-tab"):
|
||||
if (show_user_subscribe_widget) {
|
||||
reset_subscribe_widget();
|
||||
}
|
||||
if (show_user_group_container) {
|
||||
const $user_group_pill_container = $("#user-group-to-add .pill-container");
|
||||
user_group_pill_widget = user_group_picker_pill.create(
|
||||
$user_group_pill_container,
|
||||
user.user_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_remove_stream_subscription(
|
||||
@@ -1314,6 +1404,27 @@ export function initialize(): void {
|
||||
});
|
||||
});
|
||||
|
||||
$("body").on("click", "#user-profile-modal .add-groups-button", (e) => {
|
||||
e.preventDefault();
|
||||
const user_id = Number.parseInt($("#user-profile-modal").attr("data-user-id")!, 10);
|
||||
const $alert_box = $("#user-profile-groups-tab .user-profile-group-list-alert");
|
||||
const item = $("#user-group-to-add .pill-container .input").text().trim();
|
||||
if (item) {
|
||||
$("#user-group-to-add .pill-container .input").addClass("shake");
|
||||
if (
|
||||
$("#user-group-to-add .pill-container .input").hasClass(
|
||||
"show-outline-on-invalid-input",
|
||||
)
|
||||
) {
|
||||
$("#user-group-to-add .pill-container").addClass("invalid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const group_ids = user_group_pill.get_group_ids(user_group_pill_widget);
|
||||
add_user_to_groups(group_ids, user_id, $alert_box);
|
||||
});
|
||||
|
||||
$("body").on("click", "#user-profile-modal #clear_stream_search", (e) => {
|
||||
const $input = $("#user-profile-streams-tab .stream-search");
|
||||
$input.val("");
|
||||
|
@@ -1506,3 +1506,19 @@ ul.popover-menu-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#groups-to-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#user-group-to-add {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-groups-button-wrapper {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="add_subscribers_container">
|
||||
<div class="add_subscribers_container add-button-container">
|
||||
<div class="pill-container person_picker">
|
||||
<div class="input" contenteditable="true"
|
||||
data-placeholder="{{t 'Add subscribers. Use usergroup or #channelname to bulk add subscribers.' }}">
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="add_members_container">
|
||||
<div class="add_members_container add-button-container">
|
||||
<div class="pill-container person_picker">
|
||||
<div class="input" contenteditable="true"
|
||||
data-placeholder="{{t 'Add users or groups. Use #channelname to add all subscribers.' }}">
|
||||
|
@@ -131,6 +131,28 @@
|
||||
|
||||
<div class="tabcontent" id="user-profile-groups-tab">
|
||||
<div class="alert user-profile-group-list-alert"></div>
|
||||
{{#if show_user_group_container}}
|
||||
<div class="header-section">
|
||||
<h3 class="group-tab-element-header">{{t 'Add {full_name} to groups'}}</h3>
|
||||
</div>
|
||||
<div id="groups-to-add" class="add-button-container">
|
||||
<div id="user-group-to-add">
|
||||
<div class="add-user-group-container">
|
||||
<div class="pill-container">
|
||||
<div class="input" contenteditable="true"
|
||||
data-placeholder="{{t 'Add user groups' }}">
|
||||
{{~! Squash whitespace so that placeholder is displayed when empty. ~}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-groups-button-wrapper">
|
||||
<button type="button" name="subscribe" class="add-groups-button button small rounded">
|
||||
{{t 'Add' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="group-list-top-section">
|
||||
<div class="header-section">
|
||||
<h3 class="group-tab-element-header">{{t 'Group membership' }}</h3>
|
||||
|
Reference in New Issue
Block a user