zed icon indicating copy to clipboard operation
zed copied to clipboard

Add channel reordering functionality

Open nathansobo opened this issue 7 months ago • 1 comments

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:

-- 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:

// 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:

// 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:

// 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:

// 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:

// 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:

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

nathansobo avatar May 31 '25 20:05 nathansobo

Squawk Report

šŸš’ 1 violations across 1 file(s)


crates/collab/migrations/20250530175450_add_channel_order.sql

-- Add channel_order column to channels table with default value
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;

-- Update channel_order for existing channels using ROW_NUMBER for deterministic ordering
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
);

-- Create index for efficient ordering queries
CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");

šŸš’ Rule Violations (1)

crates/collab/migrations/20250530175450_add_channel_order.sql:14:2: warning: require-concurrent-index-creation

  14 | -- Create index for efficient ordering queries
  15 | CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");

  note: Creating an index blocks writes.
  help: Create the index CONCURRENTLY.

šŸ“š More info on rules

āš”ļø Powered by Squawk (0.26.0), a linter for PostgreSQL, focused on migrations

github-actions[bot] avatar May 31 '25 20:05 github-actions[bot]