Close, reopen, and delete issues on iPhone
This commit is contained in:
11
TESTING.md
11
TESTING.md
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 can’t 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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user