mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-27 20:53:44 +00:00
feat(ui): 服务器工具栏新增删除按钮
- 工具栏次序:新建服务器、新建分组、删除、分享;删除钮作用于当前 选中节点,确认弹窗与右键菜单共用 deleteTarget 流(档位见 deleteFacts)。 - 无选中、选择失效或选中项不可删除时禁用;资格判定新增政策缝 CatalogWorkflow.canDelete(节点存在且 manual),订阅节点结构只读 不经此口径,订阅删除仍留在订阅分区。 - CONTEXT.md Vocabulary 增补 Deletable node 术语;新增 canDelete 接口测试。
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
- **Manual group**: A configuration group owned by the user. It may contain only manually owned nodes; deleting it recursively deletes its descendants. Deletion is always user-confirmed: one confirmation for an empty manual group, and a second confirmation naming the recursive subtree scale when it is non-empty.
|
||||
- **Subscription**: A remote configuration source with a persistent opaque UUID and a fixed subscription-group identity. Its URL is a mutable endpoint/credential reference; editing the URL keeps the source identity, while deleting and creating a subscription establishes a new source identity.
|
||||
- **Subscription group**: The fixed, source-owned configuration subtree provided by one subscription. Users cannot move or structurally edit its nodes. A subscription server's user-visible name is one remote-derived value from `remarks`, falling back to its address and port when absent, empty, or whitespace-only; each refresh derives that name from the latest remote record. It has no local alias or note override.
|
||||
- **Deletable node**: A manually owned node — a server configuration or a manual group — that the user can delete from the catalog. Subscription groups and their remote members are structurally read-only and are not deletable nodes; removing a subscription is subscription management, distinct from node deletion. _Avoid_: removable node.
|
||||
- **Active target**: The persisted server configuration or configuration group selected for proxying. When a group is active, the group UUID remains the target even when `sslocal` chooses an individual descendant server.
|
||||
- **Activation candidate**: A server configuration with no known local blocking problem for proxy configuration generation. This status does not guarantee remote reachability or correctness of opaque plugin options.
|
||||
- **Managed plugin**: A SIP003 plugin executable that the product packages, hash-verifies, re-signs, and distributes only inside the app bundle. The managed set changes only with app releases; users cannot supply their own plugin executables.
|
||||
|
||||
@@ -6,6 +6,14 @@ import Foundation
|
||||
extension CatalogWorkflow {
|
||||
// MARK: - 删除确认事实
|
||||
|
||||
/// 删除资格(命令入口的统一 disable 判据,如分区工具栏删除钮):节点存在
|
||||
/// 且来源为 manual。订阅节点结构只读(CONTEXT.md),其移除属订阅分区流程,
|
||||
/// 不经此口径。
|
||||
func canDelete(_ id: NodeID) -> Bool {
|
||||
guard let node = tree.node(withID: id) else { return false }
|
||||
return node.isManual
|
||||
}
|
||||
|
||||
/// 删除确认档位(CONTEXT.md:空手动组/服务器单次;非空手动组二次并点名规模)。
|
||||
/// 节点不存在时为 `nil`。
|
||||
func deleteFacts(for id: NodeID) -> DeleteConfirmKind? {
|
||||
|
||||
@@ -265,6 +265,19 @@ extension ServersView {
|
||||
.labelStyle(.iconOnly)
|
||||
.help("新建分组")
|
||||
}
|
||||
// 删除:作用于当前选中的服务器叶子或手动分组,确认弹窗与右键菜单共用
|
||||
// deleteTarget 流(档位见 deleteFacts);无选中或选中项不可删(订阅节点
|
||||
// 结构只读,CONTEXT.md)即禁用,资格判定归政策 seam。
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
deleteTarget = selection
|
||||
} label: {
|
||||
Label("删除", systemImage: "trash")
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.help("删除")
|
||||
.disabled(!canDeleteSelection)
|
||||
}
|
||||
// 分享(分享/导出术语见 CONTEXT.md):作用于选中的服务器叶子,弹窗内
|
||||
// 提供二维码与复制 ss:// 链接两条通路;无有效载荷即不可用。
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
@@ -288,6 +301,11 @@ extension ServersView {
|
||||
|
||||
// MARK: - 告警动作
|
||||
|
||||
/// 工具栏删除钮可用性:无选中或选中项不可删(订阅节点、失效选择)即禁用。
|
||||
private var canDeleteSelection: Bool {
|
||||
selection.map(workflow.canDelete) ?? false
|
||||
}
|
||||
|
||||
private func presentNewGroup(in parent: NodeID?) {
|
||||
newGroupParent = parent
|
||||
newGroupName = ""
|
||||
|
||||
@@ -79,6 +79,22 @@ final class CatalogWorkflowPolicyTests: XCTestCase {
|
||||
.subtree(count: 3, includesCredentials: true))
|
||||
}
|
||||
|
||||
// MARK: - 删除资格
|
||||
|
||||
func testCanDeleteManualLeafAndGroupButNotSubscriptionOrMissing() async throws {
|
||||
let serverID = try await importServer()
|
||||
let groupID = try await workflow.createGroup(named: "组", into: nil)
|
||||
XCTAssertTrue(workflow.canDelete(serverID), "手动叶子可删")
|
||||
XCTAssertTrue(workflow.canDelete(groupID), "手动分组可删(空组同理)")
|
||||
|
||||
let fixture = try CatalogFixtures.makeSubscriptionFixture()
|
||||
try CatalogFileStore(fileURL: fileURL).save(CatalogDocument(catalog: fixture.catalog))
|
||||
workflow = makeWorkflow()
|
||||
XCTAssertFalse(workflow.canDelete(fixture.serverIDs[0]), "订阅成员结构只读")
|
||||
XCTAssertFalse(workflow.canDelete(fixture.groupID), "订阅分组结构只读")
|
||||
XCTAssertFalse(workflow.canDelete(NodeID(rawValue: "ghost")), "节点不存在")
|
||||
}
|
||||
|
||||
// MARK: - 移动目的地
|
||||
|
||||
func testMoveDestinationsAreRootAndManualGroupsExcludingOwnSubtree() async throws {
|
||||
|
||||
Reference in New Issue
Block a user