Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add create issue view
Open rudolfs opened 1 year ago
10 files changed +488 -55 517d867f e5f90e20
modified src/App.svelte
@@ -12,6 +12,7 @@
  import { unreachable } from "@app/lib/utils";

  import AuthenticationError from "@app/views/AuthenticationError.svelte";
+
  import CreateIssue from "@app/views/repo/CreateIssue.svelte";
  import Home from "@app/views/Home.svelte";
  import Issue from "@app/views/repo/Issue.svelte";
  import Issues from "@app/views/repo/Issues.svelte";
@@ -63,6 +64,8 @@
  <!-- Don't show anything -->
{:else if $activeRouteStore.resource === "home"}
  <Home {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "repo.createIssue"}
+
  <CreateIssue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issue"}
  <Issue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issues"}
modified src/components/Border.svelte
@@ -154,7 +154,7 @@
  class="container"
  {onclick}
  role="button"
-
  tabindex="0"
+
  tabindex={onclick !== undefined ? 0 : -1}
  {style}
  style:min-height={styleMinHeight}
  style:height={styleHeight}>
modified src/components/Button.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
  export let variant: "primary" | "secondary" | "ghost";
  export let onclick: (() => void) | undefined = undefined;
+
  export let disabled: boolean = false;

  $: style =
    `--button-color-1: var(--color-fill-${variant});` +
@@ -112,96 +113,115 @@
    grid-area: p5-5;
  }

-
  .container:hover .p1-3 {
+
  .container:hover:not(.disabled) .p1-3 {
    background-color: var(--button-color-2);
  }

-
  .container:hover .p2-2 {
+
  .container:hover:not(.disabled) .p2-2 {
    background-color: var(--button-color-2);
  }
-
  .container:hover .p2-3 {
+
  .container:hover:not(.disabled) .p2-3 {
    background-color: var(--button-color-4);
  }
-
  .container:hover .p2-4 {
+
  .container:hover:not(.disabled) .p2-4 {
    background-color: var(--button-color-2);
  }

-
  .container:hover .p3-1 {
+
  .container:hover:not(.disabled) .p3-1 {
    background-color: var(--button-color-2);
  }
-
  .container:hover .p3-2 {
+
  .container:hover:not(.disabled) .p3-2 {
    background-color: var(--button-color-4);
  }
-
  .container:hover .p3-3 {
+
  .container:hover:not(.disabled) .p3-3 {
    background-color: var(--button-color-2);
  }
-
  .container:hover .p3-4 {
+
  .container:hover:not(.disabled) .p3-4 {
    background-color: var(--button-color-1);
  }
-
  .container:hover .p3-5 {
+
  .container:hover:not(.disabled) .p3-5 {
    background-color: var(--button-color-1);
  }

-
  .container:hover .p4-2 {
+
  .container:hover:not(.disabled) .p4-2 {
    background-color: var(--button-color-2);
  }
-
  .container:hover .p4-3 {
+
  .container:hover:not(.disabled) .p4-3 {
    background-color: var(--button-color-1);
  }
-
  .container:hover .p4-4 {
+
  .container:hover:not(.disabled) .p4-4 {
    background-color: var(--button-color-1);
  }

-
  .container:hover .p5-3 {
+
  .container:hover:not(.disabled) .p5-3 {
    background-color: var(--button-color-1);
  }

-
  .container:active .p1-3 {
+
  .container:active:not(.disabled) .p1-3 {
    background-color: var(--button-color-1);
  }

-
  .container:active .p2-2 {
+
  .container:active:not(.disabled) .p2-2 {
    background-color: var(--button-color-1);
  }
-
  .container:active .p2-3 {
+
  .container:active:not(.disabled) .p2-3 {
    background-color: var(--button-color-3);
  }
-
  .container:active .p2-4 {
+
  .container:active:not(.disabled) .p2-4 {
    background-color: var(--button-color-1);
  }

-
  .container:active .p3-1 {
+
  .container:active:not(.disabled) .p3-1 {
    background-color: var(--button-color-1);
  }
-
  .container:active .p3-2 {
+
  .container:active:not(.disabled) .p3-2 {
    background-color: var(--button-color-3);
  }
-
  .container:active .p3-3 {
+
  .container:active:not(.disabled) .p3-3 {
    background-color: var(--button-color-1);
  }
-
  .container:active .p3-4 {
+
  .container:active:not(.disabled) .p3-4 {
    background-color: var(--button-color-2);
  }
-
  .container:active .p3-5 {
+
  .container:active:not(.disabled) .p3-5 {
    background-color: var(--button-color-1);
  }

-
  .container:active .p4-2 {
+
  .container:active:not(.disabled) .p4-2 {
    background-color: var(--button-color-1);
  }
-
  .container:active .p4-3 {
+
  .container:active:not(.disabled) .p4-3 {
    background-color: var(--button-color-2);
  }
-
  .container:active .p4-4 {
+
  .container:active:not(.disabled) .p4-4 {
    background-color: var(--button-color-1);
  }
-
  .container:active .p5-3 {
+
  .container:active:not(.disabled) .p5-3 {
    background-color: var(--button-color-1);
  }

+
  .container.disabled {
+
    color: var(--color-foreground-disabled);
+
  }
+

+
  .disabled .p1-3,
+
  .disabled .p2-2,
+
  .disabled .p2-3,
+
  .disabled .p2-4,
+
  .disabled .p3-1,
+
  .disabled .p3-2,
+
  .disabled .p3-3,
+
  .disabled .p3-4,
+
  .disabled .p3-5,
+
  .disabled .p4-2,
+
  .disabled .p4-3,
+
  .disabled .p4-4,
+
  .disabled .p5-3 {
+
    background-color: var(--color-fill-ghost);
+
  }
+

  .container {
    height: 32px;
-
    cursor: pointer;
    white-space: nowrap;

    -webkit-touch-callout: none;
@@ -223,7 +243,14 @@
</style>

<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div class="container" {onclick} role="button" tabindex="0" {style}>
+
<div
+
  class="container"
+
  style:cursor={!disabled ? "pointer" : "default"}
+
  class:disabled
+
  onclick={!disabled ? onclick : undefined}
+
  role="button"
+
  tabindex="0"
+
  {style}>
  <div class="pixel p1-1"></div>
  <div class="pixel p1-2"></div>
  <div class="pixel p1-3"></div>
added src/components/TextInput.svelte
@@ -0,0 +1,75 @@
+
<script lang="ts">
+
  import { onMount } from "svelte";
+

+
  import Border from "./Border.svelte";
+

+
  export let name: string | undefined = undefined;
+
  export let placeholder: string | undefined = undefined;
+
  export let value: string | undefined = undefined;
+

+
  export let autofocus: boolean = false;
+
  export let autoselect: boolean = false;
+
  export let disabled: boolean = false;
+

+
  let inputElement: HTMLInputElement | undefined = undefined;
+
  let focussed = false;
+

+
  onMount(() => {
+
    if (inputElement === undefined) {
+
      return;
+
    }
+
    if (autofocus) {
+
      // We set preventScroll to true for Svelte animations to work.
+
      inputElement.focus({ preventScroll: true });
+
    }
+
    if (autoselect) {
+
      inputElement.select();
+
    }
+
  });
+
</script>
+

+
<style>
+
  input {
+
    background: var(--color-background-dip);
+
    font-family: inherit;
+
    font-size: var(--font-size-small);
+
    color: var(--color-foreground-contrast);
+
    line-height: 1.6;
+
    outline: none;
+
    text-overflow: ellipsis;
+
    width: 100%;
+
    height: 100%;
+
    margin: 0;
+
    height: 32px;
+
    padding: 0.25rem 0.75rem;
+
  }
+
  input::placeholder {
+
    font-family: var(--font-family-sans-serif);
+
    color: var(--color-foreground-dim);
+
    opacity: 1 !important;
+
  }
+
  input[disabled] {
+
    cursor: not-allowed;
+
  }
+
</style>
+

+
<Border variant={focussed ? "secondary" : "ghost"} styleWidth="100%">
+
  <input
+
    on:focus={() => {
+
      focussed = true;
+
    }}
+
    on:blur={() => {
+
      focussed = false;
+
    }}
+
    bind:this={inputElement}
+
    type="text"
+
    {name}
+
    {placeholder}
+
    {disabled}
+
    bind:value
+
    autocomplete="off"
+
    spellcheck="false"
+
    on:input
+
    on:click
+
    on:change />
+
</Border>
added src/components/Textarea.svelte
@@ -0,0 +1,107 @@
+
<script lang="ts">
+
  import { afterUpdate, beforeUpdate } from "svelte";
+

+
  import Border from "./Border.svelte";
+

+
  export let value: string | undefined = undefined;
+
  export let placeholder: string | undefined = undefined;
+
  export let focus: boolean = false;
+
  // If `false` we automatically grow the textarea height.
+
  // If `true` we show a resize handle on the lower right-hand side of the
+
  // textarea to allow resizing the textarea manually.
+
  export let resizable: boolean = false;
+

+
  // Defaulting selectionStart and selectionEnd to 0, since no full support yet.
+
  export let selectionStart: number = 0;
+
  export let selectionEnd: number = 0;
+

+
  let textareaElement: HTMLTextAreaElement | undefined = undefined;
+
  let focussed = false;
+

+
  // We either auto-grow the textarea, or allow the user to resize it. These
+
  // options are mutually exclusive because a user resized textarea would
+
  // automatically shrink upon text input otherwise.
+
  $: if (textareaElement && !resizable) {
+
    // React to changes to the textarea content.
+
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+
    value;
+

+
    // Reset height to 0px on every value change so that the textarea
+
    // immediately shrinks when all text is deleted.
+
    textareaElement.style.height = `0px`;
+
    textareaElement.style.height = `${textareaElement.scrollHeight}px`;
+
  }
+

+
  $: if (textareaElement && focus) {
+
    textareaElement.focus();
+
    focus = false;
+
  }
+

+
  beforeUpdate(() => {
+
    if (textareaElement) {
+
      ({ selectionStart, selectionEnd } = textareaElement);
+
    }
+
  });
+

+
  afterUpdate(() => {
+
    if (textareaElement && focus) {
+
      textareaElement.setSelectionRange(selectionStart, selectionEnd);
+
      textareaElement.focus();
+
    }
+
  });
+
</script>
+

+
<style>
+
  textarea {
+
    background-color: var(--color-background-dip);
+
    border: 0;
+
    color: var(--color-foreground-default);
+
    font-family: inherit;
+
    height: 5rem;
+
    padding: 0.75rem;
+
    width: 100%;
+
    min-height: 6.375rem;
+
    resize: none;
+
    overflow: hidden;
+
    outline: none;
+
  }
+

+
  .resizable {
+
    resize: vertical;
+
    overflow: scroll;
+
  }
+

+
  textarea::-webkit-scrollbar-corner {
+
    background-color: transparent;
+
  }
+

+
  textarea::-webkit-resizer {
+
    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAD9QTFRFAAAAZWZmZmZmZmVmZWVmwsLBwsLCZ2ZmwsPCZmdlZWZnwcLBZmZkYGJjw8LDwsPBZmZnZWZkZ2ZkwMDBWFtcNbXb2AAAABV0Uk5TAP///////////////////////1H/YDRrSAAAAFBJREFUeJxVjUESgCAMA2mqAoqK6P/f6kzjIXIos5NumpI8g5LbpJnNQvDl52mWUYTquqnXwstshpHaTi+o+hHXccoKmHVW9yvIxv218ntivmOYAWpLfqaRAAAAAElFTkSuQmCC);
+
    background-size: 7px;
+
    background-repeat: no-repeat;
+
    background-position: bottom 1px right 1px;
+
  }
+

+
  textarea::placeholder {
+
    color: var(--color-foreground-dim);
+
  }
+
</style>
+

+
<Border variant={focussed ? "secondary" : "ghost"} styleWidth="100%">
+
  <textarea
+
    tabindex="0"
+
    bind:this={textareaElement}
+
    bind:value
+
    aria-label="textarea-comment"
+
    class="txt-small"
+
    class:resizable
+
    {placeholder}
+
    on:change
+
    on:click
+
    on:input
+
    on:focus={() => (focussed = true)}
+
    on:blur={() => (focussed = false)}
+
    on:paste
+
    on:keypress>
+
  </textarea>
+
</Border>
modified src/lib/router.ts
@@ -123,6 +123,7 @@ export function routeToPath(route: Route): string {
  } else if (route.resource === "authenticationError") {
    return "/authenticationError";
  } else if (
+
    route.resource === "repo.createIssue" ||
    route.resource === "repo.issue" ||
    route.resource === "repo.issues" ||
    route.resource === "repo.patch" ||
modified src/lib/router/definitions.ts
@@ -1,19 +1,11 @@
import type { Config } from "@bindings/Config";
import type { RepoInfo } from "@bindings/RepoInfo";
-
import type {
-
  LoadedRepoIssueRoute,
-
  LoadedRepoIssuesRoute,
-
  LoadedRepoPatchRoute,
-
  LoadedRepoPatchesRoute,
-
  RepoIssueRoute,
-
  RepoIssuesRoute,
-
  RepoPatchRoute,
-
  RepoPatchesRoute,
-
} from "@app/views/repo/router";
+
import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router";

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

import {
+
  loadCreateIssue,
  loadIssue,
  loadIssues,
  loadPatch,
@@ -45,19 +37,13 @@ export type Route =
  | AuthenticationErrorRoute
  | BootingRoute
  | HomeRoute
-
  | RepoIssueRoute
-
  | RepoIssuesRoute
-
  | RepoPatchRoute
-
  | RepoPatchesRoute;
+
  | RepoRoute;

export type LoadedRoute =
  | AuthenticationErrorRoute
  | BootingRoute
  | LoadedHomeRoute
-
  | LoadedRepoIssueRoute
-
  | LoadedRepoIssuesRoute
-
  | LoadedRepoPatchRoute
-
  | LoadedRepoPatchesRoute;
+
  | LoadedRepoRoute;

export async function loadRoute(
  route: Route,
@@ -69,6 +55,8 @@ export async function loadRoute(
    return { resource: "home", params: { repos, config } };
  } else if (route.resource === "repo.issue") {
    return loadIssue(route);
+
  } else if (route.resource === "repo.createIssue") {
+
    return loadCreateIssue(route);
  } else if (route.resource === "repo.issues") {
    return loadIssues(route);
  } else if (route.resource === "repo.patch") {
added src/views/repo/CreateIssue.svelte
@@ -0,0 +1,166 @@
+
<script lang="ts">
+
  import type { Author } from "@bindings/Author";
+
  import type { Config } from "@bindings/Config";
+
  import type { Issue } from "@bindings/Issue";
+
  import type { RepoInfo } from "@bindings/RepoInfo";
+

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

+
  import { issueStatusColor } from "@app/lib/utils";
+
  import * as router from "@app/lib/router";
+

+
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.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 OutlineButton from "@app/components/OutlineButton.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+
  import Textarea from "@app/components/Textarea.svelte";
+

+
  export let repo: RepoInfo;
+
  export let issues: Issue[];
+
  export let config: Config;
+

+
  let title: string = "";
+
  let description: string = "";
+

+
  const labels: string[] = [];
+
  const assignees: Author[] = [];
+
  const embeds: { name: string; content: string }[] = [];
+

+
  async function createIssue() {
+
    const response: Issue = await invoke("create_issue", {
+
      rid: repo.rid,
+
      new: { title, description, labels, assignees, embeds },
+
    });
+
    void router.push({
+
      resource: "repo.issue",
+
      rid: repo.rid,
+
      issue: response.id,
+
    });
+
  }
+

+
  $: 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;
+
  }
+
  .issue-teaser {
+
    max-width: 11rem;
+
    white-space: nowrap;
+
  }
+
  .issue-list {
+
    margin-top: 0.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
    padding-bottom: 1rem;
+
  }
+
  .content {
+
    padding: 0 1rem 1rem 1rem;
+
  }
+
</style>
+

+
<Layout>
+
  <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.issues", rid: repo.rid, status: "open" }}>
+
      <div class="global-flex">
+
        <Icon name="chevron-right" />
+
        {project.data.name}
+
      </div>
+
    </Link>
+
    <Icon name="chevron-right" />
+
    <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+
      Issues
+
    </Link>
+
    <Icon name="chevron-right" />
+
    New Issue
+
  </svelte:fragment>
+

+
  <svelte:fragment slot="sidebar">
+
    <Border
+
      hoverable={false}
+
      variant="ghost"
+
      styleWidth="100%"
+
      styleHeight="32px">
+
      <div style:margin-left="0.5rem">
+
        <Icon name="issue" />
+
      </div>
+
      <span class="txt-small txt-semibold">Issues</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.issues.open + project.meta.issues.closed}
+
        </div>
+
      </div>
+
    </Border>
+

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

+
  <div class="content">
+
    <div class="title">
+
      <TextInput placeholder="Title" autofocus bind:value={title} />
+
    </div>
+
    <Textarea placeholder="Description" bind:value={description} />
+
    <div
+
      class="global-flex"
+
      style:justify-content="flex-end"
+
      style:margin-top="1.5rem">
+
      <OutlineButton
+
        variant="ghost"
+
        onclick={() => {
+
          window.history.back();
+
        }}>
+
        Cancel
+
      </OutlineButton>
+
      <Button
+
        variant="ghost"
+
        disabled={title.length === 0}
+
        onclick={createIssue}>
+
        Save
+
      </Button>
+
    </div>
+
  </div>
+
</Layout>
modified src/views/repo/Issues.svelte
@@ -4,6 +4,8 @@
  import type { IssueStatus } from "./router";
  import type { RepoInfo } from "@bindings/RepoInfo";

+
  import * as router from "@app/lib/router";
+

  import Layout from "./Layout.svelte";

  import Border from "@app/components/Border.svelte";
@@ -13,6 +15,7 @@
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import RepoHeader from "@app/components/RepoHeader.svelte";
+
  import Button from "@app/components/Button.svelte";

  export let repo: RepoInfo;
  export let issues: Issue[];
@@ -29,6 +32,14 @@
    gap: 2px;
    padding: 0 1rem 1rem 1rem;
  }
+
  .header {
+
    font-weight: var(--font-weight-medium);
+
    font-size: var(--font-size-medium);
+
    display: flex;
+
    padding: 0 1rem 1rem 1rem;
+
    align-items: center;
+
    justify-content: space-between;
+
  }
</style>

<Layout>
@@ -111,7 +122,21 @@
      </Link>
    </div>
  </svelte:fragment>
-

+
  <div class="header">
+
    <div>Issues</div>
+
    <div class="txt-regular txt-semibold">
+
      <Button
+
        variant="secondary"
+
        onclick={() => {
+
          void router.push({
+
            resource: "repo.createIssue",
+
            rid: repo.rid,
+
          });
+
        }}>
+
        <Icon name="plus" />New
+
      </Button>
+
    </div>
+
  </div>
  <div class="list">
    {#each issues as issue}
      <IssueTeaser {issue} rid={repo.rid} />
modified src/views/repo/router.ts
@@ -16,6 +16,11 @@ export interface RepoIssueRoute {
  issue: string;
}

+
export interface RepoCreateIssueRoute {
+
  resource: "repo.createIssue";
+
  rid: string;
+
}
+

export interface LoadedRepoIssueRoute {
  resource: "repo.issue";
  params: {
@@ -26,6 +31,15 @@ export interface LoadedRepoIssueRoute {
  };
}

+
export interface LoadedRepoCreateIssueRoute {
+
  resource: "repo.createIssue";
+
  params: {
+
    repo: RepoInfo;
+
    config: Config;
+
    issues: Issue[];
+
  };
+
}
+

export interface RepoIssuesRoute {
  resource: "repo.issues";
  rid: string;
@@ -78,11 +92,13 @@ export interface LoadedRepoPatchesRoute {
}

export type RepoRoute =
+
  | RepoCreateIssueRoute
  | RepoIssueRoute
  | RepoIssuesRoute
  | RepoPatchRoute
  | RepoPatchesRoute;
export type LoadedRepoRoute =
+
  | LoadedRepoCreateIssueRoute
  | LoadedRepoIssueRoute
  | LoadedRepoIssuesRoute
  | LoadedRepoPatchRoute
@@ -131,6 +147,24 @@ export async function loadPatches(
  };
}

+
export async function loadCreateIssue(
+
  route: RepoCreateIssueRoute,
+
): Promise<LoadedRepoCreateIssueRoute> {
+
  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: "all",
+
  });
+

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

export async function loadIssue(
  route: RepoIssueRoute,
): Promise<LoadedRepoIssueRoute> {
@@ -177,6 +211,9 @@ export function repoRouteToPath(route: RepoRoute): string {
  if (route.resource === "repo.issue") {
    const url = [...pathSegments, "issues", route.issue].join("/");
    return url;
+
  } else if (route.resource === "repo.createIssue") {
+
    const url = [...pathSegments, "issues", "create"].join("/");
+
    return url;
  } else if (route.resource === "repo.issues") {
    let url = [...pathSegments, "issues"].join("/");
    const searchParams = new URLSearchParams();
@@ -210,13 +247,17 @@ export function repoUrlToRoute(

  if (rid) {
    if (resource === "issues") {
-
      const id = segments.shift();
-
      if (id) {
-
        return {
-
          resource: "repo.issue",
-
          rid,
-
          issue: id,
-
        };
+
      const idOrAction = segments.shift();
+
      if (idOrAction) {
+
        if (idOrAction === "create") {
+
          return { resource: "repo.createIssue", rid };
+
        } else {
+
          return {
+
            resource: "repo.issue",
+
            rid,
+
            issue: idOrAction,
+
          };
+
        }
      } else {
        const status = searchParams.get("status");
        if (status === "open" || status === "closed") {