diff --git a/web/src/channel_folders.ts b/web/src/channel_folders.ts index 0e55a011dc..2627b8e8d4 100644 --- a/web/src/channel_folders.ts +++ b/web/src/channel_folders.ts @@ -8,6 +8,9 @@ export type ChannelFolder = z.infer; let channel_folder_name_dict: FoldDict; let channel_folder_by_id_dict: Map; +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); diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 5a985e3384..8a957bd450 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -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); diff --git a/web/src/stream_edit.ts b/web/src/stream_edit.ts index d8deac1cf9..9375fff0df 100644 --- a/web/src/stream_edit.ts +++ b/web/src/stream_edit.ts @@ -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: $("input#new_channel_folder_name").val()!.trim(), + description: $("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, + }); + }); } diff --git a/web/src/stream_settings_ui.ts b/web/src/stream_settings_ui.ts index 5774bceb31..30a1c27d87 100644 --- a/web/src/stream_settings_ui.ts +++ b/web/src/stream_settings_ui.ts @@ -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); diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index f9aee8f0bf..43e92d9d10 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -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; diff --git a/web/templates/stream_settings/create_channel_folder_modal.hbs b/web/templates/stream_settings/create_channel_folder_modal.hbs new file mode 100644 index 0000000000..229ee02905 --- /dev/null +++ b/web/templates/stream_settings/create_channel_folder_modal.hbs @@ -0,0 +1,12 @@ +
+ + +
+
+ + +
diff --git a/web/templates/stream_settings/stream_settings.hbs b/web/templates/stream_settings/stream_settings.hbs index d4ef85bdcd..a133baaf49 100644 --- a/web/templates/stream_settings/stream_settings.hbs +++ b/web/templates/stream_settings/stream_settings.hbs @@ -88,6 +88,7 @@ prefix="id_" group_setting_labels=../group_setting_labels channel_folder_widget_name="folder_id" + is_admin=../is_admin }} {{/with}}
diff --git a/web/templates/stream_settings/stream_types.hbs b/web/templates/stream_settings/stream_types.hbs index 719b8f52f9..493ae32a99 100644 --- a/web/templates/stream_settings/stream_types.hbs +++ b/web/templates/stream_settings/stream_types.hbs @@ -52,12 +52,26 @@
+ {{!-- 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. --}}
diff --git a/web/tests/dispatch.test.cjs b/web/tests/dispatch.test.cjs index 38772e8a73..c86d491e8c 100644 --- a/web/tests/dispatch.test.cjs +++ b/web/tests/dispatch.test.cjs @@ -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")); diff --git a/web/tests/lib/events.cjs b/web/tests/lib/events.cjs index 38847b8658..987ab05268 100644 --- a/web/tests/lib/events.cjs +++ b/web/tests/lib/events.cjs @@ -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: "

Channels for frontend discussions

", + date_created: 1681662420, + creator_id: 10, + is_archived: false, + }, + }, + channel_typing_edit_message__start: { type: "typing_edit_message", op: "start",