mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add pinned chats with drag-to-reorder (#23615)
https://github.com/user-attachments/assets/bd5d12a1-61b3-4b7d-83b6-317bdfb60b3c ## Summary Adds pinned chats to the agents page sidebar with server-side persistence and drag-to-reorder. Users can pin/unpin chats via the context menu, and pinned chats appear in a dedicated "Pinned" section above the time-grouped list. ## Database Migration `000453_chat_pin_order`: adds `pin_order integer DEFAULT 0 NOT NULL` column on `chats` (0 = unpinned, 1+ = pinned in display order). Three SQL queries handle pin operations server-side using CTEs with `ROW_NUMBER()`: - `PinChatByID`: normalizes existing orders and appends to end - `UnpinChatByID`: sets target to 0 and compacts remaining pins - `UpdateChatPinOrder`: shifts neighbors, clamps to `[1, pinned_count]` All queries exclude archived chats. `ArchiveChatByID` clears `pin_order` on archive. The handler rejects pinning archived chats with 400. ## Backend Pin/unpin/reorder go through the existing `PATCH /api/experimental/chats/{chat}` via the `pin_order` field on `UpdateChatRequest`. The handler routes based on current pin state: `pin_order == 0` unpins, `> 0` on an already-pinned chat reorders, `> 0` on an unpinned chat appends to end. ## Frontend - `pinChat` / `unpinChat` / `reorderPinnedChat` optimistic mutations using shared `isChatListQuery` predicate - Sidebar renders Pinned section above time groups, excludes pinned chats from time groups - Pin/Unpin context menu items (hidden for child/delegated chats) - `@dnd-kit/core` + `@dnd-kit/sortable` for drag-to-reorder with `MouseSensor`, `TouchSensor`, and `KeyboardSensor` - Local pin-order override prevents flash on drop; click blocker prevents NavLink navigation after drag --- *PR generated with Coder Agents*
This commit is contained in:
@@ -1,10 +1,178 @@
|
||||
-- name: ArchiveChatByID :exec
|
||||
UPDATE chats SET archived = true, updated_at = NOW()
|
||||
UPDATE chats SET archived = true, pin_order = 0, updated_at = NOW()
|
||||
WHERE id = @id OR root_chat_id = @id;
|
||||
|
||||
-- name: UnarchiveChatByID :exec
|
||||
UPDATE chats SET archived = false, updated_at = NOW() WHERE id = @id::uuid;
|
||||
|
||||
-- name: PinChatByID :exec
|
||||
WITH target_chat AS (
|
||||
SELECT
|
||||
id,
|
||||
owner_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
),
|
||||
-- Under READ COMMITTED, concurrent pin operations for the same
|
||||
-- owner may momentarily produce duplicate pin_order values because
|
||||
-- each CTE snapshot does not see the other's writes. The next
|
||||
-- pin/unpin/reorder operation's ROW_NUMBER() self-heals the
|
||||
-- sequence, so this is acceptable.
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.id,
|
||||
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS next_pin_order
|
||||
FROM
|
||||
chats c
|
||||
JOIN
|
||||
target_chat ON c.owner_id = target_chat.owner_id
|
||||
WHERE
|
||||
c.pin_order > 0
|
||||
AND c.archived = FALSE
|
||||
AND c.id <> target_chat.id
|
||||
),
|
||||
updates AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
ranked.next_pin_order AS pin_order
|
||||
FROM
|
||||
ranked
|
||||
UNION ALL
|
||||
SELECT
|
||||
target_chat.id,
|
||||
COALESCE((
|
||||
SELECT
|
||||
MAX(ranked.next_pin_order)
|
||||
FROM
|
||||
ranked
|
||||
), 0) + 1 AS pin_order
|
||||
FROM
|
||||
target_chat
|
||||
)
|
||||
UPDATE
|
||||
chats c
|
||||
SET
|
||||
pin_order = updates.pin_order
|
||||
FROM
|
||||
updates
|
||||
WHERE
|
||||
c.id = updates.id;
|
||||
|
||||
-- name: UnpinChatByID :exec
|
||||
WITH target_chat AS (
|
||||
SELECT
|
||||
id,
|
||||
owner_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.id,
|
||||
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position
|
||||
FROM
|
||||
chats c
|
||||
JOIN
|
||||
target_chat ON c.owner_id = target_chat.owner_id
|
||||
WHERE
|
||||
c.pin_order > 0
|
||||
AND c.archived = FALSE
|
||||
),
|
||||
target AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
ranked.current_position
|
||||
FROM
|
||||
ranked
|
||||
WHERE
|
||||
ranked.id = @id::uuid
|
||||
),
|
||||
updates AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
CASE
|
||||
WHEN ranked.id = target.id THEN 0
|
||||
WHEN ranked.current_position > target.current_position THEN ranked.current_position - 1
|
||||
ELSE ranked.current_position
|
||||
END AS pin_order
|
||||
FROM
|
||||
ranked
|
||||
CROSS JOIN
|
||||
target
|
||||
)
|
||||
UPDATE
|
||||
chats c
|
||||
SET
|
||||
pin_order = updates.pin_order
|
||||
FROM
|
||||
updates
|
||||
WHERE
|
||||
c.id = updates.id;
|
||||
|
||||
-- name: UpdateChatPinOrder :exec
|
||||
WITH target_chat AS (
|
||||
SELECT
|
||||
id,
|
||||
owner_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.id,
|
||||
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position,
|
||||
COUNT(*) OVER () :: integer AS pinned_count
|
||||
FROM
|
||||
chats c
|
||||
JOIN
|
||||
target_chat ON c.owner_id = target_chat.owner_id
|
||||
WHERE
|
||||
c.pin_order > 0
|
||||
AND c.archived = FALSE
|
||||
),
|
||||
target AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
ranked.current_position,
|
||||
LEAST(GREATEST(@pin_order::integer, 1), ranked.pinned_count) AS desired_position
|
||||
FROM
|
||||
ranked
|
||||
WHERE
|
||||
ranked.id = @id::uuid
|
||||
),
|
||||
updates AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
CASE
|
||||
WHEN ranked.id = target.id THEN target.desired_position
|
||||
WHEN target.desired_position < target.current_position
|
||||
AND ranked.current_position >= target.desired_position
|
||||
AND ranked.current_position < target.current_position THEN ranked.current_position + 1
|
||||
WHEN target.desired_position > target.current_position
|
||||
AND ranked.current_position > target.current_position
|
||||
AND ranked.current_position <= target.desired_position THEN ranked.current_position - 1
|
||||
ELSE ranked.current_position
|
||||
END AS pin_order
|
||||
FROM
|
||||
ranked
|
||||
CROSS JOIN
|
||||
target
|
||||
)
|
||||
UPDATE
|
||||
chats c
|
||||
SET
|
||||
pin_order = updates.pin_order
|
||||
FROM
|
||||
updates
|
||||
WHERE
|
||||
c.id = updates.id;
|
||||
|
||||
-- name: SoftDeleteChatMessagesAfterID :exec
|
||||
UPDATE
|
||||
chat_messages
|
||||
|
||||
Reference in New Issue
Block a user