Close, reopen, and delete issues on iPhone

This commit is contained in:
Georg Bauer
2026-08-01 15:48:14 +02:00
parent d0ec533da6
commit 5a48456333
11 changed files with 269 additions and 10 deletions

View File

@@ -173,7 +173,8 @@ xcrun simctl launch booted de.rfc1437.gotcha
returns every filter icon to neutral.
- [ ] Tap the add button. **New Issue** appears as a native modal with Cancel
and Save, title and Markdown body fields, multi-select labels, a
single-select optional milestone, and an optional inline due-date picker.
single-select optional milestone, a **Closed** switch that defaults off,
and an optional inline due-date picker.
- [ ] In the new-issue body, enter headings, emphasis, a list, a link, and a
fenced code block. **Write** syntax-highlights the Markdown and **Preview**
renders it; switching repeatedly preserves the exact source text.
@@ -182,7 +183,13 @@ xcrun simctl launch booted de.rfc1437.gotcha
due date; each new issue appears in the list and opens its detail.
- [ ] Tap the pencil button on an issue. Change its title and Markdown body, replace and
clear labels, select and clear its milestone, and add, change, and remove
its due date. Save, refresh, and relaunch to verify every change persists.
its due date. Turn **Closed** on and save, then edit it again and turn
**Closed** off. Save, refresh, and relaunch to verify every change persists.
- [ ] In the issue list, partially swipe an open issue to reveal **Close** and
**Delete**. Cancel the destructive Delete confirmation and verify nothing
changes. Full-swipe the same row to close it, switch to Closed issues, and
full-swipe it again to reopen it; each row leaves the current filtered list
only after the server mutation succeeds.
- [ ] Cancel both new and edited issues, including with the keyboard and date
picker visible. Nothing is saved, the modal dismisses normally, and the
issue list/detail remains usable with Dynamic Type and VoiceOver labels.

View File

@@ -304,6 +304,36 @@ pub async fn edit_issue(
.map_err(message)
}
pub async fn set_issue_closed(
server: &Server,
owner: &str,
repository: &str,
number: i64,
closed: bool,
) -> Result<(), String> {
client(server)?
.set_issue_state(
&scope(owner, repository)?,
&[number],
if closed { "closed" } else { "open" },
)
.await
.map(|_| ())
.map_err(message)
}
pub async fn delete_issue(
server: &Server,
owner: &str,
repository: &str,
number: i64,
) -> Result<(), String> {
client(server)?
.delete_issue(&scope(owner, repository)?, number)
.await
.map_err(message)
}
pub async fn load_pull(
server: &Server,
owner: &str,
@@ -382,6 +412,7 @@ fn shared_issue_draft(draft: IssueDraft) -> gotcha_gitea::IssueDraft {
label_ids: draft.label_ids,
milestone_id: draft.milestone_id,
due_date: draft.due_date.map(api_date),
closed: draft.closed,
}
}

View File

@@ -132,6 +132,7 @@ pub struct IssueDraft {
pub label_ids: Vec<i64>,
pub milestone_id: Option<i64>,
pub due_date: Option<i64>,
pub closed: bool,
}
pub struct MilestoneDraft {

View File

@@ -445,6 +445,7 @@ impl GotchaCore {
label_ids: Vec<i64>,
milestone_id: Option<i64>,
due_date: Option<i64>,
closed: bool,
) -> Result<i64, GotchaError> {
let (owner, repository) = validate_repository(&owner, &repository)?;
let title = title.trim();
@@ -468,6 +469,7 @@ impl GotchaCore {
label_ids,
milestone_id,
due_date,
closed,
};
let server = self.server()?;
let issue = match number {
@@ -479,6 +481,35 @@ impl GotchaCore {
.ok_or_else(|| "The saved issue has no number.".into())
}
pub async fn set_issue_closed(
&self,
owner: String,
repository: String,
number: i64,
closed: bool,
) -> Result<(), GotchaError> {
let (owner, repository) = validate_repository(&owner, &repository)?;
if number <= 0 {
return Err("Invalid issue number.".into());
}
set_issue_closed(&self.server()?, owner, repository, number, closed).await?;
Ok(())
}
pub async fn delete_issue(
&self,
owner: String,
repository: String,
number: i64,
) -> Result<(), GotchaError> {
let (owner, repository) = validate_repository(&owner, &repository)?;
if number <= 0 {
return Err("Invalid issue number.".into());
}
delete_issue(&self.server()?, owner, repository, number).await?;
Ok(())
}
pub async fn milestones(
&self,
owner: String,

View File

@@ -103,6 +103,7 @@ pub struct IssueEditorPage {
pub title: String,
pub body: String,
pub due_date: Option<i64>,
pub closed: bool,
pub labels: Vec<IssueEditorLabel>,
pub milestones: Vec<IssueEditorMilestone>,
}
@@ -666,6 +667,7 @@ pub fn issue_editor_page(data: IssueEditorData) -> IssueEditorPage {
.as_ref()
.and_then(|issue| issue.due_date.as_deref())
.and_then(parse_api_date),
closed: data.issue.as_ref().and_then(|issue| issue.state.as_deref()) == Some("closed"),
labels,
milestones,
}
@@ -1513,6 +1515,7 @@ mod tests {
title: Some("Title".into()),
body: Some("Body".into()),
due_date: Some("2024-02-29T18:00:00Z".into()),
state: Some("closed".into()),
labels: Some(vec![selected_label.clone()]),
milestone: Some(Box::new(models::Milestone {
id: Some(4),
@@ -1533,8 +1536,16 @@ mod tests {
assert_eq!((page.title.as_str(), page.body.as_str()), ("Title", "Body"));
assert_eq!(page.due_date, Some(1_709_164_800));
assert!(page.closed);
assert!(page.labels[0].selected);
assert!(page.milestones[0].selected);
let new = issue_editor_page(IssueEditorData {
issue: None,
labels: Vec::new(),
milestones: Vec::new(),
});
assert!(!new.closed);
}
#[test]

View File

@@ -108,6 +108,7 @@ pub struct IssueDraft {
pub label_ids: Vec<i64>,
pub milestone_id: Option<i64>,
pub due_date: Option<String>,
pub closed: bool,
}
#[derive(Clone, Debug)]

View File

@@ -463,6 +463,7 @@ pub fn comment_can_edit(comment: &models::Comment, viewer_id: Option<i64>) -> bo
fn create_issue_option(draft: IssueDraft) -> models::CreateIssueOption {
models::CreateIssueOption {
body: Some(draft.body),
closed: Some(draft.closed),
due_date: draft.due_date,
labels: Some(draft.label_ids),
milestone: draft.milestone_id,
@@ -476,6 +477,7 @@ fn edit_issue_draft(draft: IssueDraft) -> EditIssue {
body: Some(draft.body),
due_date: draft.due_date.clone(),
milestone: Some(draft.milestone_id.unwrap_or_default()),
state: Some(if draft.closed { "closed" } else { "open" }.into()),
title: Some(draft.title),
unset_due_date: draft.due_date.is_none().then_some(true),
..models::EditIssueOption::new()
@@ -552,13 +554,16 @@ mod tests {
label_ids: vec![2, 4],
milestone_id: None,
due_date: None,
closed: true,
};
let create = create_issue_option(draft.clone());
assert_eq!(create.title, "Title");
assert_eq!(create.body.as_deref(), Some("Body"));
assert_eq!(create.closed, Some(true));
assert_eq!(create.labels, Some(vec![2, 4]));
let edit = edit_issue_draft(draft);
assert_eq!(edit.option.milestone, Some(0));
assert_eq!(edit.option.state.as_deref(), Some("closed"));
assert_eq!(edit.option.unset_due_date, Some(true));
assert_eq!(edit.replace_labels, Some(vec![2, 4]));
}

View File

@@ -619,6 +619,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32) async throws -> CommitPage
func deleteIssue(owner: String, repository: String, number: Int64) async throws
func home(page: UInt32, filter: HomeActivityFilter) async throws -> HomePage
func issue(owner: String, repository: String, number: Int64, page: UInt32) async throws -> IssuePage
@@ -653,7 +655,7 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func repositoryFile(owner: String, repository: String, path: String) async throws -> RepositoryFilePage
func saveIssue(owner: String, repository: String, number: Int64?, title: String, body: String, labelIds: [Int64], milestoneId: Int64?, dueDate: Int64?) async throws -> Int64
func saveIssue(owner: String, repository: String, number: Int64?, title: String, body: String, labelIds: [Int64], milestoneId: Int64?, dueDate: Int64?, closed: Bool) async throws -> Int64
func saveIssueComment(owner: String, repository: String, number: Int64, commentId: Int64?, body: String) async throws
@@ -665,6 +667,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func setAppearance(index: UInt32) throws
func setIssueClosed(owner: String, repository: String, number: Int64, closed: Bool) async throws
func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String], searchText: String) throws
func setIssueStatus(status: String) throws
@@ -841,6 +845,22 @@ open func commits(owner: String, repository: String, branch: String?, path: Stri
)
}
open func deleteIssue(owner: String, repository: String, number: Int64)async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_delete_issue(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_void,
completeFunc: ffi_gotcha_core_rust_future_complete_void,
freeFunc: ffi_gotcha_core_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func home(page: UInt32, filter: HomeActivityFilter)async throws -> HomePage {
return
try await uniffiRustCallAsync(
@@ -1101,12 +1121,12 @@ open func repositoryFile(owner: String, repository: String, path: String)async t
)
}
open func saveIssue(owner: String, repository: String, number: Int64?, title: String, body: String, labelIds: [Int64], milestoneId: Int64?, dueDate: Int64?)async throws -> Int64 {
open func saveIssue(owner: String, repository: String, number: Int64?, title: String, body: String, labelIds: [Int64], milestoneId: Int64?, dueDate: Int64?, closed: Bool)async throws -> Int64 {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_save_issue(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionInt64.lower(number),FfiConverterString.lower(title),FfiConverterString.lower(body),FfiConverterSequenceInt64.lower(labelIds),FfiConverterOptionInt64.lower(milestoneId),FfiConverterOptionInt64.lower(dueDate)
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionInt64.lower(number),FfiConverterString.lower(title),FfiConverterString.lower(body),FfiConverterSequenceInt64.lower(labelIds),FfiConverterOptionInt64.lower(milestoneId),FfiConverterOptionInt64.lower(dueDate),FfiConverterBool.lower(closed)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_i64,
@@ -1176,6 +1196,22 @@ open func setAppearance(index: UInt32)throws {try rustCallWithError(FfiConvert
}
}
open func setIssueClosed(owner: String, repository: String, number: Int64, closed: Bool)async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_set_issue_closed(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number),FfiConverterBool.lower(closed)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_void,
completeFunc: ffi_gotcha_core_rust_future_complete_void,
freeFunc: ffi_gotcha_core_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String], searchText: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
uniffiCallStatus in
uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(
@@ -2171,15 +2207,17 @@ public struct IssueEditorPage: Equatable, Hashable {
public var title: String
public var body: String
public var dueDate: Int64?
public var closed: Bool
public var labels: [IssueEditorLabel]
public var milestones: [IssueEditorMilestone]
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(title: String, body: String, dueDate: Int64?, labels: [IssueEditorLabel], milestones: [IssueEditorMilestone]) {
public init(title: String, body: String, dueDate: Int64?, closed: Bool, labels: [IssueEditorLabel], milestones: [IssueEditorMilestone]) {
self.title = title
self.body = body
self.dueDate = dueDate
self.closed = closed
self.labels = labels
self.milestones = milestones
}
@@ -2203,6 +2241,7 @@ public struct FfiConverterTypeIssueEditorPage: FfiConverterRustBuffer {
title: FfiConverterString.read(from: &buf),
body: FfiConverterString.read(from: &buf),
dueDate: FfiConverterOptionInt64.read(from: &buf),
closed: FfiConverterBool.read(from: &buf),
labels: FfiConverterSequenceTypeIssueEditorLabel.read(from: &buf),
milestones: FfiConverterSequenceTypeIssueEditorMilestone.read(from: &buf)
)
@@ -2212,6 +2251,7 @@ public struct FfiConverterTypeIssueEditorPage: FfiConverterRustBuffer {
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.body, into: &buf)
FfiConverterOptionInt64.write(value.dueDate, into: &buf)
FfiConverterBool.write(value.closed, into: &buf)
FfiConverterSequenceTypeIssueEditorLabel.write(value.labels, into: &buf)
FfiConverterSequenceTypeIssueEditorMilestone.write(value.milestones, into: &buf)
}
@@ -4301,6 +4341,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 60062) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_delete_issue() != 9524) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 2797) {
return InitializationResult.apiChecksumMismatch
}
@@ -4352,7 +4395,7 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_file() != 44948) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_save_issue() != 55053) {
if (uniffi_gotcha_core_checksum_method_gotchacore_save_issue() != 3056) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_save_issue_comment() != 58596) {
@@ -4370,6 +4413,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_set_appearance() != 61293) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_closed() != 38814) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_filters() != 24891) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -299,6 +299,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_diff(uint64_t ptr, RustB
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, RustBuffer path, uint32_t pages
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_DELETE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_DELETE_ISSUE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_delete_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr, uint32_t page, RustBuffer filter
@@ -386,7 +391,7 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_repository_file(uint64_t ptr, R
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SAVE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SAVE_ISSUE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_save_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer number, RustBuffer title, RustBuffer body, RustBuffer label_ids, RustBuffer milestone_id, RustBuffer due_date
uint64_t uniffi_gotcha_core_fn_method_gotchacore_save_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer number, RustBuffer title, RustBuffer body, RustBuffer label_ids, RustBuffer milestone_id, RustBuffer due_date, int8_t closed
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SAVE_ISSUE_COMMENT
@@ -414,6 +419,11 @@ RustBuffer uniffi_gotcha_core_fn_method_gotchacore_servers(uint64_t ptr, RustCal
void uniffi_gotcha_core_fn_method_gotchacore_set_appearance(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_CLOSED
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_CLOSED
uint64_t uniffi_gotcha_core_fn_method_gotchacore_set_issue_closed(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, int8_t closed
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_FILTERS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_FILTERS
void uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer milestone, RustBuffer labels, RustBuffer search_text, RustCallStatus *_Nonnull out_status
@@ -755,6 +765,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_diff(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMITS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commits(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_DELETE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_DELETE_ISSUE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_delete_issue(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_HOME
@@ -893,6 +909,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_servers(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_APPEARANCE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_appearance(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_CLOSED
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_CLOSED
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_issue_closed(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_FILTERS

View File

@@ -26,6 +26,7 @@ final class IssuesViewController: RefreshingTableViewController {
private var rows: [IssueRow] = []
private var filterOptions: IssueFilterOptions?
private var filterTask: Task<Void, Never>?
private var mutationTask: Task<Void, Never>?
private var currentPage: UInt32 = 0
private lazy var filterButton = UIBarButtonItem(
image: context.symbol("line.3.horizontal.decrease.circle")
@@ -42,7 +43,10 @@ final class IssuesViewController: RefreshingTableViewController {
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
deinit { filterTask?.cancel() }
deinit {
filterTask?.cancel()
mutationTask?.cancel()
}
override func viewDidLoad() {
super.viewDidLoad()
@@ -143,6 +147,94 @@ final class IssuesViewController: RefreshingTableViewController {
)
}
override func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let row = rows[indexPath.row]
let close = row.state != "closed"
let stateAction = UIContextualAction(
style: .normal,
title: close ? "Close" : "Open"
) { [weak self] _, _, completion in
self?.setIssue(row, closed: close, completion: completion) ?? completion(false)
}
stateAction.image = context.symbol(close ? "checkmark.circle" : "arrow.uturn.left.circle")
stateAction.backgroundColor = close ? .systemPurple : .systemGreen
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") {
[weak self] _, _, completion in
self?.confirmDelete(row, completion: completion) ?? completion(false)
}
deleteAction.image = context.symbol("trash")
let configuration = UISwipeActionsConfiguration(actions: [stateAction, deleteAction])
configuration.performsFirstActionWithFullSwipe = true
return configuration
}
private func setIssue(
_ row: IssueRow,
closed: Bool,
completion: @escaping (Bool) -> Void
) {
mutationTask?.cancel()
mutationTask = Task {
do {
try await context.core.setIssueClosed(
owner: owner,
repository: repository,
number: row.number,
closed: closed
)
guard !Task.isCancelled else {
completion(false)
return
}
completion(true)
loadIssues(refreshing: false)
} catch {
completion(false)
if !Task.isCancelled { show(error: error) }
}
}
}
private func confirmDelete(_ row: IssueRow, completion: @escaping (Bool) -> Void) {
let alert = UIAlertController(
title: "Delete Issue #\(row.number)?",
message: "\(row.title)” will be permanently deleted. This cant be undone.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in completion(false) })
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
guard let self else {
completion(false)
return
}
self.mutationTask?.cancel()
self.mutationTask = Task {
do {
try await self.context.core.deleteIssue(
owner: self.owner,
repository: self.repository,
number: row.number
)
guard !Task.isCancelled else {
completion(false)
return
}
completion(true)
self.loadIssues(refreshing: false)
} catch {
completion(false)
if !Task.isCancelled { self.show(error: error) }
}
}
})
present(alert, animated: true)
}
private func updateFilterMenu() {
let current = context.core.settings().issueStatus
let status = UIMenu(

View File

@@ -17,6 +17,7 @@ final class IssueEditorViewController: UIViewController, UITextFieldDelegate, UI
private let previewView = UIView()
private let labelsButton = UIButton(type: .system)
private let milestoneButton = UIButton(type: .system)
private let closedSwitch = UISwitch()
private let dueSwitch = UISwitch()
private let duePicker = UIDatePicker()
private let spinner = UIActivityIndicatorView(style: .medium)
@@ -103,6 +104,14 @@ final class IssueEditorViewController: UIViewController, UITextFieldDelegate, UI
labelsButton.configuration?.image = context.symbol("tag")
milestoneButton.configuration?.image = context.symbol("flag")
let closedLabel = UILabel()
closedLabel.text = "Closed"
closedLabel.font = .preferredFont(forTextStyle: .body)
closedLabel.adjustsFontForContentSizeCategory = true
let closedRow = UIStackView(arrangedSubviews: [closedLabel, closedSwitch])
closedRow.alignment = .center
closedSwitch.accessibilityLabel = "Closed issue"
let dueLabel = UILabel()
dueLabel.text = "Due Date"
dueLabel.font = .preferredFont(forTextStyle: .body)
@@ -133,6 +142,7 @@ final class IssueEditorViewController: UIViewController, UITextFieldDelegate, UI
stack.addArrangedSubview(titleField)
stack.addArrangedSubview(labelsButton)
stack.addArrangedSubview(milestoneButton)
stack.addArrangedSubview(closedRow)
stack.addArrangedSubview(dueRow)
stack.addArrangedSubview(duePicker)
stack.addArrangedSubview(modeControl)
@@ -164,6 +174,7 @@ final class IssueEditorViewController: UIViewController, UITextFieldDelegate, UI
self.page = page
titleField.text = page.title
bodyView.text = page.body
closedSwitch.isOn = page.closed
selectedLabels = Set(page.labels.filter(\.selected).map(\.id))
selectedMilestone = page.milestones.first(where: \.selected)?.id
if let timestamp = page.dueDate {
@@ -310,7 +321,8 @@ final class IssueEditorViewController: UIViewController, UITextFieldDelegate, UI
body: bodyView.text ?? "",
labelIds: Array(selectedLabels),
milestoneId: selectedMilestone,
dueDate: dueSwitch.isOn ? utcTimestamp(forLocalDate: duePicker.date) : nil
dueDate: dueSwitch.isOn ? utcTimestamp(forLocalDate: duePicker.date) : nil,
closed: closedSwitch.isOn
)
guard !Task.isCancelled else { return }
saved(number)