channel-folders: Add UI to create new channel folder.

This commit adds a button besides the folder dropdowin in
stream settings UI which can be used to create a new folder.
This commit is contained in:
Sahil Batra
2025-05-29 18:22:10 +05:30
committed by Tim Abbott
parent 7c01e61e5a
commit 32287a084b
10 changed files with 143 additions and 1 deletions

View File

@@ -8,6 +8,9 @@ export type ChannelFolder = z.infer<typeof channel_folder_schema>;
let channel_folder_name_dict: FoldDict<ChannelFolder>;
let channel_folder_by_id_dict: Map<number, ChannelFolder>;
export const MAX_CHANNEL_FOLDER_NAME_LENGTH = 100;
export const MAX_CHANNEL_FOLDER_DESCRIPTION_LENGTH = 1024;
export function add(channel_folder: ChannelFolder): void {
channel_folder_name_dict.set(channel_folder.name, channel_folder);
channel_folder_by_id_dict.set(channel_folder.id, channel_folder);

View File

@@ -9,6 +9,7 @@ import * as blueslip from "./blueslip.ts";
import * as bot_data from "./bot_data.ts";
import * as browser_history from "./browser_history.ts";
import {buddy_list} from "./buddy_list.ts";
import * as channel_folders from "./channel_folders.ts";
import * as compose_call from "./compose_call.ts";
import * as compose_call_ui from "./compose_call_ui.ts";
import * as compose_closed_ui from "./compose_closed_ui.ts";
@@ -113,6 +114,18 @@ export function dispatch_normal_event(event) {
attachments_ui.update_attachments(event);
break;
case "channel_folder":
switch (event.op) {
case "add": {
channel_folders.add(event.channel_folder);
break;
}
default:
blueslip.error("Unexpected event type channel_folder/" + event.op);
break;
}
break;
case "custom_profile_fields":
realm.custom_profile_fields = event.fields;
settings_profile_fields.populate_profile_fields(realm.custom_profile_fields);

View File

@@ -10,6 +10,7 @@ import render_inline_decorated_channel_name from "../templates/inline_decorated_
import render_change_stream_info_modal from "../templates/stream_settings/change_stream_info_modal.hbs";
import render_confirm_stream_privacy_change_modal from "../templates/stream_settings/confirm_stream_privacy_change_modal.hbs";
import render_copy_email_address_modal from "../templates/stream_settings/copy_email_address_modal.hbs";
import render_create_channel_folder_modal from "../templates/stream_settings/create_channel_folder_modal.hbs";
import render_stream_description from "../templates/stream_settings/stream_description.hbs";
import render_stream_settings from "../templates/stream_settings/stream_settings.hbs";
@@ -17,6 +18,7 @@ import * as blueslip from "./blueslip.ts";
import type {Bot} from "./bot_data.ts";
import * as browser_history from "./browser_history.ts";
import * as channel from "./channel.ts";
import * as channel_folders from "./channel_folders.ts";
import * as confirm_dialog from "./confirm_dialog.ts";
import {show_copied_confirmation} from "./copied_tooltip.ts";
import * as dialog_widget from "./dialog_widget.ts";
@@ -920,4 +922,58 @@ export function initialize(): void {
}
},
);
$("#channels_overlay_container").on("click", ".create-channel-folder-button", () => {
const html_body = render_create_channel_folder_modal({
max_channel_folder_name_length: channel_folders.MAX_CHANNEL_FOLDER_NAME_LENGTH,
max_channel_folder_description_length:
channel_folders.MAX_CHANNEL_FOLDER_DESCRIPTION_LENGTH,
});
function create_channel_folder(): void {
const close_on_success = true;
const data = {
name: $<HTMLInputElement>("input#new_channel_folder_name").val()!.trim(),
description: $<HTMLTextAreaElement>("textarea#new_channel_folder_description")
.val()!
.trim(),
};
dialog_widget.submit_api_request(
channel.post,
"/json/channel_folders/create",
data,
{
success_continuation(response_data) {
const id = z
.object({channel_folder_id: z.number()})
.parse(response_data).channel_folder_id;
// This is a temporary channel folder object added
// to channel folders data, so that the folder is
// immediately visible in the dropdown.
// This will be replaced with the actual object once
// the client receives channel_folder/add event.
const channel_folder = {
id,
name: data.name,
description: data.description,
is_archived: false,
rendered_description: "",
date_created: 0,
creator_id: people.my_current_user_id(),
};
channel_folders.add(channel_folder);
},
},
close_on_success,
);
}
dialog_widget.launch({
html_heading: $t_html({defaultMessage: "Create channel folder"}),
html_body,
id: "create_channel_folder",
html_submit_button: $t_html({defaultMessage: "Create"}),
on_click: create_channel_folder,
loading_spinner: true,
});
});
}

View File

@@ -869,6 +869,7 @@ function setup_page(callback: () => void): void {
group_setting_labels: settings_config.all_group_setting_labels.stream,
realm_has_archived_channels,
has_billing_access: settings_data.user_has_billing_access(),
is_admin: current_user.is_admin,
};
const rendered = render_stream_settings_overlay(template_data);

View File

@@ -1323,7 +1323,8 @@ div.settings-radio-input-parent {
}
#change_user_group_description,
#change_stream_description {
#change_stream_description,
#new_channel_folder_description {
width: 100%;
height: 80px;
margin-bottom: 4px;
@@ -1359,6 +1360,15 @@ div.settings-radio-input-parent {
}
#subscription_overlay .channel-folder-widget-container {
display: flex;
gap: 0.5em;
width: auto;
/* Set minimum width such that the total width of dropdown
button and "Create folder" button is at least equal to the
minimum width of pill inputs for permission settings. */
min-width: var(--modal-input-width);
flex-wrap: wrap;
.dropdown_widget_value {
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -0,0 +1,12 @@
<div>
<label for="new_channel_folder_name" class="modal-field-label">
{{t 'Channel folder name' }}
</label>
<input type="text" id="new_channel_folder_name" class="modal_text_input" name="channel_folder_name" maxlength="{{ max_channel_folder_name_length }}" />
</div>
<div>
<label for="new_channel_folder_description" class="modal-field-label">
{{t 'Description' }}
</label>
<textarea id="new_channel_folder_description" class="settings_textarea" name="channel_folder_description" maxlength="{{ max_channel_folder_description_length }}"></textarea>
</div>

View File

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

View File

@@ -52,12 +52,26 @@
</div>
<div class="input-group channel-folder-container">
{{!-- This is a modified version of dropdown_widget_with_label.hbs
component so that we can show dropdown button and button to create
a new folder on same line without having to add much CSS with
hardcoded margin and padding values. --}}
<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}}
{{#if is_admin}}
{{> ../components/action_button
label=(t "Create new folder")
attention="quiet"
intent="neutral"
type="button"
custom_classes="create-channel-folder-button"
}}
{{/if}}
</div>
</div>
</div>

View File

@@ -137,6 +137,7 @@ page_params.test_suite = false;
// For data-oriented modules, just use them, don't stub them.
const alert_words = zrequire("alert_words");
const channel_folders = zrequire("channel_folders");
const emoji = zrequire("emoji");
const message_store = zrequire("message_store");
const people = zrequire("people");
@@ -487,6 +488,23 @@ run_test("scheduled_messages", ({override}) => {
}
});
run_test("channel_folders", () => {
channel_folders.initialize({channel_folders: []});
const event = event_fixtures.channel_folder__add;
{
dispatch(event);
const folders = channel_folders.get_channel_folders();
assert.equal(folders.length, 1);
assert.equal(folders[0].id, event.channel_folder.id);
assert.equal(folders[0].name, event.channel_folder.name);
}
blueslip.expect("error", "Unexpected event type channel_folder/other");
server_events_dispatch.dispatch_normal_event({type: "channel_folder", op: "other"});
});
run_test("realm settings", ({override}) => {
override(current_user, "is_admin", true);
override(realm, "realm_date_created", new Date("2023-01-01Z"));

View File

@@ -137,6 +137,20 @@ exports.fixtures = {
upload_space_used: 90000,
},
channel_folder__add: {
type: "channel_folder",
op: "add",
channel_folder: {
id: 1,
name: "Frontend",
description: "Channels for frontend discussions",
rendered_description: "<p>Channels for frontend discussions</p>",
date_created: 1681662420,
creator_id: 10,
is_archived: false,
},
},
channel_typing_edit_message__start: {
type: "typing_edit_message",
op: "start",