Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Make `Operation` generic, and handle patch operations
Sebastian Martinez committed 1 year ago
commit 432db1ce5ba45646dacf0e86f2a9389da1d729ca
parent 63d1c87c7f10456970b868367fce9f12d2e741df
16 files changed +218 -134
modified crates/radicle-tauri/src/commands/cob.rs
@@ -2,12 +2,10 @@ use std::path::PathBuf;

use anyhow::{Context, Result};

-
use radicle::cob;
use radicle::git;
use radicle::identity;
use radicle_types as types;
use radicle_types::error::Error;
-
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::thread::Thread;
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_dialog::DialogExt;
@@ -84,13 +82,3 @@ pub async fn save_embed_to_disk(

    ctx.save_embed_to_disk(rid, oid, path)
}
-

-
#[tauri::command]
-
pub fn activity_by_id(
-
    ctx: tauri::State<AppState>,
-
    rid: identity::RepoId,
-
    type_name: cob::TypeName,
-
    id: git::Oid,
-
) -> Result<Vec<types::cobs::issue::Operation>, Error> {
-
    ctx.activity_by_id(rid, type_name, id)
-
}
modified crates/radicle-tauri/src/commands/cob/issue.rs
@@ -1,8 +1,10 @@
use radicle::git;
use radicle::identity;

+
use radicle::issue::{Action, TYPENAME};
use radicle_types as types;
use radicle_types::error::Error;
+
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::issue::Issues;
use radicle_types::traits::issue::IssuesMut;

@@ -55,3 +57,12 @@ pub(crate) fn comment_threads_by_issue_id(
) -> Result<Option<Vec<types::cobs::thread::Thread>>, Error> {
    ctx.comment_threads_by_issue_id(rid, id)
}
+

+
#[tauri::command]
+
pub fn activity_by_issue(
+
    ctx: tauri::State<AppState>,
+
    rid: identity::RepoId,
+
    id: git::Oid,
+
) -> Result<Vec<types::cobs::Operation<Action>>, Error> {
+
    ctx.activity_by_id(rid, &TYPENAME, id)
+
}
modified crates/radicle-tauri/src/commands/cob/patch.rs
@@ -3,8 +3,10 @@ use radicle::git;
use radicle::identity;
use radicle::patch;

+
use radicle::patch::{Action, TYPENAME};
use radicle_types as types;
use radicle_types::error::Error;
+
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::patch::Patches;
use radicle_types::traits::patch::PatchesMut;

@@ -109,3 +111,12 @@ pub fn edit_patch(
) -> Result<types::cobs::patch::Patch, Error> {
    ctx.edit_patch(rid, cob_id, action, opts)
}
+

+
#[tauri::command]
+
pub fn activity_by_patch(
+
    ctx: tauri::State<AppState>,
+
    rid: identity::RepoId,
+
    id: git::Oid,
+
) -> Result<Vec<types::cobs::Operation<Action>>, Error> {
+
    ctx.activity_by_id(rid, &TYPENAME, id)
+
}
modified crates/radicle-tauri/src/lib.rs
@@ -78,15 +78,16 @@ pub fn run() {
            diff::get_diff,
            cob::get_embed,
            cob::save_embed_to_disk,
-
            cob::activity_by_id,
            cob::save_embed_by_path,
            cob::save_embed_by_clipboard,
            cob::save_embed_by_bytes,
+
            cob::issue::activity_by_issue,
            cob::issue::list_issues,
            cob::issue::issue_by_id,
            cob::issue::comment_threads_by_issue_id,
            cob::issue::create_issue,
            cob::issue::edit_issue,
+
            cob::patch::activity_by_patch,
            cob::patch::list_patches,
            cob::patch::patch_by_id,
            cob::patch::edit_patch,
added crates/radicle-types/bindings/cob/Operation.ts
@@ -0,0 +1,13 @@
+
// 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";
+

+
/**
+
 * Everything that can be done in the system is represented by an `Op`.
+
 * Operations are applied to an accumulator to yield a final state.
+
 */
+
export type Operation<A> = {
+
  id: string;
+
  actions: Array<A>;
+
  author: Author;
+
  timestamp: number;
+
};
deleted crates/radicle-types/bindings/cob/issue/Operation.ts
@@ -1,27 +0,0 @@
-
// 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 Operation =
-
  & { entryId: string; timestamp: number; author: Author }
-
  & (
-
    | { "type": "assign"; assignees: Array<string> }
-
    | { "type": "edit"; title: string }
-
    | { "type": "lifecycle"; state: State }
-
    | { "type": "label"; labels: Array<string> }
-
    | {
-
      "type": "comment";
-
      body: string;
-
      replyTo?: string;
-
      embeds?: Array<Embed>;
-
    }
-
    | {
-
      "type": "comment.edit";
-
      id: string;
-
      body: string;
-
      embeds?: Array<Embed>;
-
    }
-
    | { "type": "comment.redact"; id: string }
-
    | { "type": "comment.react"; id: string; reaction: string; active: boolean }
-
  );
modified crates/radicle-types/src/cobs.rs
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;

+
use radicle::cob;
use radicle::identity;
use radicle::node::{Alias, AliasStore};

@@ -31,6 +32,21 @@ impl Author {
    }
}

+
/// Everything that can be done in the system is represented by an `Op`.
+
/// Operations are applied to an accumulator to yield a final state.
+
#[derive(Debug, Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/")]
+
pub struct Operation<A> {
+
    #[ts(as = "String")]
+
    pub id: cob::EntryId,
+
    pub actions: Vec<A>,
+
    pub author: Author,
+
    #[ts(type = "number")]
+
    pub timestamp: cob::Timestamp,
+
}
+

#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
modified crates/radicle-types/src/cobs/issue.rs
@@ -1,6 +1,5 @@
use std::collections::BTreeSet;

-
use radicle::git;
use radicle::node::AliasStore;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -55,20 +54,6 @@ impl Issue {
    }
}

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
#[ts(export_to = "cob/issue/")]
-
pub struct Operation {
-
    #[ts(as = "String")]
-
    pub entry_id: git::Oid,
-
    #[serde(flatten)]
-
    pub action: Action,
-
    #[ts(type = "number")]
-
    pub timestamp: cob::Timestamp,
-
    pub author: cobs::Author,
-
}
-

#[derive(Default, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "status")]
#[ts(export)]
modified crates/radicle-types/src/error.rs
@@ -9,6 +9,10 @@ pub enum Error {
    #[error(transparent)]
    Profile(#[from] radicle::profile::Error),

+
    /// CobStore error.
+
    #[error(transparent)]
+
    CobStore(#[from] radicle::cob::store::Error),
+

    /// Anyhow error.
    #[error(transparent)]
    Anyhow(#[from] anyhow::Error),
modified crates/radicle-types/src/traits/cobs.rs
@@ -2,39 +2,42 @@ use radicle::cob::object::Storage;
use radicle::storage::refs::draft;
use radicle::storage::{self, ReadStorage};
use radicle::{cob, git, identity};
+
use serde::de::DeserializeOwned;

use crate::error::Error;
use crate::traits::Profile;

pub trait Cobs: Profile {
-
    fn activity_by_id(
+
    #[allow(clippy::unnecessary_filter_map)]
+
    fn activity_by_id<A: DeserializeOwned>(
        &self,
        rid: identity::RepoId,
-
        type_name: cob::TypeName,
+
        type_name: &cob::TypeName,
        id: git::Oid,
-
    ) -> Result<Vec<crate::cobs::issue::Operation>, Error> {
+
    ) -> Result<Vec<crate::cobs::Operation<A>>, Error> {
        let profile = self.profile();
        let aliases = profile.aliases();
        let repo = profile.storage.repository(rid)?;
-
        let ops = cob::store::ops(&id.into(), &type_name, &repo).unwrap();
-
        let mut actions: Vec<crate::cobs::issue::Operation> = Vec::new();
-

-
        for op in ops.into_iter() {
-
            actions.extend(op.actions.iter().filter_map(
-
                |action: &Vec<u8>| -> Option<crate::cobs::issue::Operation> {
-
                    let action: crate::cobs::issue::Action = serde_json::from_slice(action).ok()?;
+
        let iter = cob::store::ops(&id.into(), type_name, &repo)?;
+
        let ops = iter
+
            .into_iter()
+
            .filter_map(|op| {
+
                let actions = op
+
                    .actions
+
                    .iter()
+
                    .filter_map(|a| serde_json::from_slice(a).ok())
+
                    .collect::<Vec<_>>();

-
                    Some(crate::cobs::issue::Operation {
-
                        entry_id: op.id,
-
                        action,
-
                        author: crate::cobs::Author::new(&op.author.into(), &aliases),
-
                        timestamp: op.timestamp,
-
                    })
-
                },
-
            ))
-
        }
+
                Some(crate::cobs::Operation {
+
                    id: op.id,
+
                    actions,
+
                    author: crate::cobs::Author::new(&op.author.into(), &aliases),
+
                    timestamp: op.timestamp,
+
                })
+
            })
+
            .collect::<Vec<_>>();

-
        Ok::<_, Error>(actions)
+
        Ok::<_, Error>(ops)
    }

    fn publish_draft(
modified crates/test-http-api/src/api.rs
@@ -8,13 +8,14 @@ use axum::routing::post;
use axum::Router;
use hyper::header::CONTENT_TYPE;
use hyper::Method;
-
use radicle::cob::TypeName;
use serde::{Deserialize, Serialize};
use tower_http::cors::{self, CorsLayer};

use radicle::{git, identity};
use radicle_types as types;
-
use radicle_types::cobs::issue::{Action, NewIssue};
+
use radicle_types::cobs::issue;
+
use radicle_types::cobs::issue::NewIssue;
+
use radicle_types::cobs::patch;
use radicle_types::cobs::CobOptions;
use radicle_types::error::Error;
use radicle_types::traits::auth::Auth;
@@ -57,7 +58,14 @@ pub fn router(ctx: Context) -> Router {
        .route("/list_repos", post(repo_root_handler))
        .route("/repo_by_id", post(repo_handler))
        .route("/diff_stats", post(diff_stats_handler))
-
        .route("/activity_by_id", post(activity_handler))
+
        .route(
+
            "/activity_by_issue",
+
            post(activity_issue_handler::<issue::Action>),
+
        )
+
        .route(
+
            "/activity_by_patch",
+
            post(activity_patch_handler::<patch::Action>),
+
        )
        .route("/get_diff", post(diff_handler))
        .route("/list_issues", post(issues_handler))
        .route("/create_issue", post(create_issue_handler))
@@ -206,7 +214,7 @@ async fn create_issue_comment_handler(
struct EditIssuesBody {
    pub rid: identity::RepoId,
    pub cob_id: git::Oid,
-
    pub action: Action,
+
    pub action: issue::Action,
    pub opts: CobOptions,
}

@@ -228,15 +236,23 @@ async fn edit_issue_handler(
#[serde(rename_all = "camelCase")]
struct ActivityBody {
    pub rid: identity::RepoId,
-
    pub type_name: TypeName,
    pub id: git::Oid,
}

-
async fn activity_handler(
+
async fn activity_issue_handler<A: serde::Serialize + serde::de::DeserializeOwned>(
+
    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)?;
+

+
    Ok::<_, Error>(Json(activity))
+
}
+

+
async fn activity_patch_handler<A: serde::Serialize + serde::de::DeserializeOwned>(
    State(ctx): State<Context>,
-
    Json(ActivityBody { rid, type_name, id }): Json<ActivityBody>,
+
    Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
-
    let activity = ctx.activity_by_id(rid, type_name, id)?;
+
    let activity = ctx.activity_by_id::<A>(rid, &radicle::cob::patch::TYPENAME, id)?;

    Ok::<_, Error>(Json(activity))
}
modified src/components/IssueTimelineLifecycleAction.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
-
  import type { Operation } from "@bindings/cob/issue/Operation";
+
  import type { Operation } from "@bindings/cob/Operation";
+
  import type { Action } from "@bindings/cob/issue/Action";

  import { authorForNodeId, formatTimestamp } from "@app/lib/utils";

@@ -8,19 +9,20 @@
  import NodeId from "@app/components/NodeId.svelte";

  interface Props {
-
    operation: Extract<Operation, { type: "lifecycle" }>;
+
    action: Extract<Action, { type: "lifecycle" }>;
+
    op: Operation<Action>;
  }

-
  const { operation }: Props = $props();
+
  const { op, action }: Props = $props();
</script>

<Border variant="float" stylePadding="1rem">
  <div class="txt-small">
    <div class="global-flex txt-small">
-
      <NodeId {...authorForNodeId(operation.author)} />
+
      <NodeId {...authorForNodeId(op.author)} />
      changed status to
-
      <IssueStateBadge state={operation.state} />
-
      {formatTimestamp(operation.timestamp)}
+
      <IssueStateBadge state={action.state} />
+
      {formatTimestamp(op.timestamp)}
    </div>
  </div>
</Border>
added src/components/PatchTimelineLifecycleAction.svelte
@@ -0,0 +1,28 @@
+
<script lang="ts">
+
  import type { Operation } from "@bindings/cob/Operation";
+
  import type { Action } from "@bindings/cob/patch/Action";
+

+
  import { authorForNodeId, formatTimestamp } from "@app/lib/utils";
+

+
  import Border from "@app/components/Border.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import PatchStateBadge from "./PatchStateBadge.svelte";
+

+
  interface Props {
+
    action: Extract<Action, { type: "lifecycle" }>;
+
    op: Operation<Action>;
+
  }
+

+
  const { op, action }: Props = $props();
+
</script>
+

+
<Border variant="float" stylePadding="1rem">
+
  <div class="txt-small">
+
    <div class="global-flex txt-small">
+
      <NodeId {...authorForNodeId(op.author)} />
+
      changed status to
+
      <PatchStateBadge state={action.state} />
+
      {formatTimestamp(op.timestamp)}
+
    </div>
+
  </div>
+
</Border>
modified src/views/repo/Issue.svelte
@@ -1,9 +1,10 @@
<script lang="ts">
+
  import type { Action } from "@bindings/cob/issue/Action";
  import type { Author } from "@bindings/cob/Author";
  import type { Config } from "@bindings/config/Config";
  import type { Embed } from "@bindings/cob/thread/Embed";
  import type { Issue } from "@bindings/cob/issue/Issue";
-
  import type { Operation } from "@bindings/cob/issue/Operation";
+
  import type { Operation } from "@bindings/cob/Operation";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Thread } from "@bindings/cob/thread/Thread";
  import type { IssueStatus } from "./router";
@@ -45,7 +46,7 @@
    repo: RepoInfo;
    issue: Issue;
    issues: Issue[];
-
    activity: Operation[];
+
    activity: Operation<Action>[];
    config: Config;
    threads: Thread[];
    status: IssueStatus;
@@ -149,9 +150,8 @@
        rid: repo.rid,
        id: issue.id,
      }),
-
      invoke<Operation[]>("activity_by_id", {
+
      invoke<Operation<Action>[]>("activity_by_issue", {
        rid: repo.rid,
-
        typeName: "xyz.radicle.issue",
        id: issue.id,
      }),
      invoke<Thread[]>("comment_threads_by_issue_id", {
@@ -524,26 +524,28 @@

    <div>
      {#each activity as op}
-
        {#if op.type === "lifecycle"}
-
          <IssueTimelineLifecycleAction operation={op} />
-
          <div class="connector"></div>
-
        {:else if op.type === "comment"}
-
          {@const thread = threads.find(t => t.root.id === op.entryId)}
-
          {#if thread}
-
            <ThreadComponent
-
              {thread}
-
              rid={repo.rid}
-
              canEditComment={partial(
-
                roles.isDelegateOrAuthor,
-
                config.publicKey,
-
                repo.delegates.map(delegate => delegate.did),
-
              )}
-
              {editComment}
-
              createReply={partial(createReply)}
-
              reactOnComment={partial(reactOnComment, config.publicKey)} />
+
        {#each op.actions as action}
+
          {#if action.type === "lifecycle"}
+
            <IssueTimelineLifecycleAction {op} {action} />
            <div class="connector"></div>
+
          {:else if action.type === "comment"}
+
            {@const thread = threads.find(t => t.root.id === op.id)}
+
            {#if thread}
+
              <ThreadComponent
+
                {thread}
+
                rid={repo.rid}
+
                canEditComment={partial(
+
                  roles.isDelegateOrAuthor,
+
                  config.publicKey,
+
                  repo.delegates.map(delegate => delegate.did),
+
                )}
+
                {editComment}
+
                createReply={partial(createReply)}
+
                reactOnComment={partial(reactOnComment, config.publicKey)} />
+
              <div class="connector"></div>
+
            {/if}
          {/if}
-
        {/if}
+
        {/each}
      {/each}
    </div>

modified src/views/repo/Patch.svelte
@@ -1,4 +1,6 @@
<script lang="ts">
+
  import type { Operation } from "@bindings/cob/Operation";
+
  import type { Action } from "@bindings/cob/patch/Action";
  import type { Author } from "@bindings/cob/Author";
  import type { Config } from "@bindings/config/Config";
  import type { Diff } from "@bindings/diff/Diff";
@@ -45,6 +47,7 @@
    patches: PaginatedQuery<Patch[]>;
    revisions: Revision[];
    config: Config;
+
    activity: Operation<Action>[];
    status: PatchStatus | undefined;
  }

@@ -56,6 +59,7 @@
    revisions,
    config,
    status: initialStatus,
+
    activity,
  }: Props = $props();
  /* eslint-enable prefer-const */

@@ -535,6 +539,17 @@
          ) && partial(editRevision, revisions[0].id)}>
        </CommentComponent>
      </div>
+
      <div class="connector"></div>
+
      <div>
+
        {#each activity as op}
+
          {#each op.actions as action}
+
            {#if action.type === "revision"}
+
              <div>New revision created {action.oid}</div>
+
              <div class="connector"></div>
+
            {/if}
+
          {/each}
+
        {/each}
+
      </div>
    {:else}
      {@const revision = revisions.slice(-1)[0]}
      {#await loadHighlightedDiff(repo.rid, revision.base, revision.head) then diff}
modified src/views/repo/router.ts
@@ -1,7 +1,9 @@
+
import type { Action as IssueAction } from "@bindings/cob/issue/Action";
+
import type { Action as PatchAction } from "@bindings/cob/patch/Action";
import type { Config } from "@bindings/config/Config";
import type { Thread } from "@bindings/cob/thread/Thread";
import type { Issue } from "@bindings/cob/issue/Issue";
-
import type { Operation } from "@bindings/cob/issue/Operation";
+
import type { Operation } from "@bindings/cob/Operation";
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
import type { Patch } from "@bindings/cob/patch/Patch";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
@@ -33,7 +35,7 @@ export interface LoadedRepoIssueRoute {
    issue: Issue;
    issues: Issue[];
    status: IssueStatus;
-
    activity: Operation[];
+
    activity: Operation<IssueAction>[];
    threads: Thread[];
  };
}
@@ -82,6 +84,7 @@ export interface LoadedRepoPatchRoute {
    patches: PaginatedQuery<Patch[]>;
    status: PatchStatus | undefined;
    revisions: Revision[];
+
    activity: Operation<PatchAction>[];
  };
}

@@ -117,28 +120,42 @@ export type LoadedRepoRoute =
export async function loadPatch(
  route: RepoPatchRoute,
): Promise<LoadedRepoPatchRoute> {
-
  const [config, repo, patches, patch, revisions] = await Promise.all([
-
    invoke<Config>("config"),
-
    invoke<RepoInfo>("repo_by_id", {
-
      rid: route.rid,
-
    }),
-
    invoke<PaginatedQuery<Patch[]>>("list_patches", {
-
      rid: route.rid,
-
      status: route.status,
-
    }),
-
    invoke<Patch>("patch_by_id", {
-
      rid: route.rid,
-
      id: route.patch,
-
    }),
-
    invoke<Revision[]>("revisions_by_patch", {
-
      rid: route.rid,
-
      id: route.patch,
-
    }),
-
  ]);
+
  const [config, repo, patches, patch, revisions, activity] = await Promise.all(
+
    [
+
      invoke<Config>("config"),
+
      invoke<RepoInfo>("repo_by_id", {
+
        rid: route.rid,
+
      }),
+
      invoke<PaginatedQuery<Patch[]>>("list_patches", {
+
        rid: route.rid,
+
        status: route.status,
+
      }),
+
      invoke<Patch>("patch_by_id", {
+
        rid: route.rid,
+
        id: route.patch,
+
      }),
+
      invoke<Revision[]>("revisions_by_patch", {
+
        rid: route.rid,
+
        id: route.patch,
+
      }),
+
      invoke<Operation<PatchAction>[]>("activity_by_patch", {
+
        rid: route.rid,
+
        id: route.patch,
+
      }),
+
    ],
+
  );

  return {
    resource: "repo.patch",
-
    params: { repo, config, patch, patches, revisions, status: route.status },
+
    params: {
+
      repo,
+
      config,
+
      patch,
+
      patches,
+
      revisions,
+
      status: route.status,
+
      activity,
+
    },
  };
}

@@ -194,9 +211,8 @@ export async function loadIssue(
      rid: route.rid,
      id: route.issue,
    }),
-
    invoke<Operation[]>("activity_by_id", {
+
    invoke<Operation<IssueAction>[]>("activity_by_issue", {
      rid: route.rid,
-
      typeName: "xyz.radicle.issue",
      id: route.issue,
    }),
    invoke<Issue[]>("list_issues", {