feat(mail): 支持自定义邮件文件夹排序

- 后端为 `folders` 增加 `sort_order`,并提供文件夹重排接口。
- 前端邮件侧边栏支持自定义文件夹拖拽排序与乐观更新。
- 补充文件夹排序相关测试,并同步更新 API 类型定义。
This commit is contained in:
LanQin_
2026-06-25 15:42:36 +08:00
parent 7799d5d5b2
commit d2a762a426
10 changed files with 453 additions and 24 deletions
+84
View File
@@ -1047,6 +1047,80 @@ func TestCustomMailFoldersCreateAndMove(t *testing.T) {
}
}
func TestCustomMailFoldersReorder(t *testing.T) {
a := newTestApp(t)
ts := httptest.NewServer(a.Router())
defer ts.Close()
admin := &testClient{t: t, server: ts}
var login map[string]any
if code := admin.do("POST", "/api/auth/login", map[string]string{"email": "admin@lanqin.local", "password": "ChangeMe123!"}, &login); code != http.StatusOK {
t.Fatalf("login code=%d body=%v", code, login)
}
createFolder := func(name string) MailFolder {
t.Helper()
var folder MailFolder
if code := admin.do("POST", "/api/mail/folders", map[string]string{"name": name}, &folder); code != http.StatusCreated {
t.Fatalf("create folder %s code=%d folder=%+v", name, code, folder)
}
return folder
}
customer := createFolder("客户")
bills := createFolder("账单")
project := createFolder("项目")
var ok map[string]any
if code := admin.do("POST", "/api/mail/folders/reorder", map[string]any{"folderIds": []string{project.ID, customer.ID, bills.ID}}, &ok); code != http.StatusOK {
t.Fatalf("reorder code=%d body=%v", code, ok)
}
if code := admin.do("POST", "/api/mail/folders/reorder", map[string]any{"folders": []map[string]any{
{"id": customer.ID, "sortOrder": 500},
{"id": project.ID, "sortOrder": 2500},
{"id": bills.ID, "sortOrder": 3500},
}}, &ok); code != http.StatusOK {
t.Fatalf("reorder with explicit sort order code=%d body=%v", code, ok)
}
var folders struct {
Items []MailFolder `json:"items"`
}
if code := admin.do("GET", "/api/mail/folders", nil, &folders); code != http.StatusOK {
t.Fatalf("list folders code=%d items=%+v", code, folders.Items)
}
got := customFolderNames(folders.Items)
want := []string{"客户", "项目", "账单"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("custom folder order=%v want=%v", got, want)
}
if folders.Items[0].ID != customer.ID {
t.Fatalf("customer folder should be before inbox after explicit sort order, first=%+v", folders.Items[0])
}
var inboxID string
for _, item := range folders.Items {
if item.Name == "Inbox" {
inboxID = item.ID
break
}
}
if inboxID == "" {
t.Fatalf("inbox not found in folders: %+v", folders.Items)
}
var bad map[string]any
if code := admin.do("POST", "/api/mail/folders/reorder", map[string]any{"folderIds": []string{project.ID, inboxID, customer.ID, bills.ID}}, &bad); code != http.StatusBadRequest {
t.Fatalf("reorder with system folder should be rejected code=%d body=%v", code, bad)
}
domain := createTestDomain(t, admin, "folders.test")
other := createTestMailbox(t, admin, domain.ID, "other", "Other", "Password123!", nil)
otherFolderID, err := a.ensureFolder(context.Background(), other.ID, "其他")
if err != nil {
t.Fatal(err)
}
if code := admin.do("POST", "/api/mail/folders/reorder", map[string]any{"folderIds": []string{project.ID, customer.ID, otherFolderID}}, &bad); code != http.StatusBadRequest {
t.Fatalf("reorder with other mailbox folder should be rejected code=%d body=%v", code, bad)
}
}
func TestCatchAllStoresUnregisteredMailForAdminOnly(t *testing.T) {
a := newTestApp(t)
ts := httptest.NewServer(a.Router())
@@ -3803,6 +3877,16 @@ func folderListContains(items []MailFolder, name string) bool {
return false
}
func customFolderNames(items []MailFolder) []string {
names := []string{}
for _, item := range items {
if !isSystemFolderName(item.Name) {
names = append(names, item.Name)
}
}
return names
}
func withoutPermissions(items []string, removed ...string) []string {
removedSet := map[string]bool{}
for _, item := range removed {