Convert cob actions into customized desktop actions

This commit allows us to e.g. convert the Set of DIDs we obtain from
assignee actions on cobs into a Set of Authors with their Aliases.

Also it introduces the `FromRadicleAction` trait which can be
implemented for any type of enum we get from the `radicle` create to
convert into a type which is better suited for us.

Removes also any calling the `alias` command in the `PatchTimeline` or
`IssueTimeline` since we already get the alias from the backend the
first time now.
This commit is contained in:
Sebastian Martinez
2025-03-18 13:10:26 -06:00
committed by Rūdolfs Ošiņš
parent b1bf3c510d
commit c730e82187
15 changed files with 278 additions and 80 deletions
@@ -1,7 +1,7 @@
use radicle::git;
use radicle::identity;
use radicle::issue::{Action, TYPENAME};
use radicle::issue::TYPENAME;
use radicle_types as types;
use radicle_types::error::Error;
use radicle_types::traits::cobs::Cobs;
@@ -63,6 +63,6 @@ pub fn activity_by_issue(
ctx: tauri::State<AppState>,
rid: identity::RepoId,
id: git::Oid,
) -> Result<Vec<types::cobs::Operation<Action>>, Error> {
) -> Result<Vec<types::cobs::Operation<types::cobs::issue::Action>>, Error> {
ctx.activity_by_id(rid, &TYPENAME, id)
}
@@ -1,4 +1,4 @@
use radicle::patch::{Action, TYPENAME};
use radicle::patch::TYPENAME;
use radicle::{cob, git, identity};
use radicle_types as types;
@@ -105,6 +105,6 @@ pub fn activity_by_patch(
ctx: tauri::State<AppState>,
rid: identity::RepoId,
id: git::Oid,
) -> Result<Vec<types::cobs::Operation<Action>>, Error> {
) -> Result<Vec<types::cobs::Operation<models::patch::Action>>, Error> {
ctx.activity_by_id(rid, &TYPENAME, id)
}
@@ -1,9 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "../Author";
import type { Embed } from "../thread/Embed";
import type { State } from "./State";
export type Action =
| { "type": "assign"; assignees: Array<string> }
| { "type": "assign"; assignees: Array<Author> }
| { "type": "edit"; title: string }
| { "type": "lifecycle"; state: State }
| { "type": "label"; labels: Array<string> }
@@ -1,4 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "../Author";
import type { CodeLocation } from "../thread/CodeLocation";
import type { Embed } from "../thread/Embed";
import type { Verdict } from "./Verdict";
@@ -7,7 +8,7 @@ export type Action =
| { "type": "edit"; title: string; target: string }
| { "type": "label"; labels: Array<string> }
| { "type": "lifecycle"; state: { status: "draft" | "open" | "archived" } }
| { "type": "assign"; assignees: Array<string> }
| { "type": "assign"; assignees: Array<Author> }
| { "type": "merge"; revision: string; commit: string }
| {
"type": "review";
+10 -1
View File
@@ -1,3 +1,4 @@
use radicle::profile::Aliases;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -11,7 +12,7 @@ pub mod repo;
pub mod stream;
pub mod thread;
#[derive(Debug, Clone, Serialize, TS, Deserialize)]
#[derive(Debug, Clone, Serialize, TS, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/")]
@@ -30,6 +31,14 @@ impl Author {
alias: aliases.alias(did),
}
}
pub fn did(&self) -> &identity::Did {
&self.did
}
}
pub trait FromRadicleAction<A> {
fn from_radicle_action(value: A, aliases: &Aliases) -> Self;
}
/// Everything that can be done in the system is represented by an `Op`.
+49 -4
View File
@@ -10,6 +10,9 @@ use radicle::issue;
use crate::cobs;
use super::Author;
use super::FromRadicleAction;
#[derive(TS, Serialize)]
#[ts(export)]
#[ts(export_to = "cob/issue/")]
@@ -136,10 +139,7 @@ pub struct NewIssue {
#[ts(export_to = "cob/issue/")]
pub enum Action {
#[serde(rename = "assign")]
Assign {
#[ts(as = "Vec<String>")]
assignees: BTreeSet<identity::Did>,
},
Assign { assignees: BTreeSet<Author> },
#[serde(rename = "edit")]
Edit { title: String },
@@ -189,3 +189,48 @@ pub enum Action {
active: bool,
},
}
impl FromRadicleAction<radicle::issue::Action> for Action {
fn from_radicle_action(
value: radicle::issue::Action,
aliases: &radicle::profile::Aliases,
) -> Self {
match value {
radicle::issue::Action::Assign { assignees } => Self::Assign {
assignees: assignees
.iter()
.map(|a| Author::new(a, aliases))
.collect::<BTreeSet<_>>(),
},
radicle::issue::Action::Comment {
body,
reply_to,
embeds,
} => Self::Comment {
body,
reply_to,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::issue::Action::CommentEdit { id, body, embeds } => Self::CommentEdit {
id,
body,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::issue::Action::CommentReact {
id,
reaction,
active,
} => Self::CommentReact {
id,
reaction,
active,
},
radicle::issue::Action::CommentRedact { id } => Self::CommentRedact { id },
radicle::issue::Action::Label { labels } => Self::Label { labels },
radicle::issue::Action::Lifecycle { state } => Self::Lifecycle {
state: state.into(),
},
radicle::issue::Action::Edit { title } => Self::Edit { title },
}
}
}
@@ -2,15 +2,17 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;
use radicle::node::AliasStore;
use radicle::profile::Aliases;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use radicle::cob;
use radicle::git;
use radicle::identity;
use radicle::patch;
use crate::cobs;
use crate::cobs::Author;
use crate::cobs::FromRadicleAction;
#[derive(Debug, TS, Serialize)]
#[ts(export)]
@@ -329,10 +331,7 @@ pub enum Action {
state: patch::Lifecycle,
},
#[serde(rename = "assign")]
Assign {
#[ts(as = "Vec<String>")]
assignees: BTreeSet<identity::Did>,
},
Assign { assignees: BTreeSet<cobs::Author> },
#[serde(rename = "merge")]
Merge {
#[ts(as = "String")]
@@ -512,3 +511,160 @@ pub enum Action {
active: bool,
},
}
impl FromRadicleAction<radicle::patch::Action> for Action {
fn from_radicle_action(value: radicle::patch::Action, aliases: &Aliases) -> Self {
match value {
radicle::patch::Action::ReviewRedact { review } => Self::ReviewRedact { review },
radicle::patch::Action::RevisionCommentReact {
revision,
comment,
reaction,
active,
} => Self::RevisionCommentReact {
revision,
comment,
reaction,
active,
},
radicle::patch::Action::RevisionCommentRedact { revision, comment } => {
Self::RevisionCommentRedact { revision, comment }
}
radicle::patch::Action::RevisionCommentEdit {
revision,
comment,
body,
embeds,
} => Self::RevisionCommentEdit {
revision,
comment,
body,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::patch::Action::RevisionComment {
revision,
location,
body,
reply_to,
embeds,
} => Self::RevisionComment {
revision,
location: location.map(Into::into),
body,
reply_to,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::patch::Action::RevisionRedact { revision } => {
Self::RevisionRedact { revision }
}
radicle::patch::Action::RevisionReact {
revision,
location,
reaction,
active,
} => Self::RevisionReact {
revision,
location: location.map(Into::into),
reaction,
active,
},
radicle::patch::Action::RevisionEdit {
revision,
description,
embeds,
} => Self::RevisionEdit {
revision,
description,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::patch::Action::Assign { assignees } => Self::Assign {
assignees: assignees
.iter()
.map(|a| Author::new(a, aliases))
.collect::<BTreeSet<_>>(),
},
radicle::patch::Action::Edit { title, target } => Self::Edit { title, target },
radicle::patch::Action::Label { labels } => Self::Label { labels },
radicle::patch::Action::Lifecycle { state } => Self::Lifecycle { state },
radicle::patch::Action::Merge { revision, commit } => Self::Merge { revision, commit },
radicle::patch::Action::Revision {
description,
base,
oid,
resolves,
} => Self::Revision {
description,
base,
oid,
resolves,
},
radicle::patch::Action::Review {
revision,
summary,
verdict,
labels,
} => Self::Review {
revision,
summary,
verdict: verdict.map(Into::into),
labels,
},
radicle::patch::Action::ReviewComment {
review,
body,
location,
reply_to,
embeds,
} => Self::ReviewComment {
review,
body,
location: location.map(Into::into),
reply_to,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::patch::Action::ReviewCommentEdit {
review,
comment,
body,
embeds,
} => Self::ReviewCommentEdit {
review,
comment,
body,
embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
},
radicle::patch::Action::ReviewCommentReact {
review,
comment,
reaction,
active,
} => Self::ReviewCommentReact {
review,
comment,
reaction,
active,
},
radicle::patch::Action::ReviewCommentRedact { review, comment } => {
Self::ReviewCommentRedact { review, comment }
}
radicle::patch::Action::ReviewCommentResolve { review, comment } => {
Self::ReviewCommentResolve { review, comment }
}
radicle::patch::Action::ReviewCommentUnresolve { review, comment } => {
Self::ReviewCommentUnresolve { review, comment }
}
radicle::patch::Action::ReviewEdit {
review,
summary,
verdict,
labels,
} => Self::ReviewEdit {
review,
summary,
verdict: verdict.map(Into::into),
labels,
},
}
}
}
+13 -3
View File
@@ -2,17 +2,18 @@ use radicle::storage::ReadStorage;
use radicle::{cob, git, identity};
use serde::de::DeserializeOwned;
use crate::cobs::FromRadicleAction;
use crate::error::Error;
use crate::traits::Profile;
pub trait Cobs: Profile {
#[allow(clippy::unnecessary_filter_map)]
fn activity_by_id<A: DeserializeOwned>(
fn activity_by_id<A: DeserializeOwned, B: FromRadicleAction<A>>(
&self,
rid: identity::RepoId,
type_name: &cob::TypeName,
id: git::Oid,
) -> Result<Vec<crate::cobs::Operation<A>>, Error> {
) -> Result<Vec<crate::cobs::Operation<B>>, Error> {
let profile = self.profile();
let aliases = profile.aliases();
let repo = profile.storage.repository(rid)?;
@@ -23,7 +24,16 @@ pub trait Cobs: Profile {
let actions = op
.actions
.iter()
.filter_map(|a| serde_json::from_slice(a).ok())
.filter_map(|a| {
if let Ok(r) = serde_json::from_slice::<A>(a) {
let x = B::from_radicle_action(r, &aliases);
Some(x)
} else {
log::error!("Not able to deserialize the action");
None
}
})
.collect::<Vec<_>>();
Some(crate::cobs::Operation {
+6 -1
View File
@@ -1,3 +1,5 @@
use std::collections::BTreeSet;
use radicle::issue::cache::Issues as _;
use radicle::node::Handle;
use radicle::storage::ReadStorage;
@@ -142,7 +144,10 @@ pub trait IssuesMut: Profile {
issue.lifecycle(state.into(), &signer)?;
}
cobs::issue::Action::Assign { assignees } => {
issue.assign(assignees, &signer)?;
issue.assign(
assignees.iter().map(|a| *a.did()).collect::<BTreeSet<_>>(),
&signer,
)?;
}
cobs::issue::Action::Label { labels } => {
issue.label(labels, &signer)?;
+6 -1
View File
@@ -1,3 +1,5 @@
use std::collections::BTreeSet;
use radicle::node::Handle;
use radicle::patch::cache::Patches as _;
use radicle::storage::ReadStorage;
@@ -198,7 +200,10 @@ pub trait PatchesMut: Profile {
patch.lifecycle(state, &signer)?;
}
models::patch::Action::Assign { assignees } => {
patch.assign(assignees, &signer)?;
patch.assign(
assignees.iter().map(|a| *a.did()).collect::<BTreeSet<_>>(),
&signer,
)?;
}
models::patch::Action::Label { labels } => {
patch.label(labels, &signer)?;
+13 -7
View File
@@ -13,10 +13,10 @@ use tower_http::cors::{self, CorsLayer};
use radicle::{git, identity};
use radicle_types as types;
use radicle_types::cobs;
use radicle_types::cobs::issue;
use radicle_types::cobs::issue::NewIssue;
use radicle_types::cobs::CobOptions;
use radicle_types::cobs::{self, FromRadicleAction};
use radicle_types::domain::inbox::models::notification::NotificationCount;
use radicle_types::domain::patch::models;
use radicle_types::domain::patch::service::Service;
@@ -71,11 +71,11 @@ pub fn router(ctx: Context) -> Router {
.route("/diff_stats", post(diff_stats_handler))
.route(
"/activity_by_issue",
post(activity_issue_handler::<issue::Action>),
post(activity_issue_handler::<radicle::issue::Action, issue::Action>),
)
.route(
"/activity_by_patch",
post(activity_patch_handler::<models::patch::Action>),
post(activity_patch_handler::<radicle::patch::Action, models::patch::Action>),
)
.route("/get_diff", post(diff_handler))
.route("/list_issues", post(issues_handler))
@@ -259,20 +259,26 @@ struct ActivityBody {
pub id: git::Oid,
}
async fn activity_issue_handler<A: serde::Serialize + serde::de::DeserializeOwned>(
async fn activity_issue_handler<
A: serde::Serialize + serde::de::DeserializeOwned,
B: FromRadicleAction<A> + serde::Serialize,
>(
State(ctx): State<Context>,
Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
let activity = ctx.activity_by_id::<A>(rid, &radicle::cob::issue::TYPENAME, id)?;
let activity = ctx.activity_by_id::<A, B>(rid, &radicle::cob::issue::TYPENAME, id)?;
Ok::<_, Error>(Json(activity))
}
async fn activity_patch_handler<A: serde::Serialize + serde::de::DeserializeOwned>(
async fn activity_patch_handler<
A: serde::Serialize + serde::de::DeserializeOwned,
B: FromRadicleAction<A> + serde::Serialize,
>(
State(ctx): State<Context>,
Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
let activity = ctx.activity_by_id::<A>(rid, &radicle::cob::patch::TYPENAME, id)?;
let activity = ctx.activity_by_id::<A, B>(rid, &radicle::cob::patch::TYPENAME, id)?;
Ok::<_, Error>(Json(activity))
}
+5 -25
View File
@@ -18,11 +18,9 @@
formatTimestamp,
issueStatusColor,
pluralize,
publicKeyFromDid,
} from "@app/lib/utils";
import Icon from "./Icon.svelte";
import NodeId from "./NodeId.svelte";
import { invoke } from "@app/lib/invoke";
import Id from "./Id.svelte";
interface Props {
@@ -66,7 +64,7 @@
return result;
}
function itemDiff(previousState: string[], newState: string[]) {
function itemDiff<A>(previousState: A[], newState: A[]) {
const removed = previousState.filter(x => !newState.includes(x));
const added = newState.filter(x => !previousState.includes(x));
return { removed, added };
@@ -169,7 +167,7 @@
<div class="wrapper">
<NodeId {...authorForNodeId(op.author)} />
{#if op.previous && op.previous.type === op.type}
{@const changed = itemDiff(
{@const changed = itemDiff<Author>(
op.previous?.assignees ?? [],
op.assignees,
)}
@@ -177,38 +175,20 @@
{#if changed.added.length}
assigned
{#each changed.added as assignee}
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
<NodeId
{...authorForNodeId({
did: assignee,
alias: alias ?? undefined,
})} />
{/await}
<NodeId {...authorForNodeId(assignee)} />
{/each}
{/if}
{#if changed.removed.length}
unassigned
{#each changed.removed as assignee}
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
<NodeId
{...authorForNodeId({
did: assignee,
alias: alias ?? undefined,
})} />
{/await}
<NodeId {...authorForNodeId(assignee)} />
{/each}
{/if}
{/if}
{:else}
assigned
{#each op.assignees as assignee}
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
<NodeId
{...authorForNodeId({
did: assignee,
alias: alias ?? undefined,
})} />
{/await}
<NodeId {...authorForNodeId(assignee)} />
{/each}
{/if}
<div title={absoluteTimestamp(op.timestamp)}>
+5 -25
View File
@@ -18,12 +18,10 @@
formatTimestamp,
patchStatusColor,
pluralize,
publicKeyFromDid,
} from "@app/lib/utils";
import Icon from "./Icon.svelte";
import Id from "./Id.svelte";
import NodeId from "./NodeId.svelte";
import { invoke } from "@app/lib/invoke";
interface Props {
patchId: string;
@@ -67,7 +65,7 @@
return result;
}
function itemDiff(previousState: string[], newState: string[]) {
function itemDiff<A>(previousState: A[], newState: A[]) {
const removed = previousState.filter(x => !newState.includes(x));
const added = newState.filter(x => !previousState.includes(x));
return { removed, added };
@@ -190,44 +188,26 @@
<div class="wrapper">
<NodeId {...authorForNodeId(op.author)} />
{#if op.previous && op.previous.type === op.type}
{@const changed = itemDiff(
{@const changed = itemDiff<Author>(
op.previous?.assignees ?? [],
op.assignees,
)}
{#if changed.added.length}
assigned
{#each changed.added as assignee}
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
<NodeId
{...authorForNodeId({
did: assignee,
alias: alias ?? undefined,
})} />
{/await}
<NodeId {...authorForNodeId(assignee)} />
{/each}
{/if}
{#if changed.removed.length}
unassigned
{#each changed.removed as assignee}
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
<NodeId
{...authorForNodeId({
did: assignee,
alias: alias ?? undefined,
})} />
{/await}
<NodeId {...authorForNodeId(assignee)} />
{/each}
{/if}
{:else}
assigned
{#each op.assignees as assignee}
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
<NodeId
{...authorForNodeId({
did: assignee,
alias: alias ?? undefined,
})} />
{/await}
<NodeId {...authorForNodeId(assignee)} />
{/each}
{/if}
<div title={absoluteTimestamp(op.timestamp)}>
+1 -1
View File
@@ -126,7 +126,7 @@
cobId: issue.id,
action: {
type: "assign",
assignees: assignees.map(a => a.did),
assignees,
},
opts: { announce: $nodeRunning && $announce },
});
+1 -1
View File
@@ -157,7 +157,7 @@
cobId: patch.id,
action: {
type: "assign",
assignees: assignees.map(a => a.did),
assignees,
},
opts: { announce: $nodeRunning && $announce },
});