mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-02 02:00:31 +08:00
Add patch deletion
This commit is contained in:
@@ -85,6 +85,16 @@ pub fn edit_patch(
|
||||
ctx.edit_patch(rid, cob_id, action, opts)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_patch(
|
||||
ctx: tauri::State<AppState>,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<(), Error> {
|
||||
ctx.delete_patch(rid, cob_id, opts)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_patch_review(
|
||||
ctx: tauri::State<AppState>,
|
||||
|
||||
@@ -43,10 +43,10 @@ pub fn run() {
|
||||
cob::issue::rebuild_issue_cache,
|
||||
cob::job::list_jobs,
|
||||
cob::patch::activity_by_patch,
|
||||
cob::patch::delete_patch,
|
||||
cob::patch::edit_patch,
|
||||
cob::patch::list_patches,
|
||||
cob::patch::patch_by_id,
|
||||
cob::patch::edit_patch,
|
||||
cob::patch::create_patch_review,
|
||||
cob::patch::rebuild_patch_cache,
|
||||
cob::patch::review_by_patch_and_revision_and_id,
|
||||
|
||||
@@ -179,6 +179,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Node(#[from] radicle::node::Error),
|
||||
|
||||
/// Cob remove error.
|
||||
#[error(transparent)]
|
||||
CobRemove(#[from] radicle::cob::error::Remove),
|
||||
|
||||
/// Serde JSON error.
|
||||
#[error(transparent)]
|
||||
SerdeJSON(#[from] serde_json::error::Error),
|
||||
|
||||
@@ -332,4 +332,37 @@ pub trait PatchesMut: Profile {
|
||||
&aliases,
|
||||
))
|
||||
}
|
||||
|
||||
/// Remove a patch COB. Equivalent to `rad patch delete`.
|
||||
///
|
||||
/// Only the COB ref under our own namespace is dropped. On a patch we did
|
||||
/// not author we hold no such ref, so this returns `Ok` having deleted
|
||||
/// nothing — while still evicting the cache entry, hiding the patch until
|
||||
/// the next rebuild. Callers must restrict this to our own patches.
|
||||
fn delete_patch(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let mut node = Node::new(profile.home().socket_from_env());
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
|
||||
// Remove via the cache-backed store so the patch is dropped from both
|
||||
// the git refs and the COB cache that listings read from; otherwise the
|
||||
// deleted patch keeps showing up in the patch list.
|
||||
let mut patches = profile.patches_mut(&repo, &signer)?;
|
||||
patches.remove(&cob_id.into())?;
|
||||
drop(patches);
|
||||
|
||||
if opts.announce()
|
||||
&& let Err(e) = node.announce_refs_for(rid, [profile.public_key])
|
||||
{
|
||||
log::error!("Not able to announce changes: {}", e)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ pub fn router(ctx: Context) -> Router {
|
||||
.route("/revisions_by_patch", post(revision_handler))
|
||||
.route("/edit_patch", post(edit_patch_handler))
|
||||
.route("/create_patch_review", post(create_patch_review_handler))
|
||||
.route("/delete_patch", post(delete_patch_handler))
|
||||
.route("/get_embed", post(get_embeds_handler))
|
||||
.route("/save_embed_by_path", post(save_embed_handler))
|
||||
.route("/save_embed_by_clipboard", post(save_embed_handler))
|
||||
@@ -715,6 +716,23 @@ async fn create_patch_review_handler(
|
||||
Ok::<_, Error>(Json(review_id))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeletePatchBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub cob_id: git::Oid,
|
||||
pub opts: CobOptions,
|
||||
}
|
||||
|
||||
async fn delete_patch_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(DeletePatchBody { rid, cob_id, opts }): Json<DeletePatchBody>,
|
||||
) -> impl IntoResponse {
|
||||
ctx.delete_patch(rid, cob_id, opts)?;
|
||||
|
||||
Ok::<_, Error>(Json(()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct JobsBody {
|
||||
pub rid: identity::RepoId,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
didFromPublicKey,
|
||||
formatTimestamp,
|
||||
patchStatusLabel,
|
||||
publicKeyFromDid,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
import { announce } from "@app/components/AnnounceSwitch.svelte";
|
||||
@@ -121,6 +122,14 @@
|
||||
),
|
||||
);
|
||||
|
||||
// Deleting drops the COB ref under our own namespace, so peers prune it when
|
||||
// they next fetch. We hold no such ref on anyone else's patch, where deleting
|
||||
// would only evict the local cache and the patch would return on the next
|
||||
// fetch — so the action is author-only.
|
||||
const isOwnPatch = $derived(
|
||||
publicKeyFromDid(patch.author.did) === config.publicKey,
|
||||
);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
let tab: "patch" | "revisions" | "timeline" = $state(
|
||||
revisions.length > 1 ? "revisions" : "patch",
|
||||
@@ -560,6 +569,30 @@
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
let deleteMenuExpanded = $state(false);
|
||||
let deleting = $state(false);
|
||||
async function deletePatch() {
|
||||
if (deleting) return;
|
||||
deleting = true;
|
||||
try {
|
||||
await invoke("delete_patch", {
|
||||
rid: repo.rid,
|
||||
cobId: patch.id,
|
||||
opts: { announce: $nodeRunning && $announce },
|
||||
});
|
||||
void router.push({
|
||||
resource: "repo.patches",
|
||||
rid: repo.rid,
|
||||
status: patch.state.status,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Deleting patch failed", error);
|
||||
} finally {
|
||||
deleting = false;
|
||||
deleteMenuExpanded = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -573,6 +606,45 @@
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.confirm-delete {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
min-width: 16rem;
|
||||
}
|
||||
.confirm-delete-text {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.confirm-delete-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.confirm-delete-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 2rem;
|
||||
padding: 0 0.75rem;
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-feedback-error-fill);
|
||||
color: var(--color-text-on-brand);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
.confirm-delete-button:hover:not(:disabled),
|
||||
.confirm-delete-button:focus-visible:not(:disabled) {
|
||||
background-color: var(--color-feedback-error-fill-hover);
|
||||
}
|
||||
.confirm-delete-button:active:not(:disabled) {
|
||||
background-color: var(--color-feedback-error-fill-active);
|
||||
}
|
||||
.confirm-delete-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.breadcrumb-link {
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
@@ -886,6 +958,53 @@
|
||||
style:margin-left="auto"
|
||||
style:gap="0.5rem"
|
||||
style:z-index="40">
|
||||
{#if isOwnPatch}
|
||||
<Popover
|
||||
popoverPadding="0"
|
||||
placement="bottom-end"
|
||||
bind:expanded={deleteMenuExpanded}>
|
||||
{#snippet toggle(onclick)}
|
||||
<Button
|
||||
variant="naked"
|
||||
{onclick}
|
||||
active={deleteMenuExpanded}
|
||||
title="Delete patch from your node">
|
||||
<Icon name="trash" />
|
||||
<span class="global-hide-on-medium-desktop-down">Delete</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet popover()}
|
||||
<div
|
||||
style:border="1px solid var(--color-border-subtle)"
|
||||
style:border-radius="var(--border-radius-sm)"
|
||||
style:background-color="var(--color-surface-canvas)">
|
||||
<div class="confirm-delete">
|
||||
<div class="confirm-delete-text txt-body-m-regular">
|
||||
Delete this patch from your node? This removes your copy
|
||||
only — you won't be able to restore it here, and peers who
|
||||
have already replicated the patch keep theirs.
|
||||
</div>
|
||||
<div class="confirm-delete-actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={deleting}
|
||||
onclick={() => (deleteMenuExpanded = false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
class="confirm-delete-button txt-body-m-medium"
|
||||
disabled={deleting}
|
||||
onclick={deletePatch}>
|
||||
<Icon name="trash" />
|
||||
{deleting ? "Deleting…" : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
<ShareButton
|
||||
explorerPath={`${repo.rid}/patches/${patch.id}`}
|
||||
id={patch.id}
|
||||
|
||||
Reference in New Issue
Block a user