Skip to content

feat(groups): add drag-and-drop reorder for group list#398

Merged
tbphp merged 3 commits intotbphp:devfrom
QLHazyCoder:main
Mar 24, 2026
Merged

feat(groups): add drag-and-drop reorder for group list#398
tbphp merged 3 commits intotbphp:devfrom
QLHazyCoder:main

Conversation

@QLHazyCoder
Copy link
Copy Markdown
Contributor

@QLHazyCoder QLHazyCoder commented Mar 23, 2026

变更内容 / Change Content

  • Bug fix

  • New feature

  • Other changes

  • Add PUT /api/groups/reorder endpoint with transactional batch update for group sort order.

  • Implement drag-and-drop reordering in GroupList.vue with visual drop indicators and rollback on save failure.

  • Disable drag sorting while searching and on touch devices to avoid ambiguous interactions.

  • Add i18n messages for reorder validation and UI feedback in zh-CN, en-US, and ja-JP.

自查清单 / Checklist

  • I have tested my changes locally.
  • I have updated the necessary documentation.

Summary by CodeRabbit

  • New Features

    • Drag-and-drop group reordering in the UI with persistent save
    • New API endpoint to persist reordered groups
  • Bug Fixes

    • Improved validation and error handling for reorder requests (empty/invalid/duplicate items, missing groups)
    • Prevented a server-side error path that could cause failures during reorder operations
  • Localization

    • Added reorder-related messages and success text in English, Japanese, and Simplified Chinese
  • Accessibility

    • Screen-reader hints and graceful behavior on touch devices for drag handles

- Add PUT /api/groups/reorder endpoint with transactional batch update
- Implement drag-and-drop UI in GroupList.vue with visual feedback
- Support touch device detection and search mode restrictions
- Add i18n messages for zh-CN, en-US, ja-JP
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3048b658-337c-454a-a585-f94e498ee598

📥 Commits

Reviewing files that changed from the base of the PR and between ea939c9 and 3f8dfeb.

📒 Files selected for processing (1)
  • web/src/components/keys/GroupList.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/keys/GroupList.vue

Walkthrough

Adds backend and frontend support for reordering groups: new PUT API endpoint and handler, service method that validates and transactionally updates group sort orders with row locks and cache invalidation, localization strings, client API call, and drag-and-drop UI in GroupList.vue.

Changes

Cohort / File(s) Summary
Handler & Router
internal/handler/group_handler.go, internal/router/router.go
Added ReorderGroups handler and request types (GroupReorderRequest, GroupReorderItemRequest), validateGroupReorderItems, and defensive handleGroupError nil check. Registered PUT /api/groups/reorder.
Service Layer
internal/services/group_service.go
Added GroupReorderItem DTO and ReorderGroups(ctx, items) implementing input validation, transaction with row-level FOR UPDATE locks, existence check, per-row sort updates, commit/rollback, and post-commit cache invalidation (logs errors but does not fail).
Backend i18n
internal/i18n/locales/en-US.go, internal/i18n/locales/ja-JP.go, internal/i18n/locales/zh-CN.go
Added localization keys for reorder validation errors and success.groups_reordered.
Frontend API
web/src/api/keys.ts
Added keysApi.reorderGroups(items) which PUTs /groups/reorder (hides UI messages).
Frontend Component & i18n
web/src/components/keys/GroupList.vue, web/src/locales/en-US.ts, web/src/locales/ja-JP.ts, web/src/locales/zh-CN.ts
Implemented in-component drag-and-drop with displayGroups, drag/drop state, drop-target geometry, persistence via keysApi.reorderGroups, rollback on failure, conditional enabling (touch, search, loading), ARIA hints, and added UI locale strings for drag hints and messages.

Sequence Diagram

sequenceDiagram
    actor User
    participant Vue as GroupList Component
    participant API as keysApi.reorderGroups()
    participant Handler as ReorderGroups Handler
    participant Service as GroupService.ReorderGroups()
    participant DB as Database
    participant Cache as Group Cache Manager

    User->>Vue: Drag & drop groups
    Vue->>Vue: Update displayGroups locally
    User->>Vue: Drop to finalize
    Vue->>API: PUT /groups/reorder (items)
    API->>Handler: HTTP request -> bind & validate
    alt Validation fails
        Handler-->>API: Return I18n validation error
        API-->>Vue: Error response
        Vue->>Vue: Restore previous order & show error
    else
        Handler->>Service: ReorderGroups(ctx, items)
        Service->>DB: Begin transaction
        Service->>DB: SELECT COUNT(id) WHERE id IN (items) FOR UPDATE
        alt Count mismatch
            Service-->>Handler: Return I18n not-found error
            Handler-->>Vue: Error response
            Vue->>Vue: Restore previous order & show error
        else
            Service->>DB: UPDATE groups SET sort = ? WHERE id = ? (FOR UPDATE)
            Service->>DB: Commit
            Service->>Cache: Invalidate()
            Service-->>Handler: success
            Handler-->>Vue: success.groups_reordered
            Vue->>Vue: Show success & sync displayGroups
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
I hopped through code to sort the rows,
Dragged tiny groups where order goes.
Transactions held, cache given a tweak,
A rabbit’s nudge — the lists now speak.
Hooray for hops and tidy shows!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding drag-and-drop reordering capability to the group list component.
Description check ✅ Passed The description covers the change type (new feature), lists key implementation changes, and completes the checklist items; however, it does not include the related issue reference in the 'Closes #' field as specified in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/components/keys/GroupList.vue (1)

384-417: ⚠️ Potential issue | 🟠 Major

The drag handle is still unreachable for keyboard users.

This <div role="button"> is not focusable and has no keyboard move interaction, so the new reorder flow is effectively mouse-only. The hint at Lines 415-417 also never helps keyboard users if the control cannot receive focus. Please add a real button plus keyboard reordering, or a separate non-drag reorder control.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/keys/GroupList.vue` around lines 384 - 417, The drag
handle (div.group-icon) is not keyboard-accessible; make it a real focusable
control and add keyboard reorder support by replacing the non-focusable <div
class="group-icon"> with a <button> (or add tabindex="0") and wire keydown
handlers that call the existing drag/reorder logic (use
handleDragStart(group.id)/handleDragEnd or delegate to new methods like
handleReorderUp(group.id) and handleReorderDown(group.id)); ensure the
aria-describedby still references `drag-hint-${group.id}` (dragDisabledHint) and
update the handlers referenced in the template (group-icon, handleDragStart,
handleDragEnd, dragDisabledHint) so keyboard users can focus the control and use
ArrowUp/ArrowDown (or Space/Enter to pick up + Esc to cancel) to reorder items.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/handler/group_handler.go`:
- Around line 137-166: Change GroupReorderItemRequest.Sort from int to *int so
you can distinguish omitted vs explicit 0 (update the struct
GroupReorderItemRequest). In validateGroupReorderItems, add a nil check for
item.Sort and return the "validation.reorder_sort_negative" (or a new
"validation.reorder_sort_required") error when item.Sort == nil, and keep the
negative-value check against *item.Sort. Finally, update the code that maps
request items into the internal model/DB (the block that reads Items and assigns
Sort) to dereference the pointer (*item.Sort) when constructing the target
objects.
- Around line 27-30: The type assertion for services.I18nError currently returns
false when svcErr is a typed-nil, which incorrectly signals the error wasn't
handled; change the logic in the block that checks "if svcErr, ok :=
err.(*services.I18nError); ok {" so that if svcErr == nil you treat it as an
internal error (handle/log/respond as other I18nError cases) and return true
instead of false; ensure the branch for nil svcErr follows the same
error-response/handling pathway as the non-nil svcErr cases so callers see the
error was handled.

In `@internal/services/group_service.go`:
- Around line 315-325: The COUNT-based existence check is racy; after counting
you must ensure each Update actually modified a row or lock the target rows
beforehand. Fix by either selecting and locking the groups before the loop
(e.g., use tx.Clauses(clause.Locking{Strength:"UPDATE"}).Where("id IN ?",
ids).Find(&groups)) then perform updates, or after each
tx.Model(&models.Group{}).Where("id = ?", item.ID).Update("sort", item.Sort)
check the result.RowsAffected and if RowsAffected != 1 return
NewI18nError(app_errors.ErrValidation, "validation.reorder_group_not_found",
nil); ensure you use the tx result variable (e.g., res :=
tx.Model(...).Update(...)) to inspect res.Error and res.RowsAffected and return
parsed DB errors when res.Error != nil.

In `@web/src/components/keys/GroupList.vue`:
- Around line 233-239: On failed reorder (inside the catch block around
reorderGroups) don't just roll back displayGroups to previousOrder; also
re-fetch the authoritative list from the server to avoid leaving a stale local
state. Update the catch path that references reorderGroups, displayGroups,
previousOrder, savingOrder and suspendAutoScroll to call the same refresh/fetch
routine you use in Keys.vue (the view-level fetch or refreshGroups function) or
emit the existing refresh event so the component reloads groups from the server,
then keep the existing error toast.

---

Outside diff comments:
In `@web/src/components/keys/GroupList.vue`:
- Around line 384-417: The drag handle (div.group-icon) is not
keyboard-accessible; make it a real focusable control and add keyboard reorder
support by replacing the non-focusable <div class="group-icon"> with a <button>
(or add tabindex="0") and wire keydown handlers that call the existing
drag/reorder logic (use handleDragStart(group.id)/handleDragEnd or delegate to
new methods like handleReorderUp(group.id) and handleReorderDown(group.id));
ensure the aria-describedby still references `drag-hint-${group.id}`
(dragDisabledHint) and update the handlers referenced in the template
(group-icon, handleDragStart, handleDragEnd, dragDisabledHint) so keyboard users
can focus the control and use ArrowUp/ArrowDown (or Space/Enter to pick up + Esc
to cancel) to reorder items.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7cc4250-8485-4199-9c76-e1fd64ec5cef

📥 Commits

Reviewing files that changed from the base of the PR and between 8ffcb46 and cce6b4e.

📒 Files selected for processing (11)
  • internal/handler/group_handler.go
  • internal/i18n/locales/en-US.go
  • internal/i18n/locales/ja-JP.go
  • internal/i18n/locales/zh-CN.go
  • internal/router/router.go
  • internal/services/group_service.go
  • web/src/api/keys.ts
  • web/src/components/keys/GroupList.vue
  • web/src/locales/en-US.ts
  • web/src/locales/ja-JP.ts
  • web/src/locales/zh-CN.ts

@QLHazyCoder
Copy link
Copy Markdown
Contributor Author

我核对后处理如下:

  1. “COUNT 后逐条 UPDATE 可能部分成功”这个问题成立,已修复。
    现在在事务内先锁定并确认目标行存在,然后每次 UPDATE 都检查 RowsAffected == 1;任一步不满足都会返回 validation.reorder_group_not_found,并由 deferred rollback 回滚整笔事务。

  2. “catch 分支未和服务端重新同步”这个问题也成立,已修复。
    失败分支在本地回滚 UI 后,额外触发一次 refresh,避免服务端已提交但客户端因超时/断连误判失败时出现长期不一致。

  3. “typed-nil *services.I18nError”我没有按建议修改。
    当前仓库里没有发现会把 (*services.I18nError)(nil) 塞进 error 的真实返回路径,现有 I18nError 都通过 NewI18nError(...) 构造,属于理论性提醒,不是这次提交里的实际 bug。

  4. “GroupReorderItemRequest.Sort 应改为 *int”我暂未修改。
    这条更像接口契约收紧建议,不是当前前端主路径上的 bug,因为前端 reorder 请求始终显式传 sort。若后续要增强 API 健壮性,我建议只把 Sort 改成 *int 并校验 nil;ID 不需要改成指针,因为缺失后会落到现有的 item.ID == 0 校验里。

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/keys/GroupList.vue`:
- Around line 379-383: The ref access inside the template ref callback is using
groupItemRefs.set(...) on a Ref<Map<...>> which causes a runtime error; update
the callback to call groupItemRefs.value.set(group.id, el as HTMLElement) (and
ensure null checks for el and group.id remain) so you operate on the underlying
Map; reference the existing symbol groupItemRefs and the callback that checks
group.id to locate and fix the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb785a89-99bd-4623-9a4d-b0a11af88a79

📥 Commits

Reviewing files that changed from the base of the PR and between cce6b4e and ea939c9.

📒 Files selected for processing (2)
  • internal/services/group_service.go
  • web/src/components/keys/GroupList.vue

@tbphp tbphp self-requested a review March 24, 2026 12:09
@tbphp tbphp added the enhancement New feature or request label Mar 24, 2026
@tbphp tbphp added this to the v1.4.5 milestone Mar 24, 2026
@tbphp tbphp changed the base branch from main to dev March 24, 2026 12:12
@tbphp tbphp merged commit 21a2e8e into tbphp:dev Mar 24, 2026
1 check passed
@tbphp
Copy link
Copy Markdown
Owner

tbphp commented Mar 24, 2026

@QLHazyCoder 🎉 感谢您的贡献,此PR已合并,并在内部进行测试。
如果测试通过将在下个版本发布。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants