mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +08:00
Support priority ordering in table and card views with automatic priority calculation, same-tier and between-tier drop targets, live previews, conflict detection, and undo. Keep drag mode off by default and place its toggle before Batch Operations. Consolidate sorting controls and preserve the shared visibility and view controls.
245 lines
7.3 KiB
Go
245 lines
7.3 KiB
Go
package model
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Keep priorities lossless in the web console's JSON number representation.
|
|
const maxSortablePriority int64 = 1<<53 - 1
|
|
|
|
var ErrChannelPriorityConflict = errors.New("Channel priorities changed. Refresh and try again.")
|
|
var ErrChannelPriorityInvalid = errors.New("Invalid channel priority move")
|
|
var ErrChannelPriorityRange = errors.New("Channel priority is outside the sortable range")
|
|
|
|
type ChannelPriority struct {
|
|
ID int `json:"id"`
|
|
Priority *int64 `json:"priority"`
|
|
}
|
|
|
|
func (item ChannelPriority) value() int64 {
|
|
if item.Priority == nil {
|
|
return 0
|
|
}
|
|
return *item.Priority
|
|
}
|
|
|
|
type ChannelPriorityChange struct {
|
|
ID int `json:"id"`
|
|
Previous *int64 `json:"previous"`
|
|
Priority *int64 `json:"priority"`
|
|
}
|
|
|
|
type ChannelPriorityOrder struct {
|
|
Revision string `json:"revision"`
|
|
Items []ChannelPriority `json:"items"`
|
|
Changes []ChannelPriorityChange `json:"changes"`
|
|
}
|
|
|
|
type ChannelPriorityMove struct {
|
|
Revision string `json:"revision"`
|
|
ID int `json:"id"`
|
|
TargetID int `json:"target_id"`
|
|
Position string `json:"position"` // before / after use descending priority order
|
|
Preview bool `json:"preview"`
|
|
Restore []ChannelPriorityChange `json:"restore,omitempty"`
|
|
}
|
|
|
|
func channelPriorityRevision(items []ChannelPriority) string {
|
|
data, _ := common.Marshal(items)
|
|
return fmt.Sprintf("%x", sha256.Sum256(data))
|
|
}
|
|
|
|
func GetChannelPriorityOrder() (ChannelPriorityOrder, error) {
|
|
result := ChannelPriorityOrder{Items: []ChannelPriority{}, Changes: []ChannelPriorityChange{}}
|
|
err := DB.Model(&Channel{}).Select("id", "priority").Order("id").Find(&result.Items).Error
|
|
result.Revision = channelPriorityRevision(result.Items)
|
|
return result, err
|
|
}
|
|
|
|
// ReorderChannelPriority updates only priorities, including the routing copy in
|
|
// abilities. A snapshot check also protects preview/commit and undo from stale
|
|
// clients; row locks serialize concurrent reorders on MySQL and PostgreSQL.
|
|
func ReorderChannelPriority(move ChannelPriorityMove) (ChannelPriorityOrder, error) {
|
|
result := ChannelPriorityOrder{Items: []ChannelPriority{}, Changes: []ChannelPriorityChange{}}
|
|
err := DB.Transaction(func(tx *gorm.DB) error {
|
|
query := tx.Model(&Channel{}).Select("id", "priority").Order("id")
|
|
if !move.Preview {
|
|
query = lockForUpdate(query)
|
|
}
|
|
if err := query.Find(&result.Items).Error; err != nil {
|
|
return err
|
|
}
|
|
if move.Revision == "" || move.Revision != channelPriorityRevision(result.Items) {
|
|
return ErrChannelPriorityConflict
|
|
}
|
|
changes, err := planChannelPriorityMove(result.Items, move)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.Changes = changes
|
|
byID := make(map[int]*ChannelPriority, len(result.Items))
|
|
for i := range result.Items {
|
|
byID[result.Items[i].ID] = &result.Items[i]
|
|
}
|
|
for _, change := range changes {
|
|
byID[change.ID].Priority = change.Priority
|
|
if move.Preview {
|
|
continue
|
|
}
|
|
if err := tx.Model(&Channel{}).Where("id = ?", change.ID).Update("priority", change.Priority).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&Ability{}).Where("channel_id = ?", change.ID).Update("priority", change.Priority).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
result.Revision = channelPriorityRevision(result.Items)
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
// Equal-priority channels remain together. Opening a slot shifts the shorter
|
|
// contiguous run of occupied integer priorities, measured by affected channels.
|
|
func planChannelPriorityMove(items []ChannelPriority, move ChannelPriorityMove) ([]ChannelPriorityChange, error) {
|
|
changes := []ChannelPriorityChange{}
|
|
byID := make(map[int]ChannelPriority, len(items))
|
|
for _, item := range items {
|
|
if item.value() < -maxSortablePriority || item.value() > maxSortablePriority {
|
|
return nil, ErrChannelPriorityRange
|
|
}
|
|
byID[item.ID] = item
|
|
}
|
|
if len(move.Restore) > 0 {
|
|
if move.ID != 0 || move.TargetID != 0 || move.Position != "" {
|
|
return nil, ErrChannelPriorityInvalid
|
|
}
|
|
seen := make(map[int]bool, len(move.Restore))
|
|
for _, change := range move.Restore {
|
|
item, ok := byID[change.ID]
|
|
if !ok || seen[change.ID] || (change.Priority != nil && (*change.Priority < -maxSortablePriority || *change.Priority > maxSortablePriority)) {
|
|
return nil, ErrChannelPriorityInvalid
|
|
}
|
|
if !equalPriority(item.Priority, change.Previous) {
|
|
return nil, ErrChannelPriorityConflict
|
|
}
|
|
seen[change.ID] = true
|
|
if !equalPriority(item.Priority, change.Priority) {
|
|
changes = append(changes, change)
|
|
}
|
|
}
|
|
return changes, nil
|
|
}
|
|
item, ok := byID[move.ID]
|
|
target, targetOK := byID[move.TargetID]
|
|
if !ok || !targetOK || move.ID == move.TargetID || !slices.Contains([]string{"before", "after", "same"}, move.Position) {
|
|
return nil, ErrChannelPriorityInvalid
|
|
}
|
|
values := make(map[int]int64, len(items))
|
|
for _, entry := range items {
|
|
values[entry.ID] = entry.value()
|
|
}
|
|
if move.Position == "same" {
|
|
values[item.ID] = target.value()
|
|
} else {
|
|
counts := make(map[int64]int)
|
|
for _, entry := range items {
|
|
if entry.ID != item.ID {
|
|
counts[entry.value()]++
|
|
}
|
|
}
|
|
levels := make([]int64, 0, len(counts))
|
|
for level := range counts {
|
|
levels = append(levels, level)
|
|
}
|
|
slices.SortFunc(levels, func(a, b int64) int {
|
|
if a > b {
|
|
return -1
|
|
}
|
|
if a < b {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
index := slices.Index(levels, target.value())
|
|
if move.Position == "after" {
|
|
index++
|
|
}
|
|
hi, lo := maxSortablePriority+1, -maxSortablePriority-1
|
|
if index > 0 {
|
|
hi = levels[index-1]
|
|
}
|
|
if index < len(levels) {
|
|
lo = levels[index]
|
|
}
|
|
// Dropping into the channel's existing slot must not renumber it.
|
|
if item.value() < hi && item.value() > lo {
|
|
return changes, nil
|
|
}
|
|
if hi-lo < 2 {
|
|
up, down := index-1, index
|
|
upCost, downCost := 0, 0
|
|
for up >= 0 {
|
|
upCost += counts[levels[up]]
|
|
if (up == 0 && levels[up] < maxSortablePriority) || (up > 0 && levels[up-1]-levels[up] > 1) {
|
|
break
|
|
}
|
|
up--
|
|
}
|
|
for down < len(levels) {
|
|
downCost += counts[levels[down]]
|
|
if (down == len(levels)-1 && levels[down] > -maxSortablePriority) || (down+1 < len(levels) && levels[down]-levels[down+1] > 1) {
|
|
break
|
|
}
|
|
down++
|
|
}
|
|
canUp, canDown := up >= 0 && index > 0, down < len(levels) && index < len(levels)
|
|
if !canUp && !canDown {
|
|
return nil, ErrChannelPriorityRange
|
|
}
|
|
if canUp && (!canDown || upCost < downCost) {
|
|
for id, value := range values {
|
|
if id != item.ID && value >= levels[index-1] && value <= levels[up] {
|
|
values[id]++
|
|
}
|
|
}
|
|
hi++
|
|
} else {
|
|
for id, value := range values {
|
|
if id != item.ID && value <= levels[index] && value >= levels[down] {
|
|
values[id]--
|
|
}
|
|
}
|
|
lo--
|
|
}
|
|
}
|
|
value := lo + (hi-lo)/2
|
|
if index == 0 {
|
|
value = min(maxSortablePriority, lo+10)
|
|
} else if index == len(levels) {
|
|
value = max(-maxSortablePriority, hi-10)
|
|
}
|
|
values[item.ID] = value
|
|
}
|
|
for _, entry := range items {
|
|
value := values[entry.ID]
|
|
if value != entry.value() {
|
|
changes = append(changes, ChannelPriorityChange{ID: entry.ID, Previous: entry.Priority, Priority: &value})
|
|
}
|
|
}
|
|
return changes, nil
|
|
}
|
|
|
|
func equalPriority(a, b *int64) bool {
|
|
if a == nil || b == nil {
|
|
return a == nil && b == nil
|
|
}
|
|
return *a == *b
|
|
}
|