settings: Add UI to manage channel folders.

Fixes part of #35545.
This commit is contained in:
Sahil Batra
2025-08-01 08:46:33 +05:30
committed by Tim Abbott
parent 7732aa0423
commit c127a0ae9f
12 changed files with 186 additions and 2 deletions

View File

@@ -323,6 +323,10 @@ Source: https://lucide.dev/icons/undo-2
Copyright: 2013-2022 Cole Bemis
License: ISC License
Files: web/shared/icons/folder.svg
Copyright: 2013-2023 Cole Bemis (https://lucide.dev/icons/folder)
License: ISC License
Files: web/third/bootstrap/css/bootstrap.app.css
Copyright: 2012 Twitter, Inc.
License: Apache-2.0

View File

@@ -222,6 +222,7 @@ EXEMPT_FILES = make_set(
"web/src/settings_components.ts",
"web/src/settings_emoji.ts",
"web/src/settings_exports.ts",
"web/src/settings_folders.ts",
"web/src/settings_invites.ts",
"web/src/settings_linkifiers.ts",
"web/src/settings_muted_users.ts",

View File

@@ -0,0 +1,8 @@
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
d="M 4,2 C 2.354992,2 1,3.354992 1,5 v 13 c 0,1.645008 1.354992,3 3,3 h 16 c 1.645008,0 3,-1.354992 3,-3 V 8 C 23,6.354992 21.645008,5 20,5 h -7.900391 a 1.0001,1.0001 0 0 0 -0.0098,0 c -0.341098,0.00334 -0.656154,-0.1643212 -0.84375,-0.4492188 a 1.0001,1.0001 0 0 0 -0.0078,-0.00977 L 10.435547,3.3496094 C 9.8813254,2.5080344 8.937363,2.0001659 7.9296875,2 Z m 0,2 h 3.9296875 c 0.3374958,5.56e-5 0.6503148,0.1673542 0.8359375,0.4492187 a 1.0001,1.0001 0 0 0 0.00586,0.00977 l 0.8027344,1.1914062 c 7.94e-4,0.00121 0.00311,7.486e-4 0.00391,0.00195 C 10.138461,6.5006393 11.092583,7.0099688 12.109375,7 H 20 c 0.564129,0 1,0.4358712 1,1 v 10 c 0,0.564129 -0.435871,1 -1,1 H 4 C 3.4358712,19 3,18.564129 3,18 V 5 C 3,4.4358712 3.4358712,4 4,4 Z" />
</svg>

After

Width:  |  Height:  |  Size: 865 B

View File

@@ -26,7 +26,7 @@ export function initialize(params: StateData["channel_folders"]): void {
}
export function get_channel_folders(include_archived = false): ChannelFolder[] {
const channel_folders = [...channel_folder_by_id_dict.values()].sort((a, b) => a.id - b.id);
const channel_folders = [...channel_folder_by_id_dict.values()];
return channel_folders.filter((channel_folder) => {
if (!include_archived && channel_folder.is_archived) {
return false;

View File

@@ -63,6 +63,7 @@ import * as settings_components from "./settings_components.ts";
import * as settings_config from "./settings_config.ts";
import * as settings_emoji from "./settings_emoji.ts";
import * as settings_exports from "./settings_exports.ts";
import * as settings_folders from "./settings_folders.ts";
import * as settings_invites from "./settings_invites.ts";
import * as settings_linkifiers from "./settings_linkifiers.ts";
import * as settings_notifications from "./settings_notifications.ts";
@@ -122,6 +123,7 @@ export function dispatch_normal_event(event) {
case "add": {
channel_folders.add(event.channel_folder);
inbox_ui.complete_rerender();
settings_folders.populate_channel_folders();
break;
}
case "update":

View File

@@ -0,0 +1,99 @@
import $ from "jquery";
import SortableJS from "sortablejs";
import render_admin_channel_folder_list_item from "../templates/settings/admin_channel_folder_list_item.hbs";
import * as channel from "./channel.ts";
import * as channel_folders from "./channel_folders.ts";
import * as channel_folders_ui from "./channel_folders_ui.ts";
import * as ListWidget from "./list_widget.ts";
import * as settings_ui from "./settings_ui.ts";
import {current_user} from "./state_data.ts";
import * as util from "./util.ts";
const meta = {
loaded: false,
};
function update_folder_order(this: HTMLElement): void {
const order: number[] = [];
$(".channel-folder-row").each(function () {
order.push(Number.parseInt($(this).attr("data-channel-folder-id")!, 10));
});
settings_ui.do_settings_change(
channel.patch,
"/json/channel_folders",
{order: JSON.stringify(order)},
$("#admin-channel-folder-status").expectOne(),
);
}
export function do_populate_channel_folders(): void {
const folders_data = channel_folders.get_channel_folders();
const $channel_folders_table = $("#admin_channel_folders_table").expectOne();
ListWidget.create($channel_folders_table, folders_data, {
name: "channel_folders_list",
get_item(folder) {
return folder;
},
modifier_html(folder) {
return render_admin_channel_folder_list_item({
folder_name: folder.name,
folder_description: folder.description,
id: folder.id,
is_admin: current_user.is_admin,
});
},
$parent_container: $("#channel-folder-settings").expectOne(),
$simplebar_container: $("#channel-folder-settings .progressive-table-wrapper"),
});
if (current_user.is_admin) {
const field_list = util.the($("#admin_channel_folders_table"));
SortableJS.create(field_list, {
onUpdate: update_folder_order,
filter: "input",
preventOnFilter: false,
});
}
}
export function populate_channel_folders(): void {
if (!meta.loaded) {
// If outside callers call us when we're not loaded, just
// exit and we'll draw the widgets again during set_up().
return;
}
do_populate_channel_folders();
}
export function set_up(): void {
do_populate_channel_folders();
meta.loaded = true;
$("#channel-folder-settings").on(
"click",
".add-channel-folder-button",
channel_folders_ui.add_channel_folder,
);
$("#channel-folder-settings").on("click", ".edit-channel-folder-button", (e) => {
const folder_id = Number.parseInt(
$(e.target).closest(".channel-folder-row").attr("data-channel-folder-id")!,
10,
);
channel_folders_ui.handle_editing_channel_folder(folder_id);
});
$("#channel-folder-settings").on("click", ".archive-channel-folder-button", (e) => {
const folder_id = Number.parseInt(
$(e.target).closest(".channel-folder-row").attr("data-channel-folder-id")!,
10,
);
channel_folders_ui.handle_archiving_channel_folder(folder_id);
});
}
export function reset(): void {
meta.loaded = false;
}

View File

@@ -7,6 +7,7 @@ import * as settings_account from "./settings_account.ts";
import * as settings_bots from "./settings_bots.ts";
import * as settings_emoji from "./settings_emoji.ts";
import * as settings_exports from "./settings_exports.ts";
import * as settings_folders from "./settings_folders.ts";
import * as settings_invites from "./settings_invites.ts";
import * as settings_linkifiers from "./settings_linkifiers.ts";
import * as settings_muted_users from "./settings_muted_users.ts";
@@ -78,6 +79,7 @@ export function initialize(): void {
"organization-level-user-defaults",
settings_realm_user_settings_defaults.set_up,
);
load_func_dict.set("channel-folders", settings_folders.set_up);
}
export function load_settings_section(section: string): void {
@@ -115,5 +117,6 @@ export function reset_sections(): void {
settings_user_topics.reset();
settings_muted_users.reset();
alert_words_ui.reset();
settings_folders.reset();
// settings_users doesn't need a reset()
}

View File

@@ -902,7 +902,8 @@ input[type="checkbox"] {
.admin_profile_fields_table,
.edit_profile_field_choices_container,
.profile_field_choices_table,
.admin_linkifiers_table {
.admin_linkifiers_table,
.admin_channel_folders_table {
.movable-row {
.move-handle {
cursor: move;

View File

@@ -0,0 +1,31 @@
<tr class="channel-folder-row movable-row" data-channel-folder-id="{{id}}">
<td class="channel_folder_name_container">
{{#if is_admin}}
<span class="move-handle">
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
</span>
{{/if}}
<span class="channel_folder_name">{{folder_name}}</span>
</td>
<td class="channel_folder_description_container">
<span class="channel_folder_description">{{folder_description}}</span>
</td>
{{#if is_admin}}
<td class="actions">
{{> ../components/icon_button
icon="edit"
intent="neutral"
custom_classes="tippy-zulip-delayed-tooltip edit-channel-folder-button"
data-tippy-content=(t "Edit")
}}
{{> ../components/icon_button
icon="trash"
intent="danger"
custom_classes="tippy-zulip-delayed-tooltip archive-channel-folder-button"
data-tippy-content=(t "Delete")
aria-label=(t "Delete")
}}
</td>
{{/if}}
</tr>

View File

@@ -0,0 +1,28 @@
<div id="channel-folder-settings" class="settings-section" data-name="channel-folders">
<div class="settings_panel_list_header">
<h3>{{t "Channel folders"}}</h3>
<div class="alert-notification" id="admin-channel-folder-status"></div>
{{#if is_admin}}
{{> ../components/action_button
custom_classes="add-channel-folder-button"
label=(t "Add a new channel folder")
attention="quiet"
intent="brand"
}}
{{/if}}
</div>
<div class="progressive-table-wrapper" data-simplebar data-simplebar-tab-index="-1">
<table class="table table-striped admin_channel_folders_table">
<thead>
<tr>
<th>{{t "Name" }}</th>
<th>{{t "Description" }}</th>
{{#if is_admin}}
<th class="actions">{{t "Actions" }}</th>
{{/if}}
</tr>
</thead>
<tbody id="admin_channel_folders_table" data-empty="{{t 'No channel folders configured.' }}"></tbody>
</table>
</div>
</div>

View File

@@ -17,6 +17,8 @@
{{> bot_list_admin . }}
{{> admin_channel_folders .}}
{{> default_streams_list_admin . }}
{{> auth_methods_settings_admin . }}

View File

@@ -109,6 +109,11 @@
<div class="text">{{t "Default user settings" }}</div>
<i class="locked fa fa-lock tippy-zulip-tooltip" {{#if is_admin}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i>
</li>
<li class="sidebar-item collapse-org-settings {{#unless is_admin}}hide-org-settings{{/unless}}" tabindex="0" data-section="channel-folders">
<i class="sidebar-item-icon zulip-icon zulip-icon-folder"></i>
<div class="text">{{t "Channel folders" }}</div>
<i class="locked fa fa-lock tippy-zulip-tooltip" {{#if is_admin}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i>
</li>
{{#unless is_guest}}
<li class="sidebar-item collapse-org-settings {{#unless is_admin}}hide-org-settings{{/unless}}" tabindex="0" data-section="default-channels-list">
<i class="sidebar-item-icon fa fa-exchange" aria-hidden="true"></i>