refactor(servers): remove list sorting

This commit is contained in:
Qiu Yuzhou
2026-10-15 00:45:52 +08:00
parent a8fedace16
commit ea064a5c8a
6 changed files with 40 additions and 160 deletions
@@ -121,7 +121,7 @@ struct CatalogTreeSnapshot: Equatable {
extension CatalogTreeSnapshot {
/// 折叠投影后的可见行(深度优先):收起分组的子树不出现,其余保持目录
/// 序。行序与深度是纯树事实;折叠集合由调用方按自己的持久化策略持有。
func visibleRows(collapsed: Set<NodeID>) -> [CatalogTreeRow] {
func visibleRows(source: NodeSource? = nil, collapsed: Set<NodeID>) -> [CatalogTreeRow] {
var rows: [CatalogTreeRow] = []
func walk(_ nodes: [CatalogTreeNode], depth: Int) {
for node in nodes {
@@ -131,7 +131,8 @@ extension CatalogTreeSnapshot {
}
}
}
walk(roots, depth: 0)
// 来源不跨越子树,根层筛选保留完整层级与目录顺序。
walk(source.map { source in roots.filter { $0.source == source } } ?? roots, depth: 0)
return rows
}
@@ -1178,28 +1178,6 @@
}
}
},
"创建时间": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Date Created"
}
}
}
},
"修改时间": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Date Modified"
}
}
}
},
"无": {
"extractionState": "manual",
"localizations": {
@@ -1211,17 +1189,6 @@
}
}
},
"排序": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sort"
}
}
}
},
"收藏": {
"extractionState": "manual",
"localizations": {
@@ -1,71 +0,0 @@
import Foundation
/// 展示排序不改写目录顺序;未知时间在两个方向都排在最后。
struct ServerTableSort: SortComparator {
enum Field: CaseIterable {
case name, createdAt, updatedAt
}
let field: Field
var order: SortOrder
init(_ field: Field, order: SortOrder = .forward) {
self.field = field
self.order = order
}
func compare(_ lhs: CatalogTreeNode, _ rhs: CatalogTreeNode) -> ComparisonResult {
let result: ComparisonResult
switch field {
case .name:
result = lhs.name.localizedStandardCompare(rhs.name)
case .createdAt, .updatedAt:
let left = field == .createdAt ? lhs.createdAt : lhs.updatedAt
let right = field == .createdAt ? rhs.createdAt : rhs.updatedAt
switch (left, right) {
case (nil, nil): return .orderedSame
case (nil, _): return .orderedDescending
case (_, nil): return .orderedAscending
case (.some(let left), .some(let right)): result = left.compare(right)
}
}
guard order == .reverse else { return result }
switch result {
case .orderedAscending: return .orderedDescending
case .orderedDescending: return .orderedAscending
case .orderedSame: return .orderedSame
}
}
static func sorted(_ nodes: [CatalogTreeNode], by sortOrder: [Self]) -> [CatalogTreeNode] {
guard let comparator = sortOrder.first else { return nodes }
return nodes.enumerated().sorted { lhs, rhs in
let result = comparator.compare(lhs.element, rhs.element)
return result == .orderedSame ? lhs.offset < rhs.offset : result == .orderedAscending
}.map(\.element)
}
}
extension CatalogTreeSnapshot {
/// 来源不跨越子树,因此根层筛选保留完整层级。
func roots(for source: NodeSource, sortedBy sortOrder: [ServerTableSort]) -> [CatalogTreeNode] {
ServerTableSort.sorted(roots.filter { $0.source == source }, by: sortOrder)
}
/// 每层单独排序,再按展开状态生成 Table 行;不改变子树归属。
func visibleRows(
source: NodeSource, sortedBy sortOrder: [ServerTableSort], collapsed: Set<NodeID>
) -> [CatalogTreeRow] {
var rows: [CatalogTreeRow] = []
func append(_ nodes: [CatalogTreeNode], depth: Int) {
for node in ServerTableSort.sorted(nodes, by: sortOrder) {
rows.append(CatalogTreeRow(node: node, depth: depth))
if !collapsed.contains(node.id) {
append(node.childNodes, depth: depth + 1)
}
}
}
append(roots.filter { $0.source == source }, depth: 0)
return rows
}
}
@@ -13,7 +13,6 @@ struct ServersView: View {
let activeTargetID: NodeID?
@Binding var selection: NodeID?
@Binding var section: ServerListSection
@Binding var sortOrder: [ServerTableSort]
/// 服务器管理的展开状态跨工作区页面切换保留,不落盘。
@ObservedObject var expansion: CatalogExpansionState
let clipboard: any TextClipboard
@@ -132,14 +131,13 @@ struct ServersView: View {
private var serverTable: some View {
let rows = workflow.tree.visibleRows(
source: section.source ?? .manual, sortedBy: sortOrder, collapsed: expansion.collapsedGroupIDs
source: section.source ?? .manual, collapsed: expansion.collapsedGroupIDs
)
let depths = Dictionary(uniqueKeysWithValues: rows.map { ($0.id, $0.depth) })
return Table(
of: CatalogTreeNode.self, selection: $selection,
sortOrder: Binding(get: { sortOrder }, set: { sortOrder = Array($0.prefix(1)) })
of: CatalogTreeNode.self, selection: $selection
) {
TableColumn("名称", sortUsing: ServerTableSort(.name)) { node in
TableColumn("名称") { node in
HStack(spacing: 4) {
if node.isGroup && !node.childNodes.isEmpty {
Button {
@@ -309,28 +307,6 @@ extension ServersView {
module: module, targets: workflow.connectivityTargets(source: section.source))
}
}
if section != .favorites {
ToolbarItem(placement: .primaryAction) {
Menu {
Picker(
"排序",
selection: Binding<ServerTableSort.Field?>(
get: { sortOrder.first?.field },
set: { field in sortOrder = field.map { [ServerTableSort($0)] } ?? [] })
) {
Text("无").tag(nil as ServerTableSort.Field?)
Text("名称").tag(ServerTableSort.Field.name as ServerTableSort.Field?)
Text("创建时间").tag(ServerTableSort.Field.createdAt as ServerTableSort.Field?)
Text("修改时间").tag(ServerTableSort.Field.updatedAt as ServerTableSort.Field?)
}
.pickerStyle(.inline)
} label: {
Label("排序", systemImage: "arrow.up.arrow.down")
}
.labelStyle(.iconOnly)
.help("排序")
}
}
if section == .manual {
ToolbarItem(placement: .primaryAction) {
Button {
@@ -38,7 +38,6 @@ struct MainWindowView: View {
@State private var tabSelection: WorkspaceDestination = WorkspaceRoute.initialDestination
@State private var selection: NodeID?
@State private var serverSection: ServerListSection = .manual
@State private var serverSortOrder: [ServerTableSort] = []
/// 全局导入面板由窗口壳持有。
@State private var presentedWorkspaceSheet: WorkspaceSheet?
@@ -132,7 +131,6 @@ struct MainWindowView: View {
activeTargetID: control.snapshot.activeTarget?.id,
selection: $selection,
section: $serverSection,
sortOrder: $serverSortOrder,
expansion: expansion,
clipboard: clipboard,
imageClipboard: imageClipboard,
@@ -114,9 +114,9 @@ struct SubscriptionRootGroupClassificationTests {
}
}
struct ServerTableOrderingTests {
struct ServerTableDirectoryOrderTests {
@Test
func hierarchySortsEachSiblingSetAndCollapseHidesOnlyThatSubtree() throws {
func hierarchyKeepsDirectoryOrderAndCollapseHidesOnlyThatSubtree() throws {
var catalog = ConfigurationCatalog()
let outer = try catalog.addGroup("Z 外层")
let root = try catalog.addGroup("A 根层")
@@ -125,42 +125,51 @@ struct ServerTableOrderingTests {
let leaf = try catalog.addGroup("孙节点", to: childA)
let tree = CatalogTreeSnapshot.build(
from: catalog, credentials: InMemoryCredentialStore(), plugins: NoManagedPluginProvider())
let rows = tree.visibleRows(source: .manual, sortedBy: [ServerTableSort(.name)], collapsed: [])
let rows = tree.visibleRows(source: .manual, collapsed: [])
#expect(rows.map(\.id) == [root, outer, childA, leaf, childZ])
#expect(rows.map(\.depth) == [0, 0, 1, 2, 1])
#expect(rows.map(\.id) == [outer, childZ, childA, leaf, root])
#expect(rows.map(\.depth) == [0, 1, 1, 2, 0])
#expect(
tree.visibleRows(
source: .manual, sortedBy: [ServerTableSort(.name)], collapsed: [outer]
).map(\.id) == [root, outer])
source: .manual, collapsed: [outer]
).map(\.id) == [outer, root])
#expect(tree.roots.map(\.id) == [outer, root])
#expect(tree.node(withID: outer)?.childNodes.map(\.id) == [childZ, childA])
}
@Test(arguments: [SortOrder.forward, .reverse])
func sourceFilteringAndTimeSortingKeepUnknownLastAndEqualValuesStable(order: SortOrder) {
func node(_ id: String, source: NodeSource = .manual, date: Date?) -> CatalogTreeNode {
@Test
func sourceFilteringKeepsDirectoryOrderAndCompleteSubtrees() {
func node(
_ id: String, source: NodeSource = .manual, parentID: NodeID? = nil,
children: [CatalogTreeNode] = []
) -> CatalogTreeNode {
CatalogTreeNode(
id: NodeID(rawValue: id), name: id, isGroup: true, source: source, parentID: nil,
createdAt: date, updatedAt: date, invalidReasons: [], childCount: 0,
subtreeNodeCount: 0, invalidDescendantCount: 0, children: [])
id: NodeID(rawValue: id), name: id, isGroup: true, source: source, parentID: parentID,
createdAt: nil, updatedAt: nil, invalidReasons: [], childCount: children.count,
subtreeNodeCount: children.count, invalidDescendantCount: 0, children: children)
}
let subscription = node(
"subscription", source: .subscription,
children: [
node(
"subscription-child", source: .subscription,
parentID: NodeID(rawValue: "subscription"))
])
let tree = CatalogTreeSnapshot(roots: [
node("unknown", date: nil),
node("newer", date: Date(timeIntervalSince1970: 200)),
node("equal-first", date: Date(timeIntervalSince1970: 100)),
node("equal-second", date: Date(timeIntervalSince1970: 100)),
node("subscription", source: .subscription, date: Date(timeIntervalSince1970: 50)),
node("Z-first"), subscription, node("A-last"),
])
let sorted = tree.roots(for: .manual, sortedBy: [ServerTableSort(.createdAt, order: order)])
#expect(
sorted.map(\.id.rawValue)
== (order == .forward
? ["equal-first", "equal-second", "newer", "unknown"]
: ["newer", "equal-first", "equal-second", "unknown"]))
tree.visibleRows(source: .manual, collapsed: []).map(\.id.rawValue)
== ["Z-first", "A-last"])
let subscriptionRows = tree.visibleRows(source: .subscription, collapsed: [])
#expect(subscriptionRows.map(\.id.rawValue) == ["subscription", "subscription-child"])
#expect(subscriptionRows.map(\.depth) == [0, 1])
#expect(
tree.roots(for: .manual, sortedBy: []).map(\.id.rawValue)
== ["unknown", "newer", "equal-first", "equal-second"])
tree.visibleRows(source: .subscription, collapsed: [subscription.id]).map(\.id.rawValue)
== ["subscription"])
#expect(
tree.visibleRows(collapsed: []).map(\.id.rawValue)
== ["Z-first", "subscription", "subscription-child", "A-last"])
}
}