From 63a7c9061baef32fbd7326a807a277947fd80233 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Mon, 26 Aug 2024 09:24:48 +0530 Subject: [PATCH] settings: Use new pills UI for can_manage_group setting. This UI enables the user to set can_manage_group setting to a combination of users and groups, replacing the old dropdown UI which just allowed setting user to a single system group. Fixes part of #28808. --- web/src/settings_components.ts | 149 +++++++++++++++++- web/src/settings_data.ts | 5 +- web/src/settings_org.js | 4 +- web/src/settings_preferences.ts | 1 + web/src/state_data.ts | 2 +- web/src/user_group_components.ts | 8 +- web/src/user_group_create.ts | 19 +-- web/src/user_group_edit.js | 27 +++- web/styles/subscriptions.css | 26 +++ .../user_group_settings/group_permissions.hbs | 13 +- 10 files changed, 221 insertions(+), 33 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index e5156272e6..752d54c67f 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -8,6 +8,8 @@ import render_compose_banner from "../templates/compose_banner/compose_banner.hb import * as blueslip from "./blueslip"; import * as compose_banner from "./compose_banner"; import type {DropdownWidget} from "./dropdown_widget"; +import * as group_permission_settings from "./group_permission_settings"; +import * as group_setting_pill from "./group_setting_pill"; import {$t} from "./i18n"; import { LEGACY_FONT_SIZE_PX, @@ -15,16 +17,21 @@ import { NON_COMPACT_MODE_FONT_SIZE_PX, NON_COMPACT_MODE_LINE_HEIGHT_PERCENT, } from "./information_density"; +import * as people from "./people"; import {realm_user_settings_defaults} from "./realm_user_settings_defaults"; import * as scroll_util from "./scroll_util"; import * as settings_config from "./settings_config"; import * as settings_data from "./settings_data"; -import type {CustomProfileField} from "./state_data"; +import type {CustomProfileField, group_setting_type_schema} from "./state_data"; import {realm} from "./state_data"; import * as stream_data from "./stream_data"; import type {StreamSubscription} from "./sub_store"; +import type {GroupSettingPillContainer} from "./typeahead_helper"; import type {HTMLSelectOneElement} from "./types"; +import * as user_group_pill from "./user_group_pill"; +import * as user_groups from "./user_groups"; import type {UserGroup} from "./user_groups"; +import * as user_pill from "./user_pill"; import * as util from "./util"; const MAX_CUSTOM_TIME_LIMIT_SETTING_VALUE = 2147483647; @@ -465,7 +472,6 @@ function get_field_data_input_value($input_elem: JQuery): string | undefined { return JSON.stringify(proposed_value); } -export let new_group_can_manage_group_widget: DropdownWidget | null = null; export let new_group_can_mention_group_widget: DropdownWidget | null = null; const dropdown_widget_map = new Map([ @@ -476,7 +482,6 @@ const dropdown_widget_map = new Map([ ["realm_create_multiuse_invite_group", null], ["can_remove_subscribers_group", null], ["realm_can_access_all_users_group", null], - ["can_manage_group", null], ["can_mention_group", null], ["realm_can_create_public_channel_group", null], ["realm_can_create_private_channel_group", null], @@ -513,10 +518,6 @@ export function set_new_group_can_mention_group_widget(widget: DropdownWidget): new_group_can_mention_group_widget = widget; } -export function set_new_group_can_manage_group_widget(widget: DropdownWidget): void { - new_group_can_manage_group_widget = widget; -} - export function set_dropdown_list_widget_setting_value( property_name: string, value: number | string, @@ -648,7 +649,7 @@ function get_input_type($input_elem: JQuery, input_type?: string): string { export function get_input_element_value( input_elem: HTMLElement, input_type?: string, -): boolean | number | string | null | undefined { +): boolean | number | string | null | undefined | GroupSettingType { const $input_elem = $(input_elem); input_type = get_input_type($input_elem, input_type); let input_value; @@ -696,6 +697,8 @@ export function get_input_element_value( return $input_elem.find(".language_selection_button span").attr("data-language-code"); case "auth-methods": return JSON.stringify(get_auth_method_list_data()); + case "group-setting-type": + return get_group_setting_widget_value($input_elem); default: return undefined; } @@ -869,6 +872,39 @@ export function check_stream_settings_property_changed( return current_val !== proposed_val; } +export function get_group_setting_widget_value($input_elem: JQuery): GroupSettingType { + const setting_name = extract_property_name($input_elem); + const pill_widget = get_group_setting_widget(setting_name); + assert(pill_widget !== null); + + const setting_pills = pill_widget.items(); + const direct_subgroups: number[] = []; + const direct_members: number[] = []; + for (const pill of setting_pills) { + if (pill.type === "user_group") { + direct_subgroups.push(pill.group_id); + } else { + assert(pill.user_id !== undefined); + direct_members.push(pill.user_id); + } + } + + if (direct_members.length === 0 && direct_subgroups.length === 0) { + const nobody_group = user_groups.get_user_group_from_name("role:nobody")!; + return nobody_group.id; + } + + if (direct_members.length === 0 && direct_subgroups.length === 1) { + assert(direct_subgroups[0] !== undefined); + return direct_subgroups[0]; + } + + return { + direct_subgroups, + direct_members, + }; +} + export function check_group_property_changed(elem: HTMLElement, group: UserGroup): boolean { const $elem = $(elem); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -877,6 +913,8 @@ export function check_group_property_changed(elem: HTMLElement, group: UserGroup let proposed_val; switch (property_name) { case "can_manage_group": + proposed_val = get_group_setting_widget_value($elem); + break; case "can_mention_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; @@ -1019,6 +1057,7 @@ export function populate_data_for_realm_settings_request( continue; } + assert(typeof input_value !== "object"); data[property_name] = input_value; } } @@ -1046,6 +1085,8 @@ export function populate_data_for_stream_settings_request( }; continue; } + + assert(typeof input_value !== "object"); data[property_name] = input_value; } } @@ -1089,6 +1130,7 @@ export function populate_data_for_custom_profile_field_request( const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { const property_name = extract_property_name($input_elem); + assert(typeof input_value !== "object"); data[property_name] = input_value; } } @@ -1107,6 +1149,7 @@ export function populate_data_for_default_realm_settings_request( const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { const property_name: string = extract_property_name($input_elem, true); + assert(typeof input_value !== "object"); data[property_name] = input_value; if (property_name === "dense_mode") { @@ -1348,3 +1391,93 @@ export function initialize_disable_btn_hint_popover( } tippy.default(util.the($btn_wrapper), tippy_opts); } + +const group_setting_widget_map = new Map([ + ["can_manage_group", null], + ["new_group_can_manage_group", null], +]); + +export function get_group_setting_widget(setting_name: string): GroupSettingPillContainer | null { + const pill_widget = group_setting_widget_map.get(setting_name); + + if (pill_widget === undefined) { + blueslip.error("No dropdown list widget for property", {setting_name}); + return null; + } + + return pill_widget; +} + +export function set_group_setting_widget_value( + property_name: string, + property_value: GroupSettingType, +): void { + const pill_widget = get_group_setting_widget(property_name); + assert(pill_widget !== null); + pill_widget.clear(); + + if (typeof property_value === "number") { + const user_group = user_groups.get_user_group_from_id(property_value); + if (user_group.name === "role:nobody") { + return; + } + user_group_pill.append_user_group(user_group, pill_widget); + } else { + for (const setting_sub_group_id of property_value.direct_subgroups) { + const user_group = user_groups.get_user_group_from_id(setting_sub_group_id); + if (user_group.name === "role:nobody") { + continue; + } + user_group_pill.append_user_group(user_group, pill_widget); + } + for (const setting_user_id of property_value.direct_members) { + const user = people.get_user_by_id_assert_valid(setting_user_id); + user_pill.append_user(user, pill_widget); + } + } +} + +export type GroupSettingType = z.output; + +type group_setting_name = "can_manage_group"; + +export function create_group_setting_widget({ + $pill_container, + setting_name, + group, +}: { + $pill_container: JQuery; + setting_name: group_setting_name; + group?: UserGroup; +}): void { + const pill_widget = group_setting_pill.create_pills($pill_container, setting_name); + const opts = { + setting_name, + group, + }; + group_setting_pill.set_up_pill_typeahead({pill_widget, $pill_container, opts}); + + if (group === undefined) { + group_setting_widget_map.set("new_group_" + setting_name, pill_widget); + } else { + group_setting_widget_map.set(setting_name, pill_widget); + } + + if (group !== undefined) { + set_group_setting_widget_value(setting_name, group[setting_name]); + + pill_widget.onPillCreate(() => { + save_discard_group_widget_status_handler($("#group_permission_settings"), group); + }); + pill_widget.onPillRemove(() => { + save_discard_group_widget_status_handler($("#group_permission_settings"), group); + }); + } else { + const default_group_name = group_permission_settings.get_group_permission_setting_config( + setting_name, + "group", + )!.default_group_name; + const default_group_id = user_groups.get_user_group_from_name(default_group_name)!.id; + set_group_setting_widget_value("new_group_" + setting_name, default_group_id); + } +} diff --git a/web/src/settings_data.ts b/web/src/settings_data.ts index 031c5b86d3..1bcbd37412 100644 --- a/web/src/settings_data.ts +++ b/web/src/settings_data.ts @@ -4,6 +4,7 @@ import * as group_permission_settings from "./group_permission_settings"; import {page_params} from "./page_params"; import * as settings_config from "./settings_config"; import {current_user, realm} from "./state_data"; +import type {GroupSettingType} from "./state_data"; import * as user_groups from "./user_groups"; import {user_settings} from "./user_settings"; @@ -109,7 +110,7 @@ function user_has_permission(policy_value: number): boolean { } export function user_has_permission_for_group_setting( - setting_group_id: number, + setting_group_id: GroupSettingType, setting_name: string, setting_type: "realm" | "stream" | "group", ): boolean { @@ -123,7 +124,7 @@ export function user_has_permission_for_group_setting( return false; } - return user_groups.is_user_in_group(setting_group_id, current_user.user_id); + return user_groups.is_user_in_setting_group(setting_group_id, current_user.user_id); } export function user_can_invite_users_by_email(): boolean { diff --git a/web/src/settings_org.js b/web/src/settings_org.js index 88409f4a33..45d8e6d9f9 100644 --- a/web/src/settings_org.js +++ b/web/src/settings_org.js @@ -613,8 +613,10 @@ export function discard_group_property_element_changes(elem, group) { const property_name = settings_components.extract_property_name($elem); const property_value = settings_components.get_group_property_value(property_name, group); - if (property_name === "can_manage_group" || property_name === "can_mention_group") { + if (property_name === "can_mention_group") { settings_components.set_dropdown_list_widget_setting_value(property_name, property_value); + } else if (property_name === "can_manage_group") { + settings_components.set_group_setting_widget_value(property_name, property_value); } else if (property_value !== undefined) { settings_components.set_input_element_value($elem, property_value); } else { diff --git a/web/src/settings_preferences.ts b/web/src/settings_preferences.ts index 9f0d90de9b..c22ce2bfa8 100644 --- a/web/src/settings_preferences.ts +++ b/web/src/settings_preferences.ts @@ -256,6 +256,7 @@ export function set_up(settings_panel: SettingsPanel): void { assert(setting !== undefined); const data: Record = {}; const setting_value = settings_components.get_input_element_value(this)!; + assert(typeof setting_value !== "object"); data[setting] = setting_value; if (setting === "dense_mode") { diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 33e53e600e..2f607e5c35 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -144,7 +144,7 @@ export const user_group_schema = z.object({ members: z.array(z.number()), is_system_group: z.boolean(), direct_subgroup_ids: z.array(z.number()), - can_manage_group: z.number(), + can_manage_group: group_setting_type_schema, can_mention_group: z.number(), deactivated: z.boolean(), }); diff --git a/web/src/user_group_components.ts b/web/src/user_group_components.ts index 365c6e4c80..ba2b865ba9 100644 --- a/web/src/user_group_components.ts +++ b/web/src/user_group_components.ts @@ -10,7 +10,7 @@ import type {UserGroup} from "./user_groups"; export let active_group_id: number | undefined; -type group_setting = "can_manage_group" | "can_mention_group"; +type group_setting = "can_mention_group"; export function setup_permissions_dropdown( setting_name: group_setting, group: UserGroup | undefined, @@ -58,11 +58,7 @@ export function setup_permissions_dropdown( }, }); if (for_group_creation) { - if (setting_name === "can_mention_group") { - settings_components.set_new_group_can_mention_group_widget(group_setting_widget); - } else { - settings_components.set_new_group_can_manage_group_widget(group_setting_widget); - } + settings_components.set_new_group_can_mention_group_widget(group_setting_widget); } else { settings_components.set_dropdown_setting_widget(setting_name, group_setting_widget); } diff --git a/web/src/user_group_create.ts b/web/src/user_group_create.ts index 4795050a12..6f7b30cf9c 100644 --- a/web/src/user_group_create.ts +++ b/web/src/user_group_create.ts @@ -150,13 +150,9 @@ function create_user_group(): void { } const user_ids = user_group_create_members.get_principals(); - assert(settings_components.new_group_can_manage_group_widget !== null); - const can_manage_group_value = settings_components.new_group_can_manage_group_widget.value(); - assert(can_manage_group_value !== undefined); - const can_manage_group = - typeof can_manage_group_value === "number" - ? can_manage_group_value - : Number.parseInt(can_manage_group_value, 10); + const can_manage_group = settings_components.get_group_setting_widget_value( + $("#id_new_group_can_manage_group"), + ); assert(settings_components.new_group_can_mention_group_widget !== null); const can_mention_group_value = settings_components.new_group_can_mention_group_widget.value(); @@ -170,8 +166,8 @@ function create_user_group(): void { name: group_name, description, members: JSON.stringify(user_ids), - can_manage_group, can_mention_group, + can_manage_group: JSON.stringify(can_manage_group), }; loading.make_indicator($("#user_group_creating_indicator"), { text: $t({defaultMessage: "Creating group..."}), @@ -243,6 +239,11 @@ export function set_up_handlers(): void { } }); - user_group_components.setup_permissions_dropdown("can_manage_group", undefined, true); + const $pill_container = $container.find(".can-manage-group-container .pill-container"); + settings_components.create_group_setting_widget({ + $pill_container, + setting_name: "can_manage_group", + }); + user_group_components.setup_permissions_dropdown("can_mention_group", undefined, true); } diff --git a/web/src/user_group_edit.js b/web/src/user_group_edit.js index 18d37a06f9..200b7d8fce 100644 --- a/web/src/user_group_edit.js +++ b/web/src/user_group_edit.js @@ -117,15 +117,25 @@ function update_group_permission_settings_elements(group) { const $permission_dropdown_elements = $group_permission_settings.find(".dropdown-widget-button"); + const $permission_pill_container_elements = $group_permission_settings.find(".pill-container"); + if (settings_data.can_edit_user_group(group.id)) { $permission_dropdown_elements.prop("disabled", false); + $permission_pill_container_elements.find(".input").prop("contenteditable", true); + $permission_pill_container_elements.removeClass("group_setting_disabled"); $permission_dropdown_elements.each(function () { const $dropdown_wrapper = $(this).closest(".dropdown_widget_with_label_wrapper"); $dropdown_wrapper[0]._tippy?.destroy(); }); + + $permission_pill_container_elements.each(function () { + $(this)[0]._tippy?.destroy(); + }); } else { $permission_dropdown_elements.prop("disabled", true); + $permission_pill_container_elements.find(".input").prop("contenteditable", false); + $permission_pill_container_elements.addClass("group_setting_disabled"); $permission_dropdown_elements.each(function () { const $dropdown_wrapper = $(this).closest(".dropdown_widget_with_label_wrapper"); @@ -134,6 +144,13 @@ function update_group_permission_settings_elements(group) { $t({defaultMessage: "You do not have permission to edit this setting."}), ); }); + + $permission_pill_container_elements.each(function () { + settings_components.initialize_disable_btn_hint_popover( + $(this), + $t({defaultMessage: "You do not have permission to edit this setting."}), + ); + }); } } @@ -150,12 +167,18 @@ function show_membership_settings(group) { } function show_general_settings(group) { - user_group_components.setup_permissions_dropdown("can_manage_group", group, false); user_group_components.setup_permissions_dropdown("can_mention_group", group, false); + const $edit_container = get_edit_container(group); + const $pill_container = $edit_container.find(".can-manage-group-container .pill-container"); + settings_components.create_group_setting_widget({ + $pill_container, + setting_name: "can_manage_group", + group, + }); update_general_panel_ui(group); if (!page_params.development_environment) { - $("#can_manage_group_widget_container").hide(); + $(".can-manage-group-container").hide(); } } diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 0353c8637d..aec7231987 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -688,6 +688,16 @@ h4.user_group_setting_subsection_title { } } +.group_setting_disabled.pill-container { + cursor: not-allowed; + + .pill { + .exit { + display: none; + } + } +} + #groups_overlay, #subscription_overlay { .tab-switcher { @@ -1073,6 +1083,22 @@ div.settings-radio-input-parent { display: inline; } } + + .pill-container { + /* 319px + 2 * (2px padding) + 2 * (1px border) = 325px, which is the total + width of dropdown widget buttons */ + min-width: 319px; + background-color: hsl(0deg 0% 100%); + + .input { + flex-grow: 1; + + &:first-child:empty::before { + opacity: 0.5; + content: attr(data-placeholder); + } + } + } } .group-permissions .dropdown_widget_with_label_wrapper { diff --git a/web/templates/user_group_settings/group_permissions.hbs b/web/templates/user_group_settings/group_permissions.hbs index 0a4964dcf3..32f47427ab 100644 --- a/web/templates/user_group_settings/group_permissions.hbs +++ b/web/templates/user_group_settings/group_permissions.hbs @@ -3,7 +3,12 @@ label=(t 'Who can mention this group?') value_type="number"}} -{{> ../dropdown_widget_with_label - widget_name=can_manage_group_widget_name - label=(t 'Who can manage this group?') - value_type="number"}} +
+ +
+
+ {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
+
+