snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
243
docs/features/custom-sidebar-categories.md
Normal file
243
docs/features/custom-sidebar-categories.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Custom Sidebar Categories
|
||||
|
||||
## Overview
|
||||
|
||||
Custom categories let each user group their sidebar rooms into personal, named collections that appear
|
||||
alongside the system groups (Favorites, Teams, Channels, Direct messages, …). A user can create, rename,
|
||||
delete and reorder categories; move rooms in and out of them via context menus; and toggle whether each
|
||||
category keeps showing unread rooms while collapsed.
|
||||
|
||||
Categories are **per-user** and **client-only in meaning** — they are a presentation grouping of the user's
|
||||
own subscriptions. They carry **no access, security or membership semantics**: moving a room into a category
|
||||
never changes who can see the room or the room itself. Assignment is **exclusive** — a room lives in exactly
|
||||
one place at a time (one custom category, *or* Favorites, *or* its system group).
|
||||
|
||||
Requires the `experimental-enterprise-features` license module.
|
||||
|
||||
## Data Model
|
||||
|
||||
### Category record — `ISidebarCategory`
|
||||
|
||||
```ts
|
||||
// packages/core-typings/src/IUser.ts
|
||||
export interface ISidebarCategory {
|
||||
_id: string; // Random.id() for custom; system group key for defaults
|
||||
name: string; // display name (trimmed; max 30 chars)
|
||||
default?: boolean; // true for system-group entries managed by the platform
|
||||
showUnreads?: boolean; // when collapsed, keep listing unread rooms (default: false)
|
||||
keepUnreadsOnTop?: boolean; // when expanded, sort unread rooms to the top (default: false)
|
||||
}
|
||||
```
|
||||
|
||||
### Room assignment
|
||||
|
||||
Room-to-category assignment is stored on the **subscription**, not on the category:
|
||||
|
||||
```ts
|
||||
// Subscription.category: string | null
|
||||
```
|
||||
|
||||
Set via `POST /experimental/rooms.setCategory`. The subscription field is the source of truth for which
|
||||
category a room belongs to; the category record only carries display metadata and ordering.
|
||||
|
||||
## Persistence
|
||||
|
||||
### `sidebarCategories` preference (unified group ordering + category metadata)
|
||||
|
||||
The preference `sidebarCategories: ISidebarCategory[]` is the **single source of truth** for both the
|
||||
ordered list of all visible groups and the metadata of custom categories. It contains entries for custom
|
||||
categories *and* for any system group whose metadata has been touched (show-unreads, keep-unreads-on-top,
|
||||
or position).
|
||||
|
||||
**Invariant enforced on every write:** all five dynamic groups (`Incoming_Calls`, `Incoming_Livechats`,
|
||||
`Open_Livechats`, `On_Hold_Chats`, `Unread`) are always stored first, in the order prescribed by
|
||||
`sidebarSectionsOrder`. Custom and static system groups follow. This is enforced by `withDynamicFirst`
|
||||
in `useCategoryList.ts`, called by every write hook.
|
||||
|
||||
All mutations go through `usePersistCategoriesMutation`, which calls:
|
||||
|
||||
```http
|
||||
POST /v1/users.setPreferences { data: { sidebarCategories: ISidebarCategory[] } }
|
||||
```
|
||||
|
||||
### Room assignment — `POST /experimental/rooms.setCategory`
|
||||
|
||||
```http
|
||||
POST /experimental/rooms.setCategory
|
||||
{ roomIds: string[], category: string | null }
|
||||
```
|
||||
|
||||
- Requires `experimental-enterprise-features` license.
|
||||
- Rejects system group keys (`SIDEBAR_SYSTEM_GROUP_KEYS`) as the target category.
|
||||
- Verifies the target category exists in the user's `sidebarCategories` preference before writing.
|
||||
- Writes to `subscription.category` and fires change notifications.
|
||||
- `category: null` unassigns the room, returning it to its system group.
|
||||
|
||||
## Group Ordering
|
||||
|
||||
### `sidebarSectionsOrder` preference
|
||||
|
||||
An **admin-configurable** ordered list of system group keys that defines the default group order when
|
||||
`sidebarCategories` has no explicit entry for a key. Falls back to `SIDEBAR_SYSTEM_GROUP_KEYS`.
|
||||
|
||||
### `mergeWithSectionsOrder(explicitIds, sectionsOrder)` — READ side
|
||||
|
||||
Used in `useCategoryList` when building the visible category list. Inserts any key missing from
|
||||
`explicitIds` before its first successor that is already present, maintaining the `sectionsOrder`
|
||||
relative ordering for system groups.
|
||||
|
||||
### `withDynamicFirst(ids, sectionsOrder)` — WRITE side
|
||||
|
||||
Called by every write hook before persisting `sidebarCategories`. Guarantees:
|
||||
1. All five dynamic group keys appear first (in `sectionsOrder` canonical order), regardless of whether
|
||||
they are currently visible.
|
||||
2. Static ids (custom + system) follow, merged with `sectionsOrder` via `mergeWithSectionsOrder`.
|
||||
|
||||
This makes moving logic simple: swapping two adjacent static groups is always a valid operation — dynamic
|
||||
groups can never appear below a static group in the stored list, so a DOWN move can never cross one.
|
||||
|
||||
### `SIDEBAR_DYNAMIC_GROUP_KEYS`
|
||||
|
||||
```ts
|
||||
export const SIDEBAR_DYNAMIC_GROUP_KEYS: readonly string[] = [
|
||||
'Incoming_Calls', 'Incoming_Livechats', 'Open_Livechats', 'On_Hold_Chats', 'Unread',
|
||||
];
|
||||
```
|
||||
|
||||
Groups that are hidden from the sidebar when empty. They are excluded from user reordering
|
||||
(`canMoveGroup` returns `false` for them) and are always stored first by `withDynamicFirst`.
|
||||
|
||||
## Behaviors / Flows
|
||||
|
||||
### Create — `useCreateCustomCategory`
|
||||
|
||||
1. Generates `_id` with `Random.id()`.
|
||||
2. Inserts the new entry at the first position after the last dynamic group entry in the stored list
|
||||
(using `findLastIndex`), so it appears first among static groups.
|
||||
3. Optionally assigns rooms via `useSetCategory` in the same call (`Create and move` flow).
|
||||
|
||||
### Move room — `useMoveRoomCategory`
|
||||
|
||||
Accepts a `target` (category `_id`, `'favorites'` sentinel, or `undefined` to remove).
|
||||
- `'favorites'`: calls `POST /v1/rooms.favorite` and removes the category assignment.
|
||||
- Custom category `_id`: calls `POST /experimental/rooms.setCategory`.
|
||||
- `undefined` / same as current: removes assignment (unfavorites or sets category to `null`).
|
||||
- Dispatches a toast unless `silent: true` or the room has no name.
|
||||
|
||||
### Move category position — `useMoveCategoryPosition`
|
||||
|
||||
Takes `currentKeys` (visible group keys in render order), a `key` to move, and a direction.
|
||||
Performs an adjacent swap in `currentKeys`, then calls `withDynamicFirst` on the result before
|
||||
persisting — so dynamic groups are always re-sorted to the front regardless of what the swap produced.
|
||||
|
||||
### Toggle show-unreads / keep-unreads-on-top — `useToggleUnreads`
|
||||
|
||||
`upsertGroupEntry(id, patch)` — updates the matching `ISidebarCategory` entry in `sidebarCategories`
|
||||
(or creates a new one if the entry doesn't exist yet). Always calls `withDynamicFirst` before persisting.
|
||||
|
||||
### Delete — `useDeleteCategory`
|
||||
|
||||
Removes the category entry from `sidebarCategories` and calls `POST /experimental/rooms.setCategory`
|
||||
with `category: null` for all rooms that were in it — returning them to their system group.
|
||||
|
||||
### Name validation — `useValidateCategoryName`
|
||||
|
||||
Returns an i18n error string or `undefined`. Checks in order:
|
||||
1. Empty / whitespace → `Please_enter_a_category_name`
|
||||
2. Longer than `MAX_CATEGORY_NAME_LENGTH` (30) → `Category_name_is_too_long__max__maxLength__characters`
|
||||
3. Matches a translated system group name (case-insensitive) → `Category_name_conflicts_with_system_group`
|
||||
4. Duplicate among existing custom categories (case-insensitive, excluding `excludeId`) → `A_category_with_this_name_already_exists`
|
||||
|
||||
### Favorites interaction
|
||||
|
||||
`FAVORITES_TARGET = 'favorites'` is a sentinel for the Favorites system group. Moving a room to Favorites
|
||||
calls `POST /v1/rooms.favorite` and strips the category assignment. Moving a room from Favorites into a
|
||||
category unfavorites it. Assignment is exclusive.
|
||||
|
||||
### Visibility in the room list
|
||||
|
||||
`filterGroupVisibility` (in `useCategoryList.ts`) applies when building the room list:
|
||||
- **Dynamic groups** — only shown when non-empty.
|
||||
- **All other groups** (custom + static system) — always shown when EE is active, even if empty.
|
||||
|
||||
This means custom categories are always present in the visible group list with EE, so no category entry
|
||||
in `sidebarCategories` can be silently dropped during a move.
|
||||
|
||||
## Sidebar UI
|
||||
|
||||
### Category collapser kebab — `CategoryMenu.tsx`
|
||||
|
||||
**Custom category:**
|
||||
- Order: Move up / Move down / New channel
|
||||
- Manage: Rename / Delete / New category
|
||||
- When collapsed: Show unreads toggle
|
||||
- When expanded: Keep unreads on top toggle
|
||||
|
||||
**System group:**
|
||||
- Order: Move up / Move down (dynamic groups have both disabled)
|
||||
- When collapsed: Show unreads toggle
|
||||
- When expanded: Keep unreads on top toggle
|
||||
|
||||
### Room context menu — `CategoryRoomMenu.tsx`
|
||||
|
||||
For rooms inside a custom category, adds a "Remove from {category}" item.
|
||||
|
||||
### Room kebab — `useCategoryMenuItems.ts`
|
||||
|
||||
Builds the "Move to" submenu: Favorites + each custom category with a checkmark on the current one.
|
||||
Appended with a "New category" item and "Remove from {category}" when applicable.
|
||||
|
||||
### Room-header grouping menu — `RoomHeaderCategoryMenu.tsx`
|
||||
|
||||
Placed before the room title in `RoomHeader`. Icon: `star-filled` (favorited) / `folder` (custom category) / `star` (default). Same item list as the room kebab submenu.
|
||||
|
||||
### Modals — `categories/`
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `CreateCategoryModal.tsx` | Create or "create and move" — single name field, max 30 chars. |
|
||||
| `ManageCategoryModal.tsx` | Rename with duplicate/empty validation. |
|
||||
| `DeleteCategoryModal.tsx` | Confirmation + "rooms will return to default groups" warning. |
|
||||
| `hooks/useCategoryModals.tsx` | Single entry point: `openCreate(room?)` / `openManage(category)` / `openDelete(category)`. |
|
||||
|
||||
### Navbar create menu
|
||||
|
||||
`useCreateNewMenu.ts` adds a "Category" item (icon: `folder`) in the navbar `+` menu.
|
||||
|
||||
## REST Contract
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
|--------|----------|---------|
|
||||
| POST | `/v1/users.setPreferences` | Persist `sidebarCategories` (ordering + metadata) |
|
||||
| POST | `/experimental/rooms.setCategory` | Assign / unassign rooms to a category |
|
||||
| POST | `/v1/rooms.favorite` | Favorite / unfavorite (used by the Favorites target) |
|
||||
|
||||
## Key Files
|
||||
|
||||
| Layer | File |
|
||||
|-------|------|
|
||||
| Types | `packages/core-typings/src/IUser.ts` (`ISidebarCategory`, `SIDEBAR_SYSTEM_GROUP_KEYS`) |
|
||||
| Category list + ordering | `apps/meteor/client/sidebar/hooks/useCategoryList.ts` |
|
||||
| Room list | `apps/meteor/client/sidebar/hooks/useRoomList.ts` |
|
||||
| Persist mutation | `apps/meteor/client/sidebar/categories/hooks/usePersistCategoriesMutation.ts` |
|
||||
| Create category | `apps/meteor/client/sidebar/categories/hooks/useCreateCustomCategory.ts` |
|
||||
| Move room | `apps/meteor/client/sidebar/categories/hooks/useMoveRoomCategory.ts` |
|
||||
| Move category position | `apps/meteor/client/sidebar/categories/hooks/useMoveCategoryPosition.ts` |
|
||||
| Toggle unreads | `apps/meteor/client/sidebar/categories/hooks/useToggleUnreads.ts` |
|
||||
| Name validation | `apps/meteor/client/sidebar/categories/hooks/useValidateCategoryName.ts` |
|
||||
| Custom categories list | `apps/meteor/client/sidebar/categories/hooks/useCustomCategories.ts` |
|
||||
| Set category (client) | `apps/meteor/client/sidebar/categories/hooks/useSetCategory.ts` |
|
||||
| Server endpoint | `apps/meteor/server/api/experimental/rooms.setCategory.ts` |
|
||||
| Category UI | `apps/meteor/client/sidebar/categories/` |
|
||||
| Room kebab | `apps/meteor/client/sidebar/RoomMenu.tsx` |
|
||||
| Navbar create menu | `apps/meteor/client/navbar/NavBarPagesGroup/hooks/useCreateNewMenu.ts` |
|
||||
| Room-header grouping | `apps/meteor/client/views/room/Header/icons/RoomGroupingMenu.tsx` |
|
||||
| i18n | `packages/i18n/src/locales/en.i18n.json` |
|
||||
|
||||
## Tests
|
||||
|
||||
| Kind | File |
|
||||
|------|------|
|
||||
| API (preference persistence + validation) | `apps/meteor/tests/end-to-end/api/sidebar-custom-categories.ts` |
|
||||
| E2E (Playwright) | `apps/meteor/tests/e2e/sidebar-custom-categories.spec.ts` |
|
||||
| E2E page object | `apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts` (custom-category helpers) |
|
||||
Reference in New Issue
Block a user