ZIm/crates/collab
Nathan Sobo 0a2186c87b
Add channel reordering functionality (#31833)
Release Notes:

- Added channel reordering for administrators (use `cmd-up` and
`cmd-down` on macOS or `ctrl-up` `ctrl-down` on Linux to move channels
up or down within their parent)

## Summary

This PR introduces the ability for channel administrators to reorder
channels within their parent context, providing better organizational
control over channel hierarchies. Users can now move channels up or down
relative to their siblings using keyboard shortcuts.

## Problem

Previously, channels were displayed in alphabetical order with no way to
customize their arrangement. This made it difficult for teams to
organize channels in a logical order that reflected their workflow or
importance, forcing users to prefix channel names with numbers or
special characters as a workaround.

## Solution

The implementation adds a persistent `channel_order` field to channels
that determines their display order within their parent. Channels with
the same parent are sorted by this field rather than alphabetically.

## Implementation Details

### Database Schema

Added a new column and index to support efficient ordering:

```sql
-- crates/collab/migrations/20250530175450_add_channel_order.sql
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;

CREATE INDEX CONCURRENTLY "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
```

### RPC Protocol

Extended the channel proto with ordering support:

```proto
// crates/proto/proto/channel.proto
message Channel {
    uint64 id = 1;
    string name = 2;
    ChannelVisibility visibility = 3;
    int32 channel_order = 4;
    repeated uint64 parent_path = 5;
}

message ReorderChannel {
    uint64 channel_id = 1;
    enum Direction {
        Up = 0;
        Down = 1;
    }
    Direction direction = 2;
}
```

### Server-side Logic

The reordering is handled by swapping `channel_order` values between
adjacent channels:

```rust
// crates/collab/src/db/queries/channels.rs
pub async fn reorder_channel(
    &self,
    channel_id: ChannelId,
    direction: proto::reorder_channel::Direction,
    user_id: UserId,
) -> Result<Vec<Channel>> {
    // Find the sibling channel to swap with
    let sibling_channel = match direction {
        proto::reorder_channel::Direction::Up => {
            // Find channel with highest order less than current
            channel::Entity::find()
                .filter(
                    channel::Column::ParentPath
                        .eq(&channel.parent_path)
                        .and(channel::Column::ChannelOrder.lt(channel.channel_order)),
                )
                .order_by_desc(channel::Column::ChannelOrder)
                .one(&*tx)
                .await?
        }
        // Similar logic for Down...
    };
    
    // Swap the channel_order values
    let temp_order = channel.channel_order;
    channel.channel_order = sibling_channel.channel_order;
    sibling_channel.channel_order = temp_order;
}
```

### Client-side Sorting

Optimized the sorting algorithm to avoid O(n²) complexity:

```rust
// crates/collab/src/db/queries/channels.rs
// Pre-compute sort keys for efficient O(n log n) sorting
let mut channels_with_keys: Vec<(Vec<i32>, Channel)> = channels
    .into_iter()
    .map(|channel| {
        let mut sort_key = Vec::with_capacity(channel.parent_path.len() + 1);
        
        // Build sort key from parent path orders
        for parent_id in &channel.parent_path {
            sort_key.push(channel_order_map.get(parent_id).copied().unwrap_or(i32::MAX));
        }
        sort_key.push(channel.channel_order);
        
        (sort_key, channel)
    })
    .collect();

channels_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
```

### User Interface

Added keyboard shortcuts and proper context handling:

```json
// assets/keymaps/default-macos.json
{
  "context": "CollabPanel && not_editing",
  "bindings": {
    "cmd-up": "collab_panel::MoveChannelUp",
    "cmd-down": "collab_panel::MoveChannelDown"
  }
}
```

The CollabPanel now properly sets context to distinguish between editing
and navigation modes:

```rust
// crates/collab_ui/src/collab_panel.rs
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
    let mut dispatch_context = KeyContext::new_with_defaults();
    dispatch_context.add("CollabPanel");
    dispatch_context.add("menu");
    
    let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window) {
        "editing"
    } else {
        "not_editing"
    };
    
    dispatch_context.add(identifier);
    dispatch_context
}
```

## Testing

Comprehensive tests were added to verify:
- Basic reordering functionality (up/down movement)
- Boundary conditions (first/last channels)
- Permission checks (non-admins cannot reorder)
- Ordering persistence across server restarts
- Correct broadcasting of changes to channel members

## Migration Strategy

Existing channels are assigned initial `channel_order` values based on
their current alphabetical sorting to maintain the familiar order users
expect:

```sql
UPDATE channels
SET channel_order = (
    SELECT ROW_NUMBER() OVER (
        PARTITION BY parent_path
        ORDER BY name, id
    )
    FROM channels c2
    WHERE c2.id = channels.id
);
```

## Future Enhancements

While this PR provides basic reordering functionality, potential future
improvements could include:
- Drag-and-drop reordering in the UI
- Bulk reordering operations
- Custom sorting strategies (by activity, creation date, etc.)

## Checklist

- [x] Database migration included
- [x] Tests added for new functionality
- [x] Keybindings work on macOS and Linux
- [x] Permissions properly enforced
- [x] Error handling implemented throughout
- [x] Manual testing completed
- [x] Documentation updated

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-06-04 16:56:33 +00:00
..
k8s collab: Increase max database connections to 250 (#31553) 2025-05-27 16:48:50 -04:00
migrations Add channel reordering functionality (#31833) 2025-06-04 16:56:33 +00:00
migrations.sqlite Add channel reordering functionality (#31833) 2025-06-04 16:56:33 +00:00
migrations_llm collab: Drop billing_events table (#31131) 2025-05-21 22:43:46 +00:00
seed collab: Seed GitHub users from static data (#18301) 2024-09-24 16:35:09 -04:00
src Add channel reordering functionality (#31833) 2025-06-04 16:56:33 +00:00
.env.toml Move git status updates to a background thread #2 (#24722) 2025-02-14 16:47:11 +02:00
Cargo.toml collab: Introduce StripeClient trait to abstract over Stripe interactions (#31615) 2025-05-28 18:34:44 +00:00
LICENSE-AGPL chore: Add crate licenses. (#4158) 2024-01-23 16:56:22 +01:00
postgrest_app.conf Add Postgrest to Docker Compose (#16498) 2024-08-19 20:50:45 -04:00
postgrest_llm.conf Add Postgrest to Docker Compose (#16498) 2024-08-19 20:50:45 -04:00
README.md docs: Update mentions to GitHub to use correct capitalization (#31996) 2025-06-03 14:55:24 -03:00
seed.default.json collab: Seed GitHub users from static data (#18301) 2024-09-24 16:35:09 -04:00

Zed Server

This crate is what we run at https://collab.zed.dev.

It contains our back-end logic for collaboration, to which we connect from the Zed client via a websocket after authenticating via https://zed.dev, which is a separate repo running on Vercel.

Local Development

Database setup

Before you can run the collab server locally, you'll need to set up a zed Postgres database. Follow the steps sequentially:

  1. Ensure you have postgres installed. If not, install with brew install postgresql@15.
  2. Follow the steps on Brew's formula and verify your $PATH contains /opt/homebrew/opt/postgresql@15/bin.
  3. If you hadn't done it before, create the postgres user with createuser -s postgres.
  4. You are now ready to run the bootstrap script:
script/bootstrap

This script will set up the zed Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API.

The script will create several admin users, who you'll sign in as by default when developing locally. The GitHub logins for the default users are specified in the seed.default.json file.

To use a different set of admin users, create crates/collab/seed.json.

{
  "admins": ["yourgithubhere"],
  "channels": ["zed"]
}

Testing collaborative features locally

In one terminal, run Zed's collaboration server and the livekit dev server:

foreman start

In a second terminal, run two or more instances of Zed.

script/zed-local -2

This script starts one to four instances of Zed, depending on the -2, -3 or -4 flags. Each instance will be connected to the local collab server, signed in as a different user from seed.json or seed.default.json.

Deployment

We run two instances of collab:

Both of these run on the Kubernetes cluster hosted in Digital Ocean.

Deployment is triggered by pushing to the collab-staging (or collab-production) tag in GitHub. The best way to do this is:

  • ./script/deploy-collab staging
  • ./script/deploy-collab production

You can tell what is currently deployed with ./script/what-is-deployed.

Database Migrations

To create a new migration:

./script/create-migration <name>

Migrations are run automatically on service start, so run foreman start again. The service will crash if the migrations fail.

When you create a new migration, you also need to update the SQLite schema that is used for testing.