| @@ -7,6 +7,34 @@ struct AdminReposView: View { |
| 7 | @State private var model: AdminReposViewModel |
7 | @State private var model: AdminReposViewModel |
| 8 | @State private var deleting: AdminRepo? |
8 | @State private var deleting: AdminRepo? |
| 9 | @State private var typed = "" |
9 | @State private var typed = "" |
| |
10 | /// Archive and visibility change what everyone sees, so they ask first. |
| |
11 | @State private var confirming: Pending? |
| |
12 | |
| |
13 | nonisolated struct Pending: Hashable, Sendable, Identifiable { |
| |
14 | let action: AdminRepoAction |
| |
15 | let repo: AdminRepo |
| |
16 | var id: String { repo.path + String(describing: action) } |
| |
17 | |
| |
18 | var title: String { |
| |
19 | switch action { |
| |
20 | case .archive: "Archive \(repo.path)?" |
| |
21 | case .unarchive: "Unarchive \(repo.path)?" |
| |
22 | case .makePublic: "Make \(repo.path) public?" |
| |
23 | case .makePrivate: "Make \(repo.path) private?" |
| |
24 | default: "" |
| |
25 | } |
| |
26 | } |
| |
27 | |
| |
28 | var message: String { |
| |
29 | switch action { |
| |
30 | case .archive: "It becomes read-only: pushes, issues and merge requests are refused. Audited." |
| |
31 | case .unarchive: "It takes pushes, issues and merge requests again. Audited." |
| |
32 | case .makePublic: "Anyone, signed in or not, can read it. Audited." |
| |
33 | case .makePrivate: "Only its owner and those granted access can read it. Audited." |
| |
34 | default: "" |
| |
35 | } |
| |
36 | } |
| |
37 | } |
| 10 | |
38 | |
| 11 | init(client: GitbayClient) { |
39 | init(client: GitbayClient) { |
| 12 | _model = State(initialValue: AdminReposViewModel(client: client)) |
40 | _model = State(initialValue: AdminReposViewModel(client: client)) |
| @@ -50,6 +78,17 @@ struct AdminReposView: View { |
| 50 | .task { if model.list.needsLoad { await model.load() } } |
78 | .task { if model.list.needsLoad { await model.load() } } |
| 51 | .refreshable { await model.load() } |
79 | .refreshable { await model.load() } |
| 52 | .onChange(of: model.visibility) { Task { await model.load() } } |
80 | .onChange(of: model.visibility) { Task { await model.load() } } |
| |
81 | .confirmationDialog(confirming?.title ?? "", |
| |
82 | isPresented: Binding(get: { confirming != nil }, set: { if !$0 { confirming = nil } }), |
| |
83 | titleVisibility: .visible, presenting: confirming) { pending in |
| |
84 | Button(pending.action == .makePublic ? "Make public" : "Confirm", |
| |
85 | role: pending.action == .makePublic || pending.action == .archive ? .destructive : nil) { |
| |
86 | Task { await model.perform(pending.action, on: pending.repo) } |
| |
87 | } |
| |
88 | Button("Cancel", role: .cancel) {} |
| |
89 | } message: { pending in |
| |
90 | Text(pending.message) |
| |
91 | } |
| 53 | .alert("Delete \(deleting?.path ?? "")?", isPresented: Binding(get: { deleting != nil }, set: { if !$0 { deleting = nil } }), |
92 | .alert("Delete \(deleting?.path ?? "")?", isPresented: Binding(get: { deleting != nil }, set: { if !$0 { deleting = nil } }), |
| 54 | presenting: deleting) { repo in |
93 | presenting: deleting) { repo in |
| 55 | TextField(repo.path, text: $typed) |
94 | TextField(repo.path, text: $typed) |
| @@ -82,10 +121,10 @@ struct AdminReposView: View { |
| 82 | Spacer() |
121 | Spacer() |
| 83 | Menu { |
122 | Menu { |
| 84 | Button(repo.isArchived ? "Unarchive" : "Archive") { |
123 | Button(repo.isArchived ? "Unarchive" : "Archive") { |
| 85 | Task { await model.perform(repo.isArchived ? .unarchive : .archive, on: repo) } |
124 | confirming = Pending(action: repo.isArchived ? .unarchive : .archive, repo: repo) |
| 86 | } |
125 | } |
| 87 | Button(repo.visibility == "private" ? "Make public" : "Make private") { |
126 | Button(repo.visibility == "private" ? "Make public" : "Make private") { |
| 88 | Task { await model.perform(repo.visibility == "private" ? .makePublic : .makePrivate, on: repo) } |
127 | confirming = Pending(action: repo.visibility == "private" ? .makePublic : .makePrivate, repo: repo) |
| 89 | } |
128 | } |
| 90 | Button("Rebuild symbol index") { |
129 | Button("Rebuild symbol index") { |
| 91 | Task { await model.perform(.reindex, on: repo) } |
130 | Task { await model.perform(.reindex, on: repo) } |
| @@ -126,10 +165,14 @@ struct AdminOrgLimitsView: View { |
| 126 | .multilineTextAlignment(.trailing) |
165 | .multilineTextAlignment(.trailing) |
| 127 | .autocorrectionDisabled() |
166 | .autocorrectionDisabled() |
| 128 | .textInputAutocapitalization(.never) |
167 | .textInputAutocapitalization(.never) |
| |
168 | .submitLabel(.search) |
| 129 | .onSubmit { Task { await model.load(org: org) } } |
169 | .onSubmit { Task { await model.load(org: org) } } |
| 130 | .accessibilityIdentifier("org-limits-name") |
170 | .accessibilityIdentifier("org-limits-name") |
| 131 | } |
171 | } |
| 132 | .font(.gbSans(.subheadline)) |
172 | .font(.gbSans(.subheadline)) |
| |
173 | Button("Show") { Task { await model.load(org: org) } } |
| |
174 | .disabled(org.trimmingCharacters(in: .whitespaces).isEmpty || model.working) |
| |
175 | .accessibilityIdentifier("org-limits-show") |
| 133 | } |
176 | } |
| 134 | if let error = model.actionError, !editing { |
177 | if let error = model.actionError, !editing { |
| 135 | Section { GBNotice(error, .gbWarn) } |
178 | Section { GBNotice(error, .gbWarn) } |