Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add single patch route
Open rudolfs opened 1 year ago
7 files changed +286 -41 1a0de05a ea9a425c
modified src/App.svelte
@@ -13,6 +13,7 @@
  import Home from "@app/views/Home.svelte";
  import Issue from "@app/views/repo/Issue.svelte";
  import Issues from "@app/views/repo/Issues.svelte";
+
  import Patch from "@app/views/repo/Patch.svelte";
  import Patches from "@app/views/repo/Patches.svelte";

  const activeRouteStore = router.activeRouteStore;
@@ -56,6 +57,8 @@
  <Issue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issues"}
  <Issues {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "repo.patch"}
+
  <Patch {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patches"}
  <Patches {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "authenticationError"}
modified src/components/PatchTeaser.svelte
@@ -2,8 +2,14 @@
  import type { Patch } from "@bindings/Patch";
  import type { Stats } from "@bindings/Stats";

-
  import { formatOid, formatTimestamp } from "@app/lib/utils";
+
  import {
+
    formatOid,
+
    formatTimestamp,
+
    patchStatusBackgroundColor,
+
    patchStatusColor,
+
  } from "@app/lib/utils";
  import { invoke } from "@tauri-apps/api/core";
+
  import { push } from "@app/lib/router";

  import DiffStatBadge from "./DiffStatBadge.svelte";
  import Icon from "./Icon.svelte";
@@ -12,20 +18,6 @@

  export let patch: Patch;
  export let rid: string;
-

-
  const statusColor: Record<Patch["state"]["status"], string> = {
-
    draft: "var(--color-fill-gray)",
-
    open: "var(--color-fill-success)",
-
    archived: "var(--color-foreground-yellow)",
-
    merged: "var(--color-fill-primary)",
-
  };
-

-
  const statusBackgroundColor: Record<Patch["state"]["status"], string> = {
-
    draft: "var(--color-fill-ghost)",
-
    open: "var(--color-fill-diff-green)",
-
    archived: "var(--color-fill-private)",
-
    merged: "var(--color-fill-delegate)",
-
  };
</script>

<style>
@@ -58,12 +50,19 @@
  }
</style>

-
<div class="patch-teaser">
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
+
<div
+
  tabindex="0"
+
  role="button"
+
  class="patch-teaser"
+
  onclick={() => {
+
    void push({ resource: "repo.patch", rid, patch: patch.id });
+
  }}>
  <div class="global-flex">
    <div
      class="global-counter status"
-
      style:color={statusColor[patch.state.status]}
-
      style:background-color={statusBackgroundColor[patch.state.status]}>
+
      style:color={patchStatusColor[patch.state.status]}
+
      style:background-color={patchStatusBackgroundColor[patch.state.status]}>
      <Icon name="patch" />
    </div>
    <div
modified src/lib/router.ts
@@ -120,6 +120,7 @@ export function routeToPath(route: Route): string {
  } else if (
    route.resource === "repo.issue" ||
    route.resource === "repo.issues" ||
+
    route.resource === "repo.patch" ||
    route.resource === "repo.patches"
  ) {
    return repoRouteToPath(route);
modified src/lib/router/definitions.ts
@@ -3,15 +3,22 @@ import type { RepoInfo } from "@bindings/RepoInfo";
import type {
  LoadedRepoIssueRoute,
  LoadedRepoIssuesRoute,
+
  LoadedRepoPatchRoute,
  LoadedRepoPatchesRoute,
  RepoIssueRoute,
  RepoIssuesRoute,
+
  RepoPatchRoute,
  RepoPatchesRoute,
} from "@app/views/repo/router";

import { invoke } from "@tauri-apps/api/core";

-
import { loadIssues, loadIssue, loadPatches } from "@app/views/repo/router";
+
import {
+
  loadIssue,
+
  loadIssues,
+
  loadPatch,
+
  loadPatches,
+
} from "@app/views/repo/router";

interface BootingRoute {
  resource: "booting";
@@ -40,6 +47,7 @@ export type Route =
  | HomeRoute
  | RepoIssueRoute
  | RepoIssuesRoute
+
  | RepoPatchRoute
  | RepoPatchesRoute;

export type LoadedRoute =
@@ -48,6 +56,7 @@ export type LoadedRoute =
  | LoadedHomeRoute
  | LoadedRepoIssueRoute
  | LoadedRepoIssuesRoute
+
  | LoadedRepoPatchRoute
  | LoadedRepoPatchesRoute;

export async function loadRoute(
@@ -62,6 +71,8 @@ export async function loadRoute(
    return loadIssue(route);
  } else if (route.resource === "repo.issues") {
    return loadIssues(route);
+
  } else if (route.resource === "repo.patch") {
+
    return loadPatch(route);
  } else if (route.resource === "repo.patches") {
    return loadPatches(route);
  }
modified src/lib/utils.ts
@@ -1,4 +1,5 @@
import type { Issue } from "@bindings/Issue";
+
import type { Patch } from "@bindings/Patch";

import bs58 from "bs58";
import twemojiModule from "twemoji";
@@ -115,3 +116,20 @@ export const issueStatusBackgroundColor: Record<
  open: "var(--color-fill-diff-green)",
  closed: "var(--color-fill-diff-red)",
};
+

+
export const patchStatusColor: Record<Patch["state"]["status"], string> = {
+
  draft: "var(--color-fill-gray)",
+
  open: "var(--color-fill-success)",
+
  archived: "var(--color-foreground-yellow)",
+
  merged: "var(--color-fill-primary)",
+
};
+

+
export const patchStatusBackgroundColor: Record<
+
  Patch["state"]["status"],
+
  string
+
> = {
+
  draft: "var(--color-fill-ghost)",
+
  open: "var(--color-fill-diff-green)",
+
  archived: "var(--color-fill-private)",
+
  merged: "var(--color-fill-delegate)",
+
};
added src/views/repo/Patch.svelte
@@ -0,0 +1,152 @@
+
<script lang="ts">
+
  import type { Config } from "@bindings/Config";
+
  import type { Patch } from "@bindings/Patch";
+
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { Revision } from "@bindings/Revision";
+

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

+
  import Border from "@app/components/Border.svelte";
+
  import CopyableId from "@app/components/CopyableId.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import InlineTitle from "@app/components/InlineTitle.svelte";
+
  import Layout from "./Layout.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import Markdown from "@app/components/Markdown.svelte";
+

+
  export let repo: RepoInfo;
+
  export let patch: Patch;
+
  export let patches: Patch[];
+
  export let revisions: Revision[];
+
  export let config: Config;
+

+
  $: project = repo.payloads["xyz.radicle.project"]!;
+
</script>
+

+
<style>
+
  .title {
+
    font-size: var(--font-size-medium);
+
    font-weight: var(--font-weight-medium);
+
    -webkit-user-select: text;
+
    user-select: text;
+
    margin-bottom: 1rem;
+
    margin-top: 0.35rem;
+
  }
+
  .patch-teaser {
+
    max-width: 11rem;
+
    white-space: nowrap;
+
  }
+
  .patch-list {
+
    margin-top: 0.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
    padding-bottom: 1rem;
+
  }
+
  .content {
+
    padding: 0 1rem 1rem 1rem;
+
  }
+

+
  .body {
+
    background-color: var(--color-background-float);
+
    padding: 1rem;
+
  }
+
</style>
+

+
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
+
  <svelte:fragment slot="breadcrumbs">
+
    <Link route={{ resource: "home" }}>
+
      <NodeId
+
        nodeId={config.publicKey}
+
        alias={config.alias}
+
        styleFontFamily="var(--font-family-sans-serif)"
+
        styleFontSize="var(--font-size-tiny)" />
+
    </Link>
+
    <Link route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
+
      <div class="global-flex">
+
        <Icon name="chevron-right" />
+
        {project.data.name}
+
      </div>
+
    </Link>
+
    <Icon name="chevron-right" />
+
    Patches
+
  </svelte:fragment>
+

+
  <svelte:fragment slot="header-center">
+
    <CopyableId id={patch.id} />
+
  </svelte:fragment>
+

+
  <svelte:fragment slot="sidebar">
+
    <Border
+
      hoverable={false}
+
      variant="ghost"
+
      styleWidth="100%"
+
      styleHeight="32px">
+
      <div style:margin-left="0.5rem">
+
        <Icon name="patch" />
+
      </div>
+
      <span class="txt-small txt-semibold">Patches</span>
+
      <div class="global-flex txt-small" style:margin-left="auto">
+
        <div
+
          class="global-counter"
+
          style:padding="0 6px"
+
          style:background-color="var(--color-fill-ghost)"
+
          style:gap="4px">
+
          {project.meta.patches.draft +
+
            project.meta.patches.open +
+
            project.meta.patches.merged +
+
            project.meta.patches.archived}
+
        </div>
+
      </div>
+
    </Border>
+

+
    <div class="patch-list">
+
      {#each patches as sidebarPatch}
+
        <Link
+
          variant="tab"
+
          route={{
+
            resource: "repo.patch",
+
            rid: repo.rid,
+
            patch: sidebarPatch.id,
+
          }}>
+
          <div class="global-flex">
+
            <div
+
              style:color={patchStatusColor[sidebarPatch.state.status]}
+
              style:margin-left="2px">
+
              <Icon name="patch" />
+
            </div>
+
            <span class="txt-small patch-teaser txt-overflow">
+
              <InlineTitle content={sidebarPatch.title} fontSize="small" />
+
            </span>
+
          </div>
+
        </Link>
+
      {/each}
+
    </div>
+
  </svelte:fragment>
+

+
  <div class="content">
+
    <div class="title">
+
      <InlineTitle content={patch.title} fontSize="medium" />
+
    </div>
+
    <div class="txt-small body">
+
      {#if revisions[0].description.slice(-1)[0].body !== ""}
+
        <Markdown breaks content={revisions[0].description.slice(-1)[0].body} />
+
      {:else}
+
        <span class="txt-missing">No description.</span>
+
      {/if}
+
      <div class="global-flex txt-small" style:margin-top="1.5rem">
+
        <NodeId
+
          nodeId={patch.author.did.replace("did:key:", "")}
+
          alias={patch.author.alias} />
+
        opened
+
        <div class="global-oid">{formatOid(patch.id)}</div>
+
        {formatTimestamp(patch.timestamp)}
+
      </div>
+
    </div>
+
    <div class="txt-small" style:margin-top="1rem">Revisions</div>
+
    {#each revisions as revision}
+
      <div class="global-oid">{formatOid(revision.oid)}</div>
+
    {/each}
+
  </div>
+
</Layout>
modified src/views/repo/router.ts
@@ -2,6 +2,7 @@ import type { Config } from "@bindings/Config";
import type { Issue } from "@bindings/Issue";
import type { Patch } from "@bindings/Patch";
import type { RepoInfo } from "@bindings/RepoInfo";
+
import type { Revision } from "@bindings/Revision";

import { invoke } from "@tauri-apps/api/core";
import { unreachable } from "@app/lib/utils";
@@ -42,6 +43,23 @@ export interface LoadedRepoIssuesRoute {

export type PatchStatus = "all" | Patch["state"]["status"];

+
export interface RepoPatchRoute {
+
  resource: "repo.patch";
+
  rid: string;
+
  patch: string;
+
}
+

+
export interface LoadedRepoPatchRoute {
+
  resource: "repo.patch";
+
  params: {
+
    repo: RepoInfo;
+
    config: Config;
+
    patch: Patch;
+
    patches: Patch[];
+
    revisions: Revision[];
+
  };
+
}
+

export interface RepoPatchesRoute {
  resource: "repo.patches";
  rid: string;
@@ -58,45 +76,58 @@ export interface LoadedRepoPatchesRoute {
  };
}

-
export type RepoRoute = RepoIssueRoute | RepoIssuesRoute | RepoPatchesRoute;
+
export type RepoRoute =
+
  | RepoIssueRoute
+
  | RepoIssuesRoute
+
  | RepoPatchRoute
+
  | RepoPatchesRoute;
export type LoadedRepoRoute =
  | LoadedRepoIssueRoute
  | LoadedRepoIssuesRoute
+
  | LoadedRepoPatchRoute
  | LoadedRepoPatchesRoute;

-
export async function loadPatches(
-
  route: RepoPatchesRoute,
-
): Promise<LoadedRepoPatchesRoute> {
+
export async function loadPatch(
+
  route: RepoPatchRoute,
+
): Promise<LoadedRepoPatchRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
  const config: Config = await invoke("config");
  const patches: Patch[] = await invoke("list_patches", {
    rid: route.rid,
-
    status: route.status,
+
    status: "all",
+
  });
+
  const patch: Patch = await invoke("patch_by_id", {
+
    rid: route.rid,
+
    id: route.patch,
+
  });
+
  const revisions: Revision[] = await invoke("revisions_by_patch", {
+
    rid: route.rid,
+
    id: route.patch,
  });

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

-
export async function loadIssues(
-
  route: RepoIssuesRoute,
-
): Promise<LoadedRepoIssuesRoute> {
+
export async function loadPatches(
+
  route: RepoPatchesRoute,
+
): Promise<LoadedRepoPatchesRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
  const config: Config = await invoke("config");
-
  const issues: Issue[] = await invoke("list_issues", {
+
  const patches: Patch[] = await invoke("list_patches", {
    rid: route.rid,
    status: route.status,
  });

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

@@ -122,6 +153,24 @@ export async function loadIssue(
  };
}

+
export async function loadIssues(
+
  route: RepoIssuesRoute,
+
): Promise<LoadedRepoIssuesRoute> {
+
  const repo: RepoInfo = await invoke("repo_by_id", {
+
    rid: route.rid,
+
  });
+
  const config: Config = await invoke("config");
+
  const issues: Issue[] = await invoke("list_issues", {
+
    rid: route.rid,
+
    status: route.status,
+
  });
+

+
  return {
+
    resource: "repo.issues",
+
    params: { repo, config, issues, status: route.status },
+
  };
+
}
+

export function repoRouteToPath(route: RepoRoute): string {
  const pathSegments = ["/repos", route.rid];

@@ -136,6 +185,9 @@ export function repoRouteToPath(route: RepoRoute): string {
      url += `?${searchParams}`;
    }
    return url;
+
  } else if (route.resource === "repo.patch") {
+
    const url = [...pathSegments, "patches", route.patch].join("/");
+
    return url;
  } else if (route.resource === "repo.patches") {
    let url = [...pathSegments, "patches"].join("/");
    const searchParams = new URLSearchParams();
@@ -174,16 +226,25 @@ export function repoUrlToRoute(
        }
      }
    } else if (resource === "patches") {
-
      const status = searchParams.get("status");
-
      if (
-
        status === "draft" ||
-
        status === "open" ||
-
        status === "archived" ||
-
        status === "merged"
-
      ) {
-
        return { resource: "repo.patches", rid, status };
+
      const id = segments.shift();
+
      if (id) {
+
        return {
+
          resource: "repo.patch",
+
          rid,
+
          patch: id,
+
        };
      } else {
-
        return { resource: "repo.patches", rid, status: "all" };
+
        const status = searchParams.get("status");
+
        if (
+
          status === "draft" ||
+
          status === "open" ||
+
          status === "archived" ||
+
          status === "merged"
+
        ) {
+
          return { resource: "repo.patches", rid, status };
+
        } else {
+
          return { resource: "repo.patches", rid, status: "all" };
+
        }
      }
    } else {
      return null;