diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb11db5..8a6b341 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,9 +132,12 @@ jobs: uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} with: releaseId: ${{ needs.prepare-release.outputs.release_id }} args: ${{ matrix.args }} + updaterJsonPreferNsis: false finalize: needs: [prepare-release, build] @@ -150,6 +153,13 @@ jobs: set -e gh api "repos/$REPO/releases/$RELEASE_ID/assets" --jq '.[] | "\(.id)\t\(.name)"' \ | while IFS=$'\t' read -r id name; do + # Skip updater bundles + signatures + manifest — latest.json points + # at versioned URLs and renaming would 404 the auto-update. + case "$name" in + latest.json|*.tar.gz|*.tar.gz.sig|*.sig|*.zip|*.zip.sig) + continue + ;; + esac new=$(printf '%s' "$name" | sed -E 's/([_-])[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?[._-]?/\1/') if [ "$new" != "$name" ]; then echo "Renaming: $name -> $new" diff --git a/bun.lock b/bun.lock index 3ec914c..e073817 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,8 @@ "@fontsource-variable/geist": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -421,6 +423,10 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], + "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="], + + "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.1", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], diff --git a/package.json b/package.json index 061e933..b55e0a2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@fontsource-variable/geist": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 93339dd..4e210d6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -87,6 +87,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -819,6 +828,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1132,6 +1152,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2203,7 +2234,10 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ + "bitflags 2.11.1", "libc", + "plain", + "redox_syscall 0.7.4", ] [[package]] @@ -2334,6 +2368,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2564,6 +2604,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2619,6 +2671,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2635,6 +2693,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2684,7 +2756,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2911,6 +2983,12 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.9.0" @@ -3005,6 +3083,8 @@ dependencies = [ "tauri-build", "tauri-plugin-log", "tauri-plugin-notification", + "tauri-plugin-process", + "tauri-plugin-updater", "thiserror 2.0.18", "tokio", ] @@ -3322,6 +3402,15 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.1", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3443,15 +3532,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3579,6 +3673,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -3589,6 +3695,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -3621,6 +3754,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3684,6 +3826,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3998,7 +4163,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -4219,6 +4384,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4397,6 +4573,49 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.13.2", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.10.1" @@ -5321,6 +5540,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.7" @@ -6008,6 +6236,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.2" @@ -6172,6 +6410,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 347a5ef..82cd3a5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,8 @@ log = "0.4" tauri = { version = "2.10.3", features = ["tray-icon"] } tauri-plugin-log = "2" tauri-plugin-notification = "2" +tauri-plugin-updater = "2" +tauri-plugin-process = "2" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "gzip"] } thiserror = "2" open = "5" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 193ac35..2fa735d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,9 @@ ], "permissions": [ "core:default", - "notification:default" + "notification:default", + "updater:default", + "process:default", + "process:allow-restart" ] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 0be7aec..592e53f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -478,6 +478,7 @@ pub struct PrDetails { pub checks: Vec, pub pending_review_id: Option, pub pending_review_threads_count: i64, + pub viewer_has_approved: bool, } const PR_DETAILS_QUERY: &str = r#" @@ -894,14 +895,27 @@ pub async fn get_pr_details( } let viewer_login = data.viewer.login.clone(); let mut pending_review_id: Option = None; + let mut viewer_latest_review: Option<(String, String)> = None; for r in pr.reviews.nodes { - let is_viewer_pending = r.state == "PENDING" - && r.author.as_ref().map(|a| a.login == viewer_login).unwrap_or(false); - if is_viewer_pending { + let is_viewer = r + .author + .as_ref() + .map(|a| a.login == viewer_login) + .unwrap_or(false); + if is_viewer && r.state == "PENDING" { pending_review_id = Some(r.id.clone()); continue; } if let Some(submitted_at) = r.submitted_at.clone() { + if is_viewer && r.state != "DISMISSED" { + let is_later = match &viewer_latest_review { + Some((latest_at, _)) => submitted_at > *latest_at, + None => true, + }; + if is_later { + viewer_latest_review = Some((submitted_at.clone(), r.state.clone())); + } + } timeline.push(TimelineEntry::Review { author: r.author.map(user_to_author), body: r.body, @@ -910,6 +924,9 @@ pub async fn get_pr_details( }); } } + let viewer_has_approved = viewer_latest_review + .map(|(_, state)| state == "APPROVED") + .unwrap_or(false); let mut pending_review_threads_count: i64 = 0; for t in pr.review_threads.nodes { let comments: Vec = t @@ -1051,6 +1068,7 @@ pub async fn get_pr_details( checks, pending_review_id, pending_review_threads_count, + viewer_has_approved, }) } @@ -1272,6 +1290,29 @@ pub async fn submit_pr_review( Ok(()) } +const APPROVE_PR_MUTATION: &str = r#" +mutation($input: AddPullRequestReviewInput!) { + addPullRequestReview(input: $input) { + pullRequestReview { id state } + } +} +"#; + +#[tauri::command] +pub async fn approve_pull_request(pr_node_id: String, body: String) -> AppResult<()> { + let token = auth::load_token()?.ok_or(AppError::NotAuthenticated)?; + let client = Client::new(token)?; + let variables = serde_json::json!({ + "input": { + "pullRequestId": pr_node_id, + "event": "APPROVE", + "body": body, + } + }); + let _: serde_json::Value = client.graphql(APPROVE_PR_MUTATION, variables).await?; + Ok(()) +} + #[tauri::command] pub async fn unresolve_review_thread(thread_id: String) -> AppResult<()> { let token = auth::load_token()?.ok_or(AppError::NotAuthenticated)?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 516d49b..8f49d23 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,8 +20,17 @@ pub fn run() { let conn = db::init(); - tauri::Builder::default() - .plugin(tauri_plugin_notification::init()) + let mut builder = tauri::Builder::default() + .plugin(tauri_plugin_notification::init()); + + #[cfg(desktop)] + { + builder = builder + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()); + } + + builder .manage(db::DbState(Mutex::new(conn))) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { @@ -66,6 +75,7 @@ pub fn run() { commands::get_pr_details, commands::get_pr_files, commands::merge_pull_request, + commands::approve_pull_request, commands::add_review_thread_reply, commands::resolve_review_thread, commands::unresolve_review_thread, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0f5c347..a1bf06e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -28,6 +28,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -35,5 +36,13 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/IsraelAraujo70/prism/releases/latest/download/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI2MDczNkJERTdFNzY0QQpSV1JLZG43ZWEzTmdDd0NNT3pLdW80VmxwQ2ovMFRua25kNEtRQ3VkS0FwS0RIMTFCTms5cTZ1aAo=" + } } } diff --git a/src/App.tsx b/src/App.tsx index aa7ceae..a53b2a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { PrViewer } from '@/components/pr-viewer' import { RepoList } from '@/components/repo-list' import { SearchSidebarLink } from '@/components/search-sidebar-link' import { SettingsDialog } from '@/components/settings-dialog' +import { UpdatePill } from '@/components/update-pill' import { UserMenu } from '@/components/user-menu' import { api, @@ -180,6 +181,7 @@ function App() { if (selectedRepo?.id === id) setSelectedRepo(null) }} /> + diff --git a/src/components/pr-viewer.tsx b/src/components/pr-viewer.tsx index 43bcb89..11bddd4 100644 --- a/src/components/pr-viewer.tsx +++ b/src/components/pr-viewer.tsx @@ -137,18 +137,31 @@ export function PrViewer({ pr, onBack }: Props) { > - +
+ {state.status === 'ready' && + resolveStatus(state.data) === 'open' && + !state.data.pending_review_id && + (state.data.viewer_has_approved ? ( + + ) : ( + load(true)} + /> + ))} + +
{state.status === 'ready' && ( @@ -957,6 +970,118 @@ function formatDuration( return `${hr}h ${min % 60}m` } +function ApprovedBadge() { + return ( + + + Aprovado + + ) +} + +function ApproveButton({ + prNodeId, + onApproved, +}: { + prNodeId: string + onApproved: () => Promise +}) { + const [open, setOpen] = useState(false) + const [body, setBody] = useState('') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + const ref = useRef(null) + + useEffect(() => { + if (!open) return + function onClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', onClick) + return () => document.removeEventListener('mousedown', onClick) + }, [open]) + + async function approve() { + if (submitting) return + setSubmitting(true) + setError(null) + try { + await api.approvePullRequest(prNodeId, body) + setBody('') + setOpen(false) + await onApproved() + } catch (e) { + setError(String(e)) + } finally { + setSubmitting(false) + } + } + + return ( +
+ + {open && ( +
+