Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Switch to flat project route type
Thomas Scholtes committed 2 years ago
commit 283b665b3147b695f1cfa619b780ca4a5e40f841
parent 684700a24d8f799797c270ed84169fde07b319ab
29 files changed +522 -745
modified src/App/Header/Search.svelte
@@ -49,13 +49,9 @@
      if (searchResult.results.length === 1) {
        const { project, baseUrl } = searchResult.results[0];
        void router.push({
-
          resource: "projects",
-
          params: {
-
            view: { resource: "tree" },
-
            id: project.id,
-
            peer: undefined,
-
            baseUrl,
-
          },
+
          resource: "project.tree",
+
          project: project.id,
+
          seed: baseUrl,
        });
      } else {
        modal.show({
modified src/App/Header/SearchResultsModal.svelte
@@ -37,12 +37,9 @@
            <Link
              on:afterNavigate={modal.hide}
              route={{
-
                resource: "projects",
-
                params: {
-
                  view: { resource: "tree" },
-
                  baseUrl: result.baseUrl,
-
                  id: result.project.id,
-
                },
+
                resource: "project.tree",
+
                seed: result.baseUrl,
+
                project: result.project.id,
              }}>
              <span title={result.baseUrl.hostname}>
                <span>{result.project.name}</span>
modified src/lib/router.ts
@@ -187,19 +187,13 @@ function pathToRoute(url: URL): Route | null {
        const baseUrl = extractBaseUrl(hostAndPort);
        const id = segments.shift();
        if (id) {
-
          const params = resolveProjectRoute(baseUrl, id, segments, url.search);
-
          if (params) {
-
            return {
-
              resource: "projects",
-
              params: params,
-
            };
-
          }
-
          return null;
+
          return resolveProjectRoute(baseUrl, id, segments, url.search);
+
        } else {
+
          return {
+
            resource: "seeds",
+
            params: { baseUrl, projectPageIndex: 0 },
+
          };
        }
-
        return {
-
          resource: "seeds",
-
          params: { baseUrl, projectPageIndex: 0 },
-
        };
      }
      return null;
    }
@@ -235,8 +229,17 @@ export function routeToPath(route: Route): string {
    return seedPath(route.params.baseUrl);
  } else if (route.resource === "loadError") {
    return "";
-
  } else if (route.resource === "projects") {
-
    return projectRouteToPath(route.params);
+
  } else if (
+
    route.resource === "project.tree" ||
+
    route.resource === "project.history" ||
+
    route.resource === "project.commit" ||
+
    route.resource === "project.issues" ||
+
    route.resource === "project.newIssue" ||
+
    route.resource === "project.issue" ||
+
    route.resource === "project.patches" ||
+
    route.resource === "project.patch"
+
  ) {
+
    return projectRouteToPath(route);
  } else if (route.resource === "booting") {
    return "";
  } else if (route.resource === "notFound") {
modified src/lib/router/definitions.ts
@@ -55,8 +55,17 @@ export async function loadRoute(route: Route): Promise<LoadedRoute> {
    return await loadSeedRoute(route.params);
  } else if (route.resource === "home") {
    return await loadHomeRoute();
-
  } else if (route.resource === "projects") {
-
    return await loadProjectRoute(route.params);
+
  } else if (
+
    route.resource === "project.tree" ||
+
    route.resource === "project.history" ||
+
    route.resource === "project.commit" ||
+
    route.resource === "project.issues" ||
+
    route.resource === "project.newIssue" ||
+
    route.resource === "project.issue" ||
+
    route.resource === "project.patches" ||
+
    route.resource === "project.patch"
+
  ) {
+
    return await loadProjectRoute(route);
  } else {
    return route;
  }
modified src/views/home/Index.svelte
@@ -70,14 +70,9 @@
        <div class="project">
          <Link
            route={{
-
              resource: "projects",
-
              params: {
-
                view: { resource: "tree" },
-
                id: project.id,
-
                baseUrl,
-
                peer: undefined,
-
                revision: undefined,
-
              },
+
              resource: "project.tree",
+
              project: project.id,
+
              seed: baseUrl,
            }}>
            <ProjectCard
              compact
modified src/views/projects/BranchSelector.svelte
@@ -2,7 +2,7 @@
  import type { BaseUrl } from "@httpd-client";
  import type {
    LoadedSourceBrowsingView,
-
    ProjectsParams,
+
    ProjectRoute,
  } from "@app/views/projects/router";

  import * as utils from "@app/lib/utils";
@@ -27,21 +27,32 @@
  $: showSelector = branchList.length > 1;
  $: selectedCommitShortId = utils.formatCommit(selectedCommitId);

-
  function routeParamsView(
+
  function routeFromView(
+
    revision: string,
    view: LoadedSourceBrowsingView,
-
  ): ProjectsParams["view"] {
+
  ): ProjectRoute {
    if (view.resource === "tree") {
      return {
-
        resource: "tree",
+
        resource: "project.tree",
+
        seed: baseUrl,
+
        project: projectId,
+
        peer,
+
        revision,
      };
    } else if (view.resource === "history") {
      return {
-
        resource: "history",
+
        resource: "project.history",
+
        seed: baseUrl,
+
        project: projectId,
+
        peer,
+
        revision,
      };
    } else if (view.resource === "commits") {
      return {
-
        resource: "commits",
-
        commitId: view.commit.commit.id,
+
        resource: "project.commit",
+
        seed: baseUrl,
+
        project: projectId,
+
        commit: view.commit.commit.id,
      };
    } else {
      return utils.unreachable(view);
@@ -105,16 +116,7 @@
          <Dropdown items={branchList}>
            <svelte:fragment slot="item" let:item>
              <Link
-
                route={{
-
                  resource: "projects",
-
                  params: {
-
                    id: projectId,
-
                    baseUrl,
-
                    peer,
-
                    revision: item.value,
-
                    view: routeParamsView(view),
-
                  },
-
                }}
+
                route={routeFromView(item.value, view)}
                on:afterNavigate={() => closeFocused()}>
                <DropdownItem
                  selected={item.value === selectedBranch}
modified src/views/projects/Cob/Revision.svelte
@@ -211,18 +211,11 @@
              previousRevOid,
            )}..{utils.formatObjectId(revisionOid)}"
            route={{
-
              resource: "projects",
-
              params: {
-
                id: projectId,
-
                baseUrl,
-
                view: {
-
                  resource: "patch",
-
                  params: {
-
                    patch: patchId,
-
                    search: `diff=${previousRevOid}..${revisionOid}`,
-
                  },
-
                },
-
              },
+
              resource: "project.patch",
+
              project: projectId,
+
              seed: baseUrl,
+
              patch: patchId,
+
              search: `diff=${previousRevOid}..${revisionOid}`,
            }}>
            <Icon name="diff" />
          </Link>
@@ -240,18 +233,11 @@
                <Link
                  title="{item}..{revisionOid}"
                  route={{
-
                    resource: "projects",
-
                    params: {
-
                      id: projectId,
-
                      baseUrl,
-
                      view: {
-
                        resource: "patch",
-
                        params: {
-
                          patch: patchId,
-
                          search: `diff=${item}..${revisionOid}`,
-
                        },
-
                      },
-
                    },
+
                    resource: "project.patch",
+
                    project: projectId,
+
                    seed: baseUrl,
+
                    patch: patchId,
+
                    search: `diff=${item}..${revisionOid}`,
                  }}>
                  {#if item === projectHead}
                    <DropdownItem selected={false} size="small">
@@ -305,12 +291,10 @@
                    <Avatar inline nodeId={revisionAuthor.id} />
                    <Link
                      route={{
-
                        resource: "projects",
-
                        params: {
-
                          id: projectId,
-
                          baseUrl,
-
                          view: { resource: "commits", commitId: commit.id },
-
                        },
+
                        resource: "project.commit",
+
                        project: projectId,
+
                        seed: baseUrl,
+
                        commit: commit.id,
                      }}>
                      <div class="commit-summary" use:twemoji>
                        <InlineMarkdown
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -10,7 +10,6 @@

  export let baseUrl: BaseUrl;
  export let commit: CommitHeader;
-
  export let peer: string | undefined = undefined;
  export let projectId: string;

  let expandCommitMessage = false;
@@ -99,13 +98,10 @@
    <div class="message">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            peer,
-
            id: projectId,
-
            baseUrl,
-
            view: { resource: "commits", commitId: commit.id },
-
          },
+
          resource: "project.commit",
+
          project: projectId,
+
          seed: baseUrl,
+
          commit: commit.id,
        }}>
        <div class="summary" use:twemoji>
          <InlineMarkdown content={commit.summary} />
@@ -136,13 +132,10 @@
      title="Browse the repository at this point in the history">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            revision: commit.id,
-
            view: { resource: "tree" },
-
          },
+
          resource: "project.tree",
+
          project: projectId,
+
          seed: baseUrl,
+
          revision: commit.id,
        }}>
        <Icon name="browse" />
      </Link>
modified src/views/projects/Header.svelte
@@ -43,13 +43,10 @@
<div class="header">
  <Link
    route={{
-
      resource: "projects",
-
      params: {
-
        id: projectId,
-
        baseUrl,
-
        view: { resource: "tree" },
-
        path: "/",
-
      },
+
      resource: "project.tree",
+
      project: projectId,
+
      seed: baseUrl,
+
      path: "/",
    }}>
    <SquareButton
      active={resource === "tree" ||
@@ -63,15 +60,9 @@
  </Link>
  <Link
    route={{
-
      resource: "projects",
-
      params: {
-
        id: projectId,
-
        baseUrl,
-
        view: {
-
          resource: "issues",
-
          params: { view: { resource: "list" } },
-
        },
-
      },
+
      resource: "project.issues",
+
      project: projectId,
+
      seed: baseUrl,
    }}>
    <SquareButton active={resource === "issues" || resource === "issue"}>
      <svelte:fragment slot="icon">
@@ -84,15 +75,9 @@

  <Link
    route={{
-
      resource: "projects",
-
      params: {
-
        id: projectId,
-
        baseUrl,
-
        view: {
-
          resource: "patches",
-
          params: { view: { resource: "list" } },
-
        },
-
      },
+
      resource: "project.patches",
+
      project: projectId,
+
      seed: baseUrl,
    }}>
    <SquareButton active={resource === "patches" || resource === "patch"}>
      <svelte:fragment slot="icon">
modified src/views/projects/History.svelte
@@ -101,7 +101,7 @@
    <div class="group">
      {#each group.commits as commit (commit.id)}
        <div class="teaser-wrapper">
-
          <CommitTeaser {peer} projectId={project.id} {baseUrl} {commit} />
+
          <CommitTeaser projectId={project.id} {baseUrl} {commit} />
        </div>
      {/each}
    </div>
modified src/views/projects/Issue.svelte
@@ -167,15 +167,10 @@
      );
      if (status === "success") {
        void router.push({
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: {
-
              resource: "issue",
-
              params: { issue: issue.id },
-
            },
-
          },
+
          resource: "project.issue",
+
          project: projectId,
+
          seed: baseUrl,
+
          issue: issue.id,
        });
      }
    }
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -105,15 +105,10 @@
    <div class="summary">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: {
-
              resource: "issue",
-
              params: { issue: issue.id },
-
            },
-
          },
+
          resource: "project.issue",
+
          project: projectId,
+
          seed: baseUrl,
+
          issue: issue.id,
        }}>
        <span class="issue-title">
          <InlineMarkdown content={issue.title} />
modified src/views/projects/Issue/New.svelte
@@ -52,15 +52,10 @@
      );

      void router.push({
-
        resource: "projects",
-
        params: {
-
          id: projectId,
-
          baseUrl,
-
          view: {
-
            resource: "issue",
-
            params: { issue: result.id },
-
          },
-
        },
+
        resource: "project.issue",
+
        project: projectId,
+
        seed: baseUrl,
+
        issue: result.id,
      });
    } catch {
      modal.show({
modified src/views/projects/Issues.svelte
@@ -120,18 +120,10 @@
          {#if !option.disabled}
            <Link
              route={{
-
                resource: "projects",
-
                params: {
-
                  id: projectId,
-
                  baseUrl,
-
                  view: {
-
                    resource: "issues",
-
                    params: {
-
                      view: { resource: "list" },
-
                      search: `state=${option.value}`,
-
                    },
-
                  },
-
                },
+
                resource: "project.issues",
+
                project: projectId,
+
                seed: baseUrl,
+
                search: `state=${option.value}`,
              }}>
              <SquareButton
                clickable={option.disabled}
@@ -154,15 +146,9 @@
    {#if $httpdStore.state === "authenticated" && utils.isLocal(baseUrl.hostname)}
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: {
-
              resource: "issues",
-
              params: { view: { resource: "new" } },
-
            },
-
          },
+
          resource: "project.newIssue",
+
          project: projectId,
+
          seed: baseUrl,
        }}>
        <SquareButton>New issue</SquareButton>
      </Link>
modified src/views/projects/Patch.svelte
@@ -327,19 +327,12 @@
          {#if !option.disabled}
            <Link
              route={{
-
                resource: "projects",
-
                params: {
-
                  id: projectId,
-
                  baseUrl,
-
                  view: {
-
                    resource: "patch",
-
                    params: {
-
                      patch: patch.id,
-
                      revision,
-
                      search: `tab=${option.value}`,
-
                    },
-
                  },
-
                },
+
                resource: "project.patch",
+
                project: projectId,
+
                seed: baseUrl,
+
                patch: patch.id,
+
                revision,
+
                search: `tab=${option.value}`,
              }}>
              <SquareButton
                size="small"
@@ -362,18 +355,11 @@
        {#if diff}
          <Link
            route={{
-
              resource: "projects",
-
              params: {
-
                id: projectId,
-
                baseUrl,
-
                view: {
-
                  resource: "patch",
-
                  params: {
-
                    patch: patch.id,
-
                    search: `diff=${diff}`,
-
                  },
-
                },
-
              },
+
              resource: "project.patch",
+
              project: projectId,
+
              seed: baseUrl,
+
              patch: patch.id,
+
              search: `diff=${diff}`,
            }}>
            <SquareButton size="small" active={true}>
              Diff {diff.substr(0, 6)}..{diff.split("..")[1].substr(0, 6)}
@@ -398,20 +384,12 @@
                <Link
                  on:afterNavigate={closeFocused}
                  route={{
-
                    resource: "projects",
-
                    params: {
-
                      id: projectId,
-

-
                      baseUrl,
-
                      view: {
-
                        resource: "patch",
-
                        params: {
-
                          patch: patch.id,
-
                          revision: item.id,
-
                          search: `tab=${currentTab}`,
-
                        },
-
                      },
-
                    },
+
                    resource: "project.patch",
+
                    project: projectId,
+
                    seed: baseUrl,
+
                    patch: patch.id,
+
                    revision: item.id,
+
                    search: `tab=${currentTab}`,
                  }}>
                  <DropdownItem
                    selected={item.id === currentRevision.id}
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -128,15 +128,10 @@
    <div class="summary">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: {
-
              resource: "patch",
-
              params: { patch: patch.id },
-
            },
-
          },
+
          resource: "project.patch",
+
          project: projectId,
+
          seed: baseUrl,
+
          patch: patch.id,
        }}>
        <span class="patch-title">
          <InlineMarkdown content={patch.title} />
modified src/views/projects/Patches.svelte
@@ -122,18 +122,10 @@
        {:else}
          <Link
            route={{
-
              resource: "projects",
-
              params: {
-
                id: projectId,
-
                baseUrl,
-
                view: {
-
                  resource: "patches",
-
                  params: {
-
                    view: { resource: "list" },
-
                    search: `state=${option.value}`,
-
                  },
-
                },
-
              },
+
              resource: "project.patches",
+
              project: projectId,
+
              seed: baseUrl,
+
              search: `state=${option.value}`,
            }}>
            <SquareButton
              clickable={option.disabled}
modified src/views/projects/PeerSelector.svelte
@@ -2,7 +2,7 @@
  import type { BaseUrl, Remote } from "@httpd-client";
  import type {
    LoadedSourceBrowsingView,
-
    ProjectsParams,
+
    ProjectRoute,
  } from "@app/views/projects/router";

  import { closeFocused } from "@app/components/Floating.svelte";
@@ -32,21 +32,30 @@
      : `${nodeId} is a peer tracked by this node`;
  }

-
  function routeParamsView(
+
  function routeFromView(
+
    peer: string,
    view: LoadedSourceBrowsingView,
-
  ): ProjectsParams["view"] {
+
  ): ProjectRoute {
    if (view.resource === "tree") {
      return {
-
        resource: "tree",
+
        resource: "project.tree",
+
        seed: baseUrl,
+
        project: projectId,
+
        peer,
      };
    } else if (view.resource === "history") {
      return {
-
        resource: "history",
+
        resource: "project.history",
+
        seed: baseUrl,
+
        project: projectId,
+
        peer,
      };
    } else if (view.resource === "commits") {
      return {
-
        resource: "commits",
-
        commitId: view.commit.commit.id,
+
        resource: "project.commit",
+
        seed: baseUrl,
+
        project: projectId,
+
        commit: view.commit.commit.id,
      };
    } else {
      return unreachable(view);
@@ -133,16 +142,7 @@
        <div class="dropdown-item">
          <Link
            on:afterNavigate={() => closeFocused()}
-
            route={{
-
              resource: "projects",
-
              params: {
-
                id: projectId,
-
                baseUrl,
-
                peer: item.id,
-
                revision: undefined,
-
                view: routeParamsView(view),
-
              },
-
            }}>
+
            route={routeFromView(item.id, view)}>
            <DropdownItem
              selected={item.id === peer}
              title={createTitle(item)}
modified src/views/projects/ProjectMeta.svelte
@@ -89,13 +89,9 @@
    <span class="truncate">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: { resource: "tree" },
-
            path: "/",
-
          },
+
          resource: "project.tree",
+
          project: projectId,
+
          seed: baseUrl,
        }}>
        <span class="project-name">
          {projectName}
modified src/views/projects/Readme.svelte
@@ -23,15 +23,12 @@
      // Markdown.
      linkBaseUrl = new URL(
        routeToPath({
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: { resource: "tree" },
-
            peer,
-
            revision,
-
            path: "README.md",
-
          },
+
          resource: "project.tree",
+
          project: projectId,
+
          seed: baseUrl,
+
          peer,
+
          revision,
+
          path: "README.md",
        }),
        window.origin,
      ).href;
modified src/views/projects/SourceBrowser/FileDiff.svelte
@@ -175,14 +175,11 @@
    <div class="browse" title="View file">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: { resource: "tree" },
-
            path: file.path,
-
            revision,
-
          },
+
          resource: "project.tree",
+
          project: projectId,
+
          seed: baseUrl,
+
          path: file.path,
+
          revision,
        }}>
        <Icon name="browse" />
      </Link>
modified src/views/projects/SourceBrowser/FileLocationChange.svelte
@@ -53,14 +53,11 @@
    <div class="browse" title="View file">
      <Link
        route={{
-
          resource: "projects",
-
          params: {
-
            id: projectId,
-
            baseUrl,
-
            view: { resource: "tree" },
-
            path: file.newPath,
-
            revision,
-
          },
+
          resource: "project.tree",
+
          project: projectId,
+
          seed: baseUrl,
+
          path: file.newPath,
+
          revision,
        }}>
        <Icon name="browse" />
      </Link>
modified src/views/projects/SourceBrowsingHeader.svelte
@@ -30,7 +30,7 @@
  $: if (revision === commitId) {
    selectedBranch = undefined;
  } else {
-
    selectedBranch = revision ?? defaultBranch;
+
    selectedBranch = revision || defaultBranch;
  }
</script>

@@ -72,14 +72,11 @@

  <Link
    route={{
-
      resource: "projects",
-
      params: {
-
        id: projectId,
-
        baseUrl,
-
        peer,
-
        revision,
-
        view: { resource: "history" },
-
      },
+
      resource: "project.history",
+
      project: projectId,
+
      seed: baseUrl,
+
      peer,
+
      revision,
    }}>
    <SquareButton
      active={view.resource === "history" || view.resource === "commits"}>
modified src/views/projects/Tree.svelte
@@ -36,15 +36,12 @@
  {:else}
    <Link
      route={{
-
        resource: "projects",
-
        params: {
-
          id: projectId,
-
          baseUrl,
-
          path: entry.path,
-
          peer,
-
          revision,
-
          view: { resource: "tree" },
-
        },
+
        resource: "project.tree",
+
        project: projectId,
+
        seed: baseUrl,
+
        path: entry.path,
+
        peer,
+
        revision,
      }}
      on:afterNavigate={() => onSelect({ detail: entry.path })}>
      <File active={entry.path === path} name={entry.name} />
modified src/views/projects/Tree/Folder.svelte
@@ -98,15 +98,12 @@
          {:else}
            <Link
              route={{
-
                resource: "projects",
-
                params: {
-
                  id: projectId,
-
                  baseUrl,
-
                  path: entry.path,
-
                  peer,
-
                  revision,
-
                  view: { resource: "tree" },
-
                },
+
                resource: "project.tree",
+
                project: projectId,
+
                seed: baseUrl,
+
                path: entry.path,
+
                peer,
+
                revision,
              }}
              on:afterNavigate={() => onSelectFile({ detail: entry.path })}>
              <File active={entry.path === currentPath} name={entry.name} />
modified src/views/projects/router.ts
@@ -18,49 +18,62 @@ import { seedPath } from "@app/views/seeds/router";

export const COMMITS_PER_PAGE = 30;

-
export interface ProjectRoute {
-
  resource: "projects";
-
  params: ProjectsParams;
-
}
+
export type ProjectRoute =
+
  | {
+
      resource: "project.tree";
+
      seed: BaseUrl;
+
      project: string;
+
      path?: string;
+
      peer?: string;
+
      revision?: string;
+
      route?: string;
+
    }
+
  | {
+
      resource: "project.history";
+
      seed: BaseUrl;
+
      project: string;
+
      peer?: string;
+
      revision?: string;
+
    }
+
  | {
+
      resource: "project.commit";
+
      seed: BaseUrl;
+
      project: string;
+
      commit: string;
+
    }
+
  | {
+
      resource: "project.issues";
+
      seed: BaseUrl;
+
      project: string;
+
      search?: string;
+
    }
+
  | { resource: "project.newIssue"; seed: BaseUrl; project: string }
+
  | {
+
      resource: "project.issue";
+
      seed: BaseUrl;
+
      project: string;
+
      issue: string;
+
    }
+
  | {
+
      resource: "project.patches";
+
      seed: BaseUrl;
+
      project: string;
+
      search?: string;
+
    }
+
  | {
+
      resource: "project.patch";
+
      seed: BaseUrl;
+
      project: string;
+
      patch: string;
+
      revision?: string;
+
      search?: string;
+
    };

export interface ProjectLoadedRoute {
  resource: "projects";
  params: ProjectLoadedParams;
}

-
export interface ProjectsParams {
-
  baseUrl: BaseUrl;
-
  id: string;
-
  view:
-
    | { resource: "tree" }
-
    | { resource: "commits"; commitId: string }
-
    | { resource: "history" }
-
    | { resource: "issue"; params: { issue: string } }
-
    | {
-
        resource: "issues";
-
        params: {
-
          view: { resource: "new" | "list" };
-
          search?: string;
-
        };
-
      }
-
    | {
-
        resource: "patches";
-
        params: {
-
          view: { resource: "list" };
-
          search?: string;
-
        };
-
      }
-
    | {
-
        resource: "patch";
-
        params: { patch: string; revision?: string; search?: string };
-
      };
-

-
  path?: string;
-
  peer?: string;
-
  revision?: string;
-
  route?: string;
-
}
-

export interface ProjectLoadedParams {
  baseUrl: BaseUrl;
  id: string;
@@ -68,16 +81,6 @@ export interface ProjectLoadedParams {
  view: ProjectLoadedView;
}

-
interface LoadedSourceBrowsingParams {
-
  loadedBranches: Record<string, string> | undefined;
-
  loadedPeers: Remote[];
-
  loadedTree: Tree;
-
}
-

-
export type BlobResult =
-
  | { ok: true; blob: Blob; highlighted: Syntax.Root | undefined }
-
  | { ok: false; error: { message: string; path: string } };
-

export type LoadedSourceBrowsingView =
  | {
      resource: "tree";
@@ -101,6 +104,12 @@ export type LoadedSourceBrowsingView =
      totalCommitCount: number;
    };

+
interface LoadedSourceBrowsingParams {
+
  loadedBranches: Record<string, string> | undefined;
+
  loadedPeers: Remote[];
+
  loadedTree: Tree;
+
}
+

export type ProjectLoadedView =
  | LoadedSourceBrowsingView
  | { resource: "issue"; issue: Issue }
@@ -114,6 +123,10 @@ export type ProjectLoadedView =
      search: string;
    };

+
export type BlobResult =
+
  | { ok: true; blob: Blob; highlighted: Syntax.Root | undefined }
+
  | { ok: false; error: { message: string; path: string } };
+

// Check whether the input is a SHA1 commit.
function isOid(input: string): boolean {
  return /^[a-fA-F0-9]{40}$/.test(input);
@@ -141,16 +154,19 @@ export function parseRevisionToOid(
}

export async function loadProjectRoute(
-
  params: ProjectsParams,
+
  route: ProjectRoute,
): Promise<ProjectLoadedRoute | LoadError> {
-
  const api = new HttpdClient(params.baseUrl);
+
  const api = new HttpdClient(route.seed);
  try {
-
    if (params.view.resource === "tree" || params.view.resource === "history") {
-
      const projectPromise = api.project.getById(params.id);
-
      const peersPromise = api.project.getAllRemotes(params.id);
+
    if (
+
      route.resource === "project.tree" ||
+
      route.resource === "project.history"
+
    ) {
+
      const projectPromise = api.project.getById(route.project);
+
      const peersPromise = api.project.getAllRemotes(route.project);
      const branchesPromise = (async () => {
-
        if (params.peer) {
-
          return (await api.project.getRemoteByPeer(params.id, params.peer))
+
        if (route.peer) {
+
          return (await api.project.getRemoteByPeer(route.project, route.peer))
            .heads;
        } else {
          return undefined;
@@ -163,33 +179,30 @@ export async function loadProjectRoute(
        branchesPromise,
      ]);

-
      if (params.route) {
+
      if (route.resource === "project.tree" && route.route) {
        const { revision, path } = detectRevision(
-
          params.route,
+
          route.route,
          branches || { [project.defaultBranch]: project.head },
        );
-
        params.revision = revision;
-
        params.path = path;
-
        // TODO Do not mutate `params`. Contruct a new `loadedParams` object
-
        // instead.
-
        delete params.route;
+
        route.revision = revision;
+
        route.path = path;
      }

      const commit = parseRevisionToOid(
-
        params.revision,
+
        route.revision,
        project.defaultBranch,
        branches || { [project.defaultBranch]: project.head },
      );
-
      const tree = await api.project.getTree(params.id, commit);
+
      const tree = await api.project.getTree(route.project, commit);
      const viewParams = {
        loadedBranches: branches,
        loadedPeers: peers,
        loadedTree: tree,
      };
-
      if (params.view.resource === "tree") {
+
      if (route.resource === "project.tree") {
        let blobResult: BlobResult;

-
        const path = params.path || "/";
+
        const path = route.path || "/";
        try {
          let blob: Blob;
          if (path === "/") {
@@ -230,20 +243,20 @@ export async function loadProjectRoute(
        return {
          resource: "projects",
          params: {
-
            id: params.id,
-
            baseUrl: params.baseUrl,
+
            id: route.project,
+
            baseUrl: route.seed,
            project,
            view: {
              resource: "tree",
-
              peer: params.peer,
-
              revision: params.revision,
+
              peer: route.peer,
+
              revision: route.revision,
              params: viewParams,
              path,
              blobResult,
            },
          },
        };
-
      } else if (params.view.resource === "history") {
+
      } else if (route.resource === "project.history") {
        const commitsResponse = await api.project.getAllCommits(project.id, {
          parent: commit,
          page: 0,
@@ -253,13 +266,13 @@ export async function loadProjectRoute(
        return {
          resource: "projects",
          params: {
-
            id: params.id,
-
            baseUrl: params.baseUrl,
+
            id: route.project,
+
            baseUrl: route.seed,
            project,
            view: {
              resource: "history",
-
              peer: params.peer,
-
              revision: params.revision,
+
              peer: route.peer,
+
              revision: route.revision,
              params: viewParams,
              commitHeaders: commitsResponse.commits.map(c => c.commit),
              totalCommitCount: commitsResponse.stats.commits,
@@ -267,33 +280,33 @@ export async function loadProjectRoute(
          },
        };
      } else {
-
        return params.view;
+
        return unreachable(route);
      }
-
    } else if (params.view.resource === "commits") {
-
      const projectPromise = api.project.getById(params.id);
-
      const peersPromise = api.project.getAllRemotes(params.id);
+
    } else if (route.resource === "project.commit") {
+
      const projectPromise = api.project.getById(route.project);
+
      const peersPromise = api.project.getAllRemotes(route.project);

      const [project, peers] = await Promise.all([
        projectPromise,
        peersPromise,
      ]);

-
      const tree = await api.project.getTree(params.id, params.view.commitId);
+
      const tree = await api.project.getTree(route.project, route.commit);
      const viewParams = {
        loadedBranches: undefined,
        loadedPeers: peers,
        loadedTree: tree,
      };
      const loadedCommit = await api.project.getCommitBySha(
-
        params.id,
-
        params.view.commitId,
+
        route.project,
+
        route.commit,
      );

      return {
        resource: "projects",
        params: {
-
          id: params.id,
-
          baseUrl: params.baseUrl,
+
          id: route.project,
+
          baseUrl: route.seed,
          project,
          view: {
            resource: "commits",
@@ -302,12 +315,12 @@ export async function loadProjectRoute(
          },
        },
      };
-
    } else if (params.view.resource === "issue") {
+
    } else if (route.resource === "project.issue") {
      try {
-
        const projectPromise = api.project.getById(params.id);
+
        const projectPromise = api.project.getById(route.project);
        const issuePromise = api.project.getIssueById(
-
          params.id,
-
          params.view.params.issue,
+
          route.project,
+
          route.issue,
        );
        const [project, issue] = await Promise.all([
          projectPromise,
@@ -316,8 +329,8 @@ export async function loadProjectRoute(
        return {
          resource: "projects",
          params: {
-
            id: params.id,
-
            baseUrl: params.baseUrl,
+
            id: route.project,
+
            baseUrl: route.seed,
            project,
            view: {
              resource: "issue",
@@ -329,18 +342,18 @@ export async function loadProjectRoute(
        return {
          resource: "loadError",
          params: {
-
            title: params.view.params.issue,
+
            title: route.issue,
            errorMessage: "Not able to load this issue.",
            stackTrace: error.stack,
          },
        };
      }
-
    } else if (params.view.resource === "patch") {
+
    } else if (route.resource === "project.patch") {
      try {
-
        const projectPromise = api.project.getById(params.id);
+
        const projectPromise = api.project.getById(route.project);
        const patchPromise = api.project.getPatchById(
-
          params.id,
-
          params.view.params.patch,
+
          route.project,
+
          route.patch,
        );
        const [project, patch] = await Promise.all([
          projectPromise,
@@ -349,14 +362,14 @@ export async function loadProjectRoute(
        return {
          resource: "projects",
          params: {
-
            id: params.id,
-
            baseUrl: params.baseUrl,
+
            id: route.project,
+
            baseUrl: route.seed,
            project,
            view: {
              resource: "patch",
-
              search: params.view.params.search || "",
-
              patch: patch,
-
              revision: params.view.params.revision,
+
              patch,
+
              revision: route.revision,
+
              search: route.search || "",
            },
          },
        };
@@ -364,63 +377,61 @@ export async function loadProjectRoute(
        return {
          resource: "loadError",
          params: {
-
            title: params.view.params.patch,
+
            title: route.patch,
            errorMessage: "Not able to load this patch.",
            stackTrace: error.stack,
          },
        };
      }
-
    } else if (params.view.resource === "issues") {
-
      const project = await api.project.getById(params.id);
-
      if (params.view.params.view.resource === "list") {
-
        return {
-
          resource: "projects",
-
          params: {
-
            id: params.id,
-
            baseUrl: params.baseUrl,
-
            view: {
-
              resource: "issues",
-
              search: params.view.params.search || "",
-
            },
-
            project,
+
    } else if (route.resource === "project.issues") {
+
      const project = await api.project.getById(route.project);
+
      return {
+
        resource: "projects",
+
        params: {
+
          id: route.project,
+
          baseUrl: route.seed,
+
          view: {
+
            resource: "issues",
+
            search: route.search || "",
          },
-
        };
-
      } else if (params.view.params.view.resource === "new") {
-
        return {
-
          resource: "projects",
-
          params: {
-
            ...params,
-
            view: {
-
              resource: "newIssue",
-
            },
-
            project,
+
          project,
+
        },
+
      };
+
    } else if (route.resource === "project.newIssue") {
+
      const project = await api.project.getById(route.project);
+
      return {
+
        resource: "projects",
+
        params: {
+
          id: route.project,
+
          baseUrl: route.seed,
+
          view: {
+
            resource: "newIssue",
          },
-
        };
-
      } else {
-
        return unreachable(params.view.params.view.resource);
-
      }
-
    } else if (params.view.resource === "patches") {
-
      const project = await api.project.getById(params.id);
+
          project,
+
        },
+
      };
+
    } else if (route.resource === "project.patches") {
+
      const project = await api.project.getById(route.project);
      return {
        resource: "projects",
        params: {
-
          id: params.id,
-
          baseUrl: params.baseUrl,
+
          id: route.project,
+
          baseUrl: route.seed,
          view: {
            resource: "patches",
-
            search: params.view.params.search || "",
+
            search: route.search || "",
          },
          project,
        },
      };
    } else {
-
      return unreachable(params.view);
+
      return unreachable(route);
    }
  } catch (error: any) {
    return {
      resource: "loadError",
      params: {
-
        title: params.id,
+
        title: route.project,
        errorMessage: "Not able to load this project.",
        stackTrace: error.stack,
      },
@@ -462,11 +473,11 @@ function sanitizeQueryString(queryString: string): string {
}

export function resolveProjectRoute(
-
  baseUrl: BaseUrl,
-
  id: string,
+
  seed: BaseUrl,
+
  project: string,
  segments: string[],
  urlSearch: string,
-
): ProjectsParams | null {
+
): ProjectRoute | null {
  let content = segments.shift();
  let peer;
  if (content === "remotes") {
@@ -476,9 +487,9 @@ export function resolveProjectRoute(

  if (!content || content === "tree") {
    return {
-
      view: { resource: "tree" },
-
      baseUrl,
-
      id,
+
      resource: "project.tree",
+
      seed,
+
      project,
      peer,
      path: undefined,
      revision: undefined,
@@ -486,64 +497,40 @@ export function resolveProjectRoute(
    };
  } else if (content === "history") {
    return {
-
      view: { resource: "history" },
-
      baseUrl,
-
      id,
+
      resource: "project.history",
+
      seed,
+
      project,
      peer,
-
      path: undefined,
-
      revision: undefined,
-
      route: segments.join("/"),
+
      revision: segments.join("/"),
    };
  } else if (content === "commits") {
    return {
-
      view: { resource: "commits", commitId: segments[0] },
-
      baseUrl,
-
      id,
-
      peer,
-
      path: undefined,
-
      revision: undefined,
-
      route: undefined,
+
      resource: "project.commit",
+
      seed,
+
      project,
+
      commit: segments[0],
    };
  } else if (content === "issues") {
    const issueOrAction = segments.shift();
    if (issueOrAction === "new") {
      return {
-
        view: {
-
          resource: "issues",
-
          params: {
-
            view: { resource: "new" },
-
            search: sanitizeQueryString(urlSearch),
-
          },
-
        },
-
        baseUrl,
-
        id,
-
        peer,
-
        path: undefined,
-
        revision: undefined,
+
        resource: "project.newIssue",
+
        seed,
+
        project,
      };
    } else if (issueOrAction) {
      return {
-
        view: { resource: "issue", params: { issue: issueOrAction } },
-
        baseUrl,
-
        id,
-
        peer,
-
        path: undefined,
-
        revision: undefined,
+
        resource: "project.issue",
+
        seed,
+
        project,
+
        issue: issueOrAction,
      };
    } else {
      return {
-
        view: {
-
          resource: "issues",
-
          params: {
-
            view: { resource: "list" },
-
            search: sanitizeQueryString(urlSearch),
-
          },
-
        },
-
        baseUrl,
-
        id,
-
        peer,
-
        path: undefined,
-
        revision: undefined,
+
        resource: "project.issues",
+
        seed,
+
        project,
+
        search: sanitizeQueryString(urlSearch),
      };
    }
  } else if (content === "patches") {
@@ -551,103 +538,100 @@ export function resolveProjectRoute(
    const revision = segments.shift();
    if (patch) {
      return {
-
        view: {
-
          resource: "patch",
-
          params: { patch, revision, search: sanitizeQueryString(urlSearch) },
-
        },
-
        baseUrl,
-
        id,
-
        peer,
-
        path: undefined,
-
        revision: undefined,
+
        resource: "project.patch",
+
        seed,
+
        project,
+
        patch,
+
        revision,
+
        search: sanitizeQueryString(urlSearch),
      };
    } else {
      return {
-
        view: {
-
          resource: "patches",
-
          params: {
-
            view: { resource: "list" },
-
            search: sanitizeQueryString(urlSearch),
-
          },
-
        },
-
        baseUrl,
-
        id,
-
        peer,
-
        path: undefined,
-
        revision: undefined,
+
        resource: "project.patches",
+
        seed,
+
        project,
+
        search: sanitizeQueryString(urlSearch),
      };
    }
+
  } else {
+
    return null;
  }
-

-
  return null;
}

-
export function projectRouteToPath(params: ProjectsParams): string {
-
  const seed = seedPath(params.baseUrl);
+
export function projectRouteToPath(route: ProjectRoute): string {
+
  const seed = seedPath(route.seed);

-
  const pathSegments = [seed, params.id];
+
  const pathSegments = [seed, route.project];

-
  if (params.peer) {
-
    pathSegments.push("remotes", params.peer);
-
  }
+
  if (route.resource === "project.tree") {
+
    if (route.peer) {
+
      pathSegments.push("remotes", route.peer);
+
    }

-
  if (params.view.resource === "tree" || params.view.resource === "history") {
-
    pathSegments.push(params.view.resource);
+
    pathSegments.push("tree");
    let omitTree = true;

-
    if (params.route && params.route !== "/") {
-
      pathSegments.push(params.route);
+
    if (route.route && route.route !== "/") {
+
      pathSegments.push(route.route);
      omitTree = false;
    } else {
-
      if (params.revision) {
-
        pathSegments.push(params.revision);
+
      if (route.revision) {
+
        pathSegments.push(route.revision);
        omitTree = false;
      }

-
      if (params.path && params.path !== "/") {
-
        pathSegments.push(params.path);
+
      if (route.path && route.path !== "/") {
+
        pathSegments.push(route.path);
        omitTree = false;
      }
    }
-
    if (params.view.resource === "tree" && omitTree) {
+
    if (omitTree) {
      pathSegments.pop();
    }

    return pathSegments.join("/");
-
  } else if (params.view.resource === "commits") {
-
    return [...pathSegments, "commits", params.view.commitId].join("/");
-
  } else if (
-
    params.view.resource === "issues" &&
-
    params.view.params?.view.resource === "new"
-
  ) {
+
  } else if (route.resource === "project.history") {
+
    if (route.peer) {
+
      pathSegments.push("remotes", route.peer);
+
    }
+

+
    pathSegments.push("history");
+

+
    if (route.revision) {
+
      pathSegments.push(route.revision);
+
    }
+
    return pathSegments.join("/");
+
  } else if (route.resource === "project.commit") {
+
    return [...pathSegments, "commits", route.commit].join("/");
+
  } else if (route.resource === "project.newIssue") {
    return [...pathSegments, "issues", "new"].join("/");
-
  } else if (params.view.resource === "issues") {
+
  } else if (route.resource === "project.issues") {
    let url = [...pathSegments, "issues"].join("/");
-
    if (params.view.params.search) {
-
      url += `?${params.view.params.search}`;
+
    if (route.search) {
+
      url += `?${route.search}`;
    }
    return url;
-
  } else if (params.view.resource === "issue") {
-
    return [...pathSegments, "issues", params.view.params.issue].join("/");
-
  } else if (params.view.resource === "patches") {
+
  } else if (route.resource === "project.issue") {
+
    return [...pathSegments, "issues", route.issue].join("/");
+
  } else if (route.resource === "project.patches") {
    let url = [...pathSegments, "patches"].join("/");
-
    if (params.view.params.search) {
-
      url += `?${params.view.params.search}`;
+
    if (route.search) {
+
      url += `?${route.search}`;
    }
    return url;
-
  } else if (params.view.resource === "patch") {
-
    pathSegments.push("patches", params.view.params.patch);
-
    if (params.view.params.revision) {
-
      pathSegments.push(params.view.params.revision);
+
  } else if (route.resource === "project.patch") {
+
    pathSegments.push("patches", route.patch);
+
    if (route.revision) {
+
      pathSegments.push(route.revision);
    }

    let url = pathSegments.join("/");
-
    if (params.view.params.search) {
-
      url += `?${params.view.params.search}`;
+
    if (route.search) {
+
      url += `?${route.search}`;
    }
    return url;
  } else {
-
    return unreachable(params.view);
+
    return unreachable(route);
  }
}

modified src/views/seeds/View.svelte
@@ -123,12 +123,9 @@
        <div style:margin-bottom="0.5rem">
          <Link
            route={{
-
              resource: "projects",
-
              params: {
-
                view: { resource: "tree" },
-
                id: project.id,
-
                baseUrl,
-
              },
+
              resource: "project.tree",
+
              project: project.id,
+
              seed: baseUrl,
            }}>
            <ProjectCard
              {activity}
modified tests/e2e/project/commit.spec.ts
@@ -7,9 +7,7 @@ import {
  aliceRemote,
} from "@tests/support/fixtures.js";

-
const modifiedFileFixture = `${sourceBrowsingUrl}/remotes/${bobRemote.substring(
-
  8,
-
)}/commits/${bobHead}`;
+
const commitUrl = `${sourceBrowsingUrl}/commits/${bobHead}`;

test("navigation from commit list", async ({ page }) => {
  await page.goto(sourceBrowsingUrl);
@@ -18,7 +16,7 @@ test("navigation from commit list", async ({ page }) => {
  await page.locator('role=link[name="7 commits"]').click();

  await page.locator("text=Update readme").click();
-
  await expect(page).toHaveURL(modifiedFileFixture);
+
  await expect(page).toHaveURL(commitUrl);
});

test("relative timestamps", async ({ page }) => {
@@ -31,14 +29,14 @@ test("relative timestamps", async ({ page }) => {
      });
    };
  });
-
  await page.goto(modifiedFileFixture);
+
  await page.goto(commitUrl);
  await expect(
    page.locator(`.commit .header >> text=${"Bob Belcher committed now"}`),
  ).toBeVisible();
});

test("modified file", async ({ page }) => {
-
  await page.goto(modifiedFileFixture);
+
  await page.goto(commitUrl);

  // Commit header.
  {
modified tests/unit/router.test.ts
@@ -6,7 +6,7 @@ window.origin = "http://localhost:3000";

describe("route invariant when parsed", () => {
  const origin = "http://localhost:3000";
-
  const baseUrl = {
+
  const seed = {
    hostname: "willow.radicle.garden",
    port: 8000,
    scheme: "http",
@@ -22,252 +22,180 @@ describe("route invariant when parsed", () => {
        // TODO: This only works with the value 0. The value is not actually
        // extract.
        projectPageIndex: 0,
-
        baseUrl,
+
        baseUrl: seed,
      },
    });
  });
  test("projects.tree", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        baseUrl,
-
        id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
-
        route: "",
-
      },
+
      resource: "project.tree",
+
      seed,
+
      project: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
+
      route: "",
    });
  });

  test("projects.tree with peer", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        baseUrl,
-
        id: "PROJECT",
-
        peer: "PEER",
-
        route: "",
-
      },
+
      resource: "project.tree",
+
      seed,
+
      project: "PROJECT",
+
      peer: "PEER",
+
      route: "",
    });
  });

  test("projects.tree with peer and revision", () => {
    const route: Route = {
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        baseUrl,
-
        id: "PROJECT",
-
        peer: "PEER",
-
        revision: "REVISION",
-
        route: "",
-
      },
+
      resource: "project.tree",
+
      seed,
+
      project: "PROJECT",
+
      peer: "PEER",
+
      revision: "REVISION",
+
      route: "",
    };
    const path = testExports.routeToPath(route);
-
    route.params.revision = undefined;
-
    route.params.route = "REVISION";
+
    route.revision = undefined;
+
    route.route = "REVISION";
    expect(testExports.pathToRoute(new URL(path, origin))).toEqual(route);
  });

  test("projects.tree with peer and revision and path", () => {
    const route: Route = {
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        baseUrl,
-
        id: "PROJECT",
-
        peer: "PEER",
-
        path: "PATH",
-
        revision: "REVISION",
-
        route: "",
-
      },
+
      resource: "project.tree",
+
      seed,
+
      project: "PROJECT",
+
      peer: "PEER",
+
      path: "PATH",
+
      revision: "REVISION",
+
      route: "",
    };
    const path = testExports.routeToPath(route);
-
    route.params.revision = undefined;
-
    route.params.path = undefined;
-
    route.params.route = "REVISION/PATH";
+
    route.revision = undefined;
+
    route.path = undefined;
+
    route.route = "REVISION/PATH";
    expect(testExports.pathToRoute(new URL(path, origin))).toEqual(route);
  });

  test("projects.history", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "history" },
-
        baseUrl,
-
        id: "PROJECT",
-
        route: "",
-
      },
+
      resource: "project.history",
+
      seed,
+
      project: "PROJECT",
+
      revision: "",
    });
  });

  test("projects.history with revision", () => {
-
    const route: Route = {
-
      resource: "projects",
-
      params: {
-
        view: { resource: "history" },
-
        baseUrl,
-
        id: "PROJECT",
-
        peer: "PEER",
-
        revision: "REVISION",
-
      },
-
    };
-
    const path = testExports.routeToPath(route);
-
    route.params.revision = undefined;
-
    route.params.route = "REVISION";
-
    expect(testExports.pathToRoute(new URL(path, origin))).toEqual(route);
+
    expectParsingInvariant({
+
      resource: "project.history",
+
      seed,
+
      project: "PROJECT",
+
      revision: "REVISION",
+
    });
  });

  test("projects.commits", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "commits", commitId: "COMMIT" },
-
        baseUrl,
-
        id: "PROJECT",
-
        peer: "PEER",
-
      },
+
      resource: "project.commit",
+
      seed,
+
      project: "PROJECT",
+
      commit: "COMMIT",
    });
  });

  test("projects.issues", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "issues",
-
          params: { view: { resource: "list" }, search: "" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.issues",
+
      seed,
+
      project: "PROJECT",
+
      search: "",
    });
  });

  test("projects.issues with search", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "issues",
-
          params: { view: { resource: "list" }, search: "SEARCH" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.issues",
+
      seed,
+
      project: "PROJECT",
+
      search: "SEARCH",
    });
  });

-
  test("projects.issues.new", () => {
+
  test("projects.newIssue", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "issues",
-
          params: { view: { resource: "new" }, search: "" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.newIssue",
+
      seed,
+
      project: "PROJECT",
    });
  });

  test("projects.issue", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "issue",
-
          params: { issue: "ISSUE" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.issue",
+
      seed,
+
      project: "PROJECT",
+
      issue: "ISSUE",
    });
  });

  test("projects.patches"),
    () => {
      expectParsingInvariant({
-
        resource: "projects",
-
        params: {
-
          view: {
-
            resource: "patches",
-
            params: { view: { resource: "list" }, search: "" },
-
          },
-
          baseUrl,
-
          id: "PROJECT",
-
        },
+
        resource: "project.patches",
+
        seed,
+
        project: "PROJECT",
      });
    };

  test("projects.patches with search", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "patches",
-
          params: { view: { resource: "list" }, search: "SEARCH" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.patches",
+
      seed,
+
      project: "PROJECT",
+
      search: "SEARCH",
    });
  });

  test("projects.patch", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "patch",
-
          params: { patch: "PATCH", search: "" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.patch",
+
      seed,
+
      project: "PROJECT",
+
      patch: "PATCH",
+
      search: "",
    });
  });

  test("projects.patch with revision", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "patch",
-
          params: { patch: "PATCH", search: "", revision: "REVISION" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.patch",
+
      seed,
+
      project: "PROJECT",
+
      patch: "PATCH",
+
      search: "",
+
      revision: "REVISION",
    });
  });

  test("projects.patch with search", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "patch",
-
          params: { patch: "PATCH", search: "SEARCH" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.patch",
+
      seed,
+
      project: "PROJECT",
+
      patch: "PATCH",
+
      search: "SEARCH",
    });
  });

  test("projects.patch with revision and search", () => {
    expectParsingInvariant({
-
      resource: "projects",
-
      params: {
-
        view: {
-
          resource: "patch",
-
          params: { patch: "PATCH", search: "SEARCH", revision: "REVISION" },
-
        },
-
        baseUrl,
-
        id: "PROJECT",
-
      },
+
      resource: "project.patch",
+
      seed,
+
      project: "PROJECT",
+
      patch: "PATCH",
+
      revision: "REVISION",
+
      search: "SEARCH",
    });
  });

@@ -316,18 +244,14 @@ describe("pathToRoute", () => {
        dummyUrl,
      ),
      output: {
-
        resource: "projects",
-
        params: {
-
          view: { resource: "tree" },
-
          baseUrl: {
-
            hostname: "willow.radicle.garden",
-
            scheme: "http",
-
            port: 8080,
-
          },
-
          peer: undefined,
-
          id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
-
          route: "",
+
        resource: "project.tree",
+
        seed: {
+
          hostname: "willow.radicle.garden",
+
          scheme: "http",
+
          port: 8080,
        },
+
        project: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
+
        route: "",
      },
      description: "Seed Project Route w trailing slash",
    },
@@ -345,18 +269,14 @@ describe("pathToRoute", () => {
        dummyUrl,
      ),
      output: {
-
        resource: "projects",
-
        params: {
-
          view: { resource: "tree" },
-
          baseUrl: {
-
            hostname: "willow.radicle.garden",
-
            scheme: "http",
-
            port: 8080,
-
          },
-
          peer: undefined,
-
          id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
-
          route: "",
+
        resource: "project.tree",
+
        seed: {
+
          hostname: "willow.radicle.garden",
+
          scheme: "http",
+
          port: 8080,
        },
+
        project: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
+
        route: "",
      },
      description: "Seed Project Route w/o trailing slash",
    },