Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
radicle-explorer src views repos Issue.svelte
<script lang="ts">
  import type { BaseUrl, Issue, Repo } from "@http-client";

  import capitalize from "lodash/capitalize";
  import uniqBy from "lodash/uniqBy";

  import * as utils from "@app/lib/utils";

  import Assignees from "@app/views/repos/Cob/Assignees.svelte";
  import Badge from "@app/components/Badge.svelte";
  import CobHeader from "@app/views/repos/Cob/CobHeader.svelte";
  import Embeds from "@app/views/repos/Cob/Embeds.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
  import InlineTitle from "@app/views/repos/components/InlineTitle.svelte";
  import Labels from "@app/views/repos/Cob/Labels.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Markdown from "@app/components/Markdown.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import Reactions from "@app/components/Reactions.svelte";
  import Separator from "./Separator.svelte";
  import Share from "@app/views/repos/Share.svelte";
  import ThreadComponent from "@app/components/Thread.svelte";

  export let baseUrl: BaseUrl;
  export let issue: Issue;
  export let repo: Repo;
  export let rawPath: (commit?: string) => string;
  export let nodeAvatarUrl: string | undefined;

  $: uniqueEmbeds = uniqBy(
    issue.discussion.flatMap(comment => comment.embeds),
    "content",
  );
  $: threads = issue.discussion
    .filter(
      comment =>
        (comment.id !== issue.discussion[0].id && !comment.replyTo) ||
        comment.replyTo === issue.discussion[0].id,
    )
    .map(thread => {
      return {
        root: thread,
        replies: issue.discussion
          .filter(comment => comment.replyTo === thread.id)
          .sort((a, b) => a.timestamp - b.timestamp),
      };
    }, []);
  $: lastDescriptionEdit =
    issue.discussion[0].edits.length > 1
      ? issue.discussion[0].edits.at(-1)
      : undefined;
</script>

<style>
  .issue {
    display: flex;
    flex: 1;
    min-height: 100%;
  }
  .main {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    background-color: var(--color-surface-subtle);
  }
  .bottom {
    padding: 0 1rem 2.5rem 1rem;
    background-color: var(--color-surface-base);
    height: 100%;
    border-top: 1px solid var(--color-border-subtle);
  }
  .connector {
    width: 1px;
    height: 1.5rem;
    margin-left: 1.25rem;
    background-color: var(--color-border-subtle);
  }
  .metadata {
    display: flex;
    flex-direction: column;
    padding: 1rem;
    border-left: 1px solid var(--color-border-subtle);
    gap: 1.5rem;
    width: 20rem;
  }
  .threads {
    display: flex;
    flex-direction: column;
  }

  .author-metadata {
    color: var(--color-text-tertiary);
    font: var(--txt-body-m-regular);
  }
  .title {
    overflow: hidden;
    text-overflow: ellipsis;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font: var(--txt-heading-l);
    word-break: break-word;
  }
  .reactions {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin-left: -0.25rem;
  }
  @media (max-width: 719.98px) {
    .bottom {
      padding: 0;
    }
  }
</style>

<Layout
  {baseUrl}
  {nodeAvatarUrl}
  {repo}
  activeTab="issues"
  stylePaddingBottom="0">
  <svelte:fragment slot="breadcrumb">
    <Separator />
    <Link
      route={{
        resource: "repo.issues",
        repo: repo.rid,
        node: baseUrl,
      }}>
      Issues
    </Link>
    <Separator />
    <span class="txt-id">
      <div class="global-hide-on-small-desktop-down">
        {issue.id}
      </div>
      <div class="global-hide-on-medium-desktop-up">
        {utils.formatObjectId(issue.id)}
      </div>
    </span>
  </svelte:fragment>

  <div class="issue">
    <div class="main">
      <CobHeader>
        <svelte:fragment slot="title">
          <div style="display: flex; gap: 1rem; width: 100%;">
            {#if issue.title}
              <div class="title">
                <InlineTitle fontSize="heading-l" content={issue.title} />
              </div>
            {:else}
              <span style:color="var(--color-text-tertiary)">No title</span>
            {/if}
          </div>
          <Share />
        </svelte:fragment>
        <svelte:fragment slot="state">
          {#if issue.state.status === "open"}
            <Badge size="tiny" variant="open">
              <Icon name="issue" />
              {capitalize(issue.state.status)}
            </Badge>
          {:else}
            <Badge size="tiny" variant="merged">
              <Icon name="issue-closed" />
              {capitalize(issue.state.status)} as
              {issue.state.reason}
            </Badge>
          {/if}
          <NodeId
            {baseUrl}
            nodeId={issue.author.id}
            alias={issue.author.alias} />
          opened
          <Id id={issue.id} />
          <span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
            {utils.formatTimestamp(issue.discussion[0].timestamp)}
          </span>
          {#if lastDescriptionEdit}
            <div
              class="author-metadata"
              title={utils.formatEditedCaption(
                lastDescriptionEdit.author,
                lastDescriptionEdit.timestamp,
              )}>
              • edited
            </div>
          {/if}
        </svelte:fragment>
        <div slot="subtitle" class="global-hide-on-desktop-up">
          <div
            style:margin-top="2rem"
            style="display: flex; flex-direction: column; gap: 0.5rem;">
            <Assignees assignees={issue.assignees} {baseUrl} />
            <Labels labels={issue.labels} />
            <Embeds embeds={uniqueEmbeds} />
          </div>
        </div>
        <svelte:fragment slot="description">
          {#if issue.discussion[0].body}
            <Markdown
              breaks
              content={issue.discussion[0].body}
              rawPath={rawPath(
                repo.payloads["xyz.radicle.project"].meta.head,
              )} />
          {:else}
            <span style:color="var(--color-text-tertiary)">No description</span>
          {/if}
          <div class="reactions">
            {#if issue.discussion[0].reactions.length > 0}
              <Reactions reactions={issue.discussion[0].reactions} />
            {/if}
          </div>
        </svelte:fragment>
      </CobHeader>
      <div class="bottom">
        {#if threads.length > 0}
          <div class="connector"></div>
          <div class="threads">
            {#each threads as thread, i (thread.root.id)}
              <ThreadComponent
                {baseUrl}
                {thread}
                rawPath={rawPath(
                  repo.payloads["xyz.radicle.project"].meta.head,
                )} />
              {#if i < threads.length - 1}
                <div class="connector"></div>
              {/if}
            {/each}
          </div>
        {/if}
      </div>
    </div>
    <div class="metadata global-hide-on-medium-desktop-down">
      <Assignees assignees={issue.assignees} {baseUrl} />
      <Labels labels={issue.labels} />
      <Embeds embeds={uniqueEmbeds} />
    </div>
  </div>
</Layout>