Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
radicle-desktop src modals CreateIssue.svelte
<script lang="ts">
  import type { Author } from "@bindings/cob/Author";
  import type { Issue } from "@bindings/cob/issue/Issue";
  import type { Embed } from "@bindings/cob/thread/Embed";
  import type { Config } from "@bindings/config/Config";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import { nodeRunning } from "@app/lib/events";
  import { invoke } from "@app/lib/invoke";
  import { disableHide, enableHide, forceHide, hide } from "@app/lib/modal";
  import * as roles from "@app/lib/roles";
  import * as router from "@app/lib/router";

  import { announce } from "@app/components/AnnounceSwitch.svelte";
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
  import Button from "@app/components/Button.svelte";
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import Icon from "@app/components/Icon.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
  import RepoAvatar from "@app/components/RepoAvatar.svelte";
  import TextInput from "@app/components/TextInput.svelte";

  interface Props {
    repo: RepoInfo;
  }

  const { repo }: Props = $props();

  let preview = $state(false);
  let title = $state("");
  let body = $state("");
  let assignees: Author[] = $state([]);
  let labels: string[] = $state([]);

  $effect(() => {
    const isDirty =
      title.trim() !== "" ||
      body.trim() !== "" ||
      labels.length > 0 ||
      assignees.length > 0;
    if (isDirty) {
      disableHide();
    } else {
      enableHide();
    }
  });

  const configPromise = invoke<Config>("config");

  async function createIssue(description: string, embeds: Embed[]) {
    return invoke<Issue>("create_issue", {
      rid: repo.rid,
      new: {
        title,
        description,
        labels: $state.snapshot(labels),
        assignees: $state.snapshot(assignees.map(a => a.did)),
        embeds,
      },
      opts: { announce: $nodeRunning && $announce },
    });
  }
</script>

<style>
  .modal {
    width: 56rem;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--border-radius-lg);
    background-color: var(--color-surface-canvas);
    overflow: hidden;
  }
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 1.5rem;
    height: 3.25rem;
    flex-shrink: 0;
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .header-left {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font: var(--txt-body-m-regular);
    color: var(--color-text-secondary);
    min-width: 0;
  }
  .repo-name {
    font: var(--txt-body-m-semibold);
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .title {
    font: var(--txt-body-m-regular);
    color: var(--color-text-primary);
    white-space: nowrap;
  }
  .body {
    padding: 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
  .title-preview {
    font: var(--txt-heading-s);
    color: var(--color-text-primary);
    padding: 0.5rem 0;
  }
  .metadata-section {
    padding: 0.5rem;
    font: var(--txt-body-m-regular);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    height: 100%;
  }
</style>

<div class="modal">
  <div class="header">
    <div class="header-left">
      <RepoAvatar
        name={repo.payloads["xyz.radicle.project"]?.data.name ?? ""}
        rid={repo.rid}
        styleWidth="1rem" />
      <span class="repo-name">
        {repo.payloads["xyz.radicle.project"]?.data.name}
      </span>
      <Icon name="chevron-right" />
      <span class="title">New issue</span>
    </div>
    <Button variant="naked" onclick={forceHide}>
      <span style:color="var(--color-text-tertiary)">
        <Icon name="close" />
      </span>
    </Button>
  </div>
  <div class="body">
    {#if preview}
      <div class="title-preview">
        {#if title.trim().length === 0}
          <span class="txt-missing">No title.</span>
        {:else}
          {title}
        {/if}
      </div>
    {:else}
      <TextInput
        placeholder="Title"
        autofocus
        onDismiss={hide}
        bind:value={title} />
    {/if}

    <ExtendedTextarea
      textAreaSize="fixed-height"
      disableSubmit={title.trim() === ""}
      disallowEmptyBody
      styleMinHeight="20rem"
      submitVariant="secondary"
      submitCaption="Save"
      hideDiscard
      close={hide}
      submit={async ({ comment, embeds }) => {
        try {
          const response = await createIssue(
            comment,
            Array.from(embeds.values()),
          );
          await router.push({
            resource: "repo.issue",
            rid: repo.rid,
            issue: response.id,
            status: "open",
          });
          forceHide();
        } catch {
          console.error("Not able to create issue.");
        }
      }}
      rid={repo.rid}
      bind:preview
      bind:body
      borderVariant="ghost"
      placeholder="Description">
      {#snippet belowTextarea()}
        {#await configPromise then config}
          {#if !!roles.isDelegate( config.publicKey, repo.delegates.map(d => d.did), )}
            <div
              style:display="flex"
              style:align-items="center"
              style:width="100%">
              <div class="metadata-section" style:flex="1">
                <LabelInput
                  allowedToEdit={true}
                  {preview}
                  {labels}
                  submitInProgress={false}
                  save={newLabels => {
                    labels = newLabels;
                  }} />
              </div>
              <div class="metadata-section" style:flex="1">
                <AssigneeInput
                  allowedToEdit={true}
                  {preview}
                  bind:assignees
                  submitInProgress={false}
                  save={newAssignees => {
                    assignees = newAssignees;
                  }} />
              </div>
            </div>
          {/if}
        {/await}
      {/snippet}
    </ExtendedTextarea>
  </div>
</div>