streams: Add UI to add or remove stream from a folder.

This commit adds UI to add a stream to a folder while
creating them and also for adding/removing an existing
stream from a folder.
This commit is contained in:
Sahil Batra
2025-05-29 12:43:51 +05:30
committed by Tim Abbott
parent 403b73e1a6
commit 7c01e61e5a
16 changed files with 176 additions and 2 deletions

View File

@@ -8,8 +8,10 @@ import render_compose_banner from "../templates/compose_banner/compose_banner.hb
import * as blueslip from "./blueslip.ts";
import * as buttons from "./buttons.ts";
import * as channel_folders from "./channel_folders.ts";
import * as compose_banner from "./compose_banner.ts";
import type {DropdownWidget} from "./dropdown_widget.ts";
import * as dropdown_widget from "./dropdown_widget.ts";
import * as group_permission_settings from "./group_permission_settings.ts";
import type {AssignedGroupPermission, GroupGroupSettingName} from "./group_permission_settings.ts";
import * as group_setting_pill from "./group_setting_pill.ts";
@@ -476,6 +478,7 @@ const dropdown_widget_map = new Map<string, DropdownWidget | null>([
["realm_default_code_block_language", null],
["realm_can_access_all_users_group", null],
["realm_can_create_web_public_channel_group", null],
["folder_id", null],
]);
export function get_widget_for_dropdown_list_settings(
@@ -902,6 +905,9 @@ export function check_stream_settings_property_changed(
case "stream_privacy":
proposed_val = get_input_element_value(elem, "radio-group");
break;
case "folder_id":
proposed_val = get_channel_folder_value_from_dropdown_widget($(elem));
break;
default:
if (current_val !== undefined) {
proposed_val = get_input_element_value(elem, typeof current_val);
@@ -1154,6 +1160,12 @@ export function populate_data_for_stream_settings_request(
continue;
}
if (property_name === "folder_id") {
const folder_id = get_channel_folder_value_from_dropdown_widget($input_elem);
data[property_name] = JSON.stringify(folder_id);
continue;
}
assert(typeof input_value !== "object");
data[property_name] = input_value;
}
@@ -1940,3 +1952,79 @@ export function get_group_assigned_user_group_permissions(group: UserGroup): {
return group_assigned_user_group_permissions;
}
export function set_up_folder_dropdown_widget(sub?: StreamSubscription): DropdownWidget {
const folder_options = (): dropdown_widget.Option[] => {
const folders = channel_folders.get_channel_folders();
const options: dropdown_widget.Option[] = folders.map((folder) => ({
name: folder.name,
unique_id: folder.id,
}));
const disabled_option = {
is_setting_disabled: true,
show_disabled_icon: false,
show_disabled_option_name: true,
unique_id: settings_config.no_folder_selected,
name: $t({defaultMessage: "None"}),
};
options.unshift(disabled_option);
return options;
};
const default_id = sub?.folder_id ?? settings_config.no_folder_selected;
let widget_name = "folder_id";
if (sub === undefined) {
widget_name = "new_channel_folder_id";
}
let $events_container = $("#stream_settings .subscription_settings");
if (sub === undefined) {
$events_container = $("#stream_creation_form");
}
const folder_widget = new dropdown_widget.DropdownWidget({
widget_name,
get_options: folder_options,
$events_container,
item_click_callback(event, dropdown, this_widget) {
dropdown.hide();
event.preventDefault();
event.stopPropagation();
this_widget.render();
if (sub !== undefined) {
const $edit_container = stream_settings_containers.get_edit_container(sub);
save_discard_stream_settings_widget_status_handler(
$edit_container.find(".channel-folder-subsection"),
stream_data.get_sub_by_id(sub.stream_id),
);
}
},
default_id,
unique_id_type: "number",
});
if (sub !== undefined) {
set_dropdown_setting_widget("folder_id", folder_widget);
}
folder_widget.setup();
return folder_widget;
}
export function set_channel_folder_dropdown_value(sub: StreamSubscription): void {
if (sub.folder_id === null) {
set_dropdown_list_widget_setting_value("folder_id", settings_config.no_folder_selected);
return;
}
set_dropdown_list_widget_setting_value("folder_id", sub.folder_id);
}
export function get_channel_folder_value_from_dropdown_widget($elem: JQuery): number | null {
const value = get_dropdown_list_widget_setting_value($elem);
assert(typeof value === "number");
if (value === settings_config.no_folder_selected) {
return null;
}
return value;
}

View File

@@ -1274,3 +1274,5 @@ export const realm_plan_types = {
standard_free: {code: 4},
plus: {code: 10},
};
export const no_folder_selected = -1;

View File

@@ -627,6 +627,9 @@ export function discard_stream_property_element_changes(
case "message_retention_days":
set_message_retention_setting_dropdown(sub);
break;
case "folder_id":
settings_components.set_channel_folder_dropdown_value(sub);
break;
default:
if (property_value !== undefined) {
const validated_property_value = z

View File

@@ -8,12 +8,14 @@ import render_change_stream_info_modal from "../templates/stream_settings/change
import * as channel from "./channel.ts";
import * as confirm_dialog from "./confirm_dialog.ts";
import * as dialog_widget from "./dialog_widget.ts";
import type {DropdownWidget} from "./dropdown_widget.ts";
import {$t, $t_html} from "./i18n.ts";
import * as keydown_util from "./keydown_util.ts";
import * as loading from "./loading.ts";
import * as onboarding_steps from "./onboarding_steps.ts";
import * as resize from "./resize.ts";
import * as settings_components from "./settings_components.ts";
import * as settings_config from "./settings_config.ts";
import * as settings_data from "./settings_data.ts";
import {current_user, realm} from "./state_data.ts";
import * as stream_create_subscribers from "./stream_create_subscribers.ts";
@@ -32,6 +34,8 @@ let created_stream: string | undefined;
// the subscribers list initially.
let current_user_subscribed_to_created_stream = true;
let folder_widget: DropdownWidget | undefined;
export function reset_created_stream(): void {
created_stream = undefined;
}
@@ -383,7 +387,7 @@ function create_stream(): void {
text: $t({defaultMessage: "Creating channel..."}),
});
const data = {
const data: Record<string, string> = {
subscriptions,
is_web_public: JSON.stringify(is_web_public),
invite_only: JSON.stringify(invite_only),
@@ -395,6 +399,14 @@ function create_stream(): void {
...group_setting_values,
};
assert(folder_widget !== undefined);
const folder_id = folder_widget.value();
if (folder_id !== settings_config.no_folder_selected) {
// We do not include "folder_id" in request data if
// new stream will not be added to any folder.
data.folder_id = JSON.stringify(folder_id);
}
// Subscribe yourself and possible other people to a new stream.
void channel.post({
url: "/json/users/me/subscriptions",
@@ -621,6 +633,7 @@ export function set_up_handlers(): void {
set_up_group_setting_widgets();
settings_components.enable_opening_typeahead_on_clicking_label($container);
folder_widget = settings_components.set_up_folder_dropdown_widget();
}
export function initialize(): void {

View File

@@ -451,6 +451,10 @@ export function update_stream_permission_group_setting(
sub[setting_name] = group_setting;
}
export function update_channel_folder(sub: StreamSubscription, folder_id: number | null): void {
sub.folder_id = folder_id;
}
export function receives_notifications(
stream_id: number,
notification_name: keyof StreamSpecificNotificationSettings,

View File

@@ -291,6 +291,7 @@ export function show_settings_for(node: HTMLElement): void {
stream_ui_updates.enable_or_disable_permission_settings_in_edit_panel(sub);
setup_group_setting_widgets(slim_sub);
stream_ui_updates.update_can_subscribe_group_label($edit_container);
settings_components.set_up_folder_dropdown_widget(sub);
$("#channels_overlay_container").on(
"click",

View File

@@ -214,6 +214,9 @@ export function update_property<P extends keyof UpdatableStreamProperties>(
}
message_live_update.rerender_messages_view();
},
folder_id(value) {
stream_settings_ui.update_channel_folder(sub, value);
},
};
if (Object.hasOwn(updaters, property) && updaters[property] !== undefined) {

View File

@@ -231,6 +231,11 @@ export function update_is_default_stream(): void {
}
}
export function update_channel_folder(sub: StreamSubscription, folder_id: number | null): void {
stream_data.update_channel_folder(sub, folder_id);
stream_ui_updates.update_channel_folder_dropdown(sub);
}
export function update_subscribers_ui(sub: StreamSubscription): void {
update_left_panel_row(sub);
stream_edit_subscribers.update_subscribers_list(sub);

View File

@@ -41,6 +41,7 @@ export const stream_schema = z.object({
can_send_message_group: group_setting_value_schema,
can_subscribe_group: group_setting_value_schema,
is_recently_active: z.boolean(),
folder_id: z.number().nullable(),
});
export const stream_specific_notification_settings_schema = z.object({

View File

@@ -352,6 +352,10 @@ export function enable_or_disable_permission_settings_in_edit_panel(
.find(".input")
.prop("contenteditable", sub.can_change_stream_permissions_requiring_metadata_access);
$stream_settings
.find(".channel-folder-widget-container button")
.prop("disabled", !sub.can_change_stream_permissions_requiring_metadata_access);
if (!sub.can_change_stream_permissions_requiring_metadata_access) {
$general_settings_container.find(".default-stream").addClass("control-label-disabled");
$permission_pill_container_elements
@@ -607,3 +611,11 @@ export function update_stream_privacy_choices(policy: string): void {
update_web_public_stream_privacy_option_state($container);
}
}
export function update_channel_folder_dropdown(sub: StreamSubscription): void {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
settings_components.set_channel_folder_dropdown_value(sub);
}

View File

@@ -1358,6 +1358,14 @@ div.settings-radio-input-parent {
margin: -6px 0 -6px -10px;
}
#subscription_overlay .channel-folder-widget-container {
.dropdown_widget_value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
#deactivation-confirm-modal {
.alert {
padding-right: 14px;

View File

@@ -27,7 +27,8 @@
<div class="stream-types">
{{> stream_types .
is_stream_edit=false
prefix="id_new_" }}
prefix="id_new_"
channel_folder_widget_name="new_channel_folder_id"}}
</div>
</section>
</div>

View File

@@ -87,6 +87,7 @@
is_stream_edit=true
prefix="id_"
group_setting_labels=../group_setting_labels
channel_folder_widget_name="folder_id"
}}
{{/with}}
<div class="stream_details_box">

View File

@@ -43,6 +43,25 @@
</div>
</div>
<div class="channel-folder-subsection {{#if is_stream_edit}}settings-subsection-parent{{/if}}">
<div class="channel-folder-title-container {{#if is_stream_edit}}subsection-header{{/if}}">
<h3 class="stream_setting_subsection_title">{{t "Folders"}}</h3>
{{#if is_stream_edit}}
{{> ../settings/settings_save_discard_widget section_name="stream-permissions" }}
{{/if}}
</div>
<div class="input-group channel-folder-container">
<label class="settings-field-label" for="{{channel_folder_widget_name}}_widget">
{{t "Channel folder"}}
</label>
<span class="prop-element hide" id="id_{{channel_folder_widget_name}}" data-setting-widget-type="dropdown-list-widget" data-setting-value-type="number"></span>
<div class="dropdown_widget_with_label_wrapper channel-folder-widget-container">
{{> ../dropdown_widget widget_name=channel_folder_widget_name}}
</div>
</div>
</div>
<div class="advanced-configurations-container {{#if is_stream_edit}}settings-subsection-parent{{/if}}">
<div class="advance-config-title-container {{#if is_stream_edit}}subsection-header{{/if}}">
<div class="advance-config-toggle-area">

View File

@@ -839,11 +839,13 @@ test("stream_settings", ({override}) => {
sub,
moderators_group.id,
);
stream_data.update_channel_folder(sub, 3);
assert.equal(sub.invite_only, false);
assert.equal(sub.history_public_to_subscribers, false);
assert.equal(sub.message_retention_days, -1);
assert.equal(sub.can_remove_subscribers_group, moderators_group.id);
assert.equal(sub.can_administer_channel_group, moderators_group.id);
assert.equal(sub.folder_id, 3);
// For guest user only retrieve subscribed streams
sub_rows = stream_settings_data.get_updated_unsorted_subs();

View File

@@ -327,6 +327,17 @@ test("update_property", ({override}) => {
assert.equal(args.sub, sub);
}
// Update channel folder
{
const stub = make_stub();
override(stream_settings_ui, "update_channel_folder", stub.f);
stream_events.update_property(stream_id, "folder_id", 3);
assert.equal(stub.num_calls, 1);
const args = stub.get_args("sub", "value");
assert.equal(args.sub.stream_id, stream_id);
assert.equal(args.value, 3);
}
// Test archiving stream
{
stream_data.subscribe_myself(sub);