Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Extract issue and patch titles into EditableTitle
Open rudolfs opened 1 year ago

This also improves the UX making the title clickable.

check check-e2e

👉 Workflow runs 👉 Branch on GitHub

4 files changed +185 -239 40684169 724132ab
added src/components/EditableTitle.svelte
@@ -0,0 +1,110 @@
+
<script lang="ts">
+
  import Icon from "@app/components/Icon.svelte";
+
  import InlineTitle from "@app/components/InlineTitle.svelte";
+
  import NakedButton from "@app/components/NakedButton.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  interface Props {
+
    allowedToEdit: true | undefined;
+
    cobId: string;
+
    title: string;
+
    updateTitle: (newTitle: string) => Promise<void>;
+
  }
+

+
  const { allowedToEdit, cobId, title, updateTitle }: Props = $props();
+

+
  let editingTitle = $state(false);
+
  let newTitle = $state("");
+

+
  $effect(() => {
+
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+
    cobId;
+

+
    editingTitle = false;
+
    newTitle = title;
+
  });
+

+
  async function save() {
+
    if (newTitle.trim().length <= 0) {
+
      return;
+
    }
+

+
    if (title === newTitle) {
+
      editingTitle = false;
+
      return;
+
    }
+

+
    await updateTitle(newTitle);
+
  }
+
</script>
+

+
<style>
+
  .title {
+
    font-size: var(--font-size-medium);
+
    font-weight: var(--font-weight-medium);
+
    -webkit-user-select: text;
+
    user-select: text;
+
    display: flex;
+
    align-items: center;
+
    word-break: break-word;
+
    min-height: 2.5rem;
+
    width: 100%;
+
  }
+
  .edit-title-icon {
+
    display: none;
+
  }
+
  .title-wrapper:hover .edit-title-icon {
+
    display: flex;
+
  }
+
</style>
+

+
<div class="title">
+
  {#if editingTitle}
+
    <TextInput
+
      valid={newTitle.trim().length > 0}
+
      bind:value={newTitle}
+
      autofocus
+
      onSubmit={save}
+
      onDismiss={() => {
+
        newTitle = title;
+
        editingTitle = false;
+
      }} />
+
    <div class="global-flex" style:margin-left="0.5rem">
+
      <NakedButton
+
        variant="ghost"
+
        styleHeight="2.5rem"
+
        disabled={!(newTitle.trim().length > 0)}
+
        onclick={save}>
+
        <Icon name="checkmark" />
+
      </NakedButton>
+
      <NakedButton
+
        variant="ghost"
+
        styleHeight="2.5rem"
+
        onclick={() => {
+
          newTitle = title;
+
          editingTitle = false;
+
        }}>
+
        <Icon name="cross" />
+
      </NakedButton>
+
    </div>
+
  {:else}
+
    <div class="global-flex" style:gap="0">
+
      <!-- svelte-ignore a11y_click_events_have_key_events -->
+
      <div
+
        class="title-wrapper global-flex"
+
        role="button"
+
        style:cursor={allowedToEdit ? "pointer" : "default"}
+
        onclick={() => {
+
          if (allowedToEdit) {
+
            editingTitle = true;
+
          }
+
        }}
+
        tabindex="0">
+
        <InlineTitle content={title} fontSize="medium" />
+
        {#if allowedToEdit}
+
          <div class="edit-title-icon"><Icon name="pen" /></div>
+
        {/if}
+
      </div>
+
    </div>
+
  {/if}
+
</div>
modified src/components/Reviews.svelte
@@ -143,7 +143,7 @@
                ? "You already published a review"
                : undefined}
              onclick={async () => {
-
                createReview("reject");
+
                await createReview("reject");
                await loadPatch();
                closeFocused();
              }}>
@@ -161,7 +161,7 @@
                ? "You already published a review"
                : undefined}
              onclick={async () => {
-
                createReview("accept");
+
                await createReview("accept");
                await loadPatch();
                closeFocused();
              }}>
modified src/views/repo/Issue.svelte
@@ -27,15 +27,14 @@
  import CommentComponent from "@app/components/Comment.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Discussion from "@app/components/Discussion.svelte";
+
  import EditableTitle from "@app/components/EditableTitle.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
  import IssueStateButton from "@app/components/IssueStateButton.svelte";
  import IssueTimeline from "@app/components/IssueTimeline.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
  import NakedButton from "@app/components/NakedButton.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";

  import Layout from "./Layout.svelte";

@@ -63,8 +62,6 @@

  let issues = $state(initialIssues);
  let status = $state(initialStatus);
-
  let editingTitle = $state(false);
-
  let updatedTitle = $state("");
  let labelSaveInProgress: boolean = $state(false);
  let assigneesSaveInProgress: boolean = $state(false);
  let hideTimeline = $state(true);
@@ -78,8 +75,6 @@
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    issue.id;

-
    editingTitle = false;
-
    updatedTitle = issue.title;
    hideTimeline = true;
  });

@@ -152,8 +147,6 @@
        id: issue.id,
      }),
    ]);
-

-
    editingTitle = false;
  }

  async function createComment(
@@ -200,29 +193,23 @@
    }
  }

-
  async function editTitle(id: string, title: string) {
-
    if (issue.title === updatedTitle) {
-
      editingTitle = false;
-
      return;
-
    }
-

+
  async function updateTitle(newTitle: string) {
    try {
      await invoke("edit_issue", {
        rid: repo.rid,
        cobId: issue.id,
        action: {
          type: "edit",
-
          id,
-
          title,
+
          id: issue.id,
+
          title: newTitle,
        },
        opts: { announce: $nodeRunning && $announce },
      });
      // Update second column issue title without reloading the whole issue list.
      const issueIndex = issues.findIndex(i => i.id === issue.id);
      if (issueIndex !== -1) {
-
        issues[issueIndex].title = updatedTitle;
+
        issues[issueIndex].title = newTitle;
      }
-
      editingTitle = false;
    } catch (error) {
      console.error("Issue title editing failed: ", error);
    } finally {
@@ -282,20 +269,8 @@
</script>

<style>
-
  .title {
-
    font-size: var(--font-size-medium);
-
    font-weight: var(--font-weight-medium);
-
    -webkit-user-select: text;
-
    user-select: text;
-
    display: flex;
-
    align-items: center;
-
    justify-content: space-between;
-
    word-break: break-word;
-
    min-height: 2.5rem;
-
  }
  .status {
    padding: 0;
-
    margin-right: 0.75rem;
    height: 2.5rem;
    width: 2.5rem;
  }
@@ -318,12 +293,6 @@
  .content {
    padding: 1rem 1rem 1rem 0;
  }
-
  .title-icons {
-
    display: flex;
-
    gap: 1rem;
-
    margin-left: 1rem;
-
    align-items: center;
-
  }
  .metadata-divider {
    width: 2px;
    background-color: var(--color-fill-ghost);
@@ -367,74 +336,26 @@
  {/snippet}

  <div class="content">
-
    <div style:margin-bottom="1rem">
-
      {#if editingTitle}
-
        <div class="title">
-
          <div
-
            class="global-counter status"
-
            style:color={issueStatusColor[issue.state.status]}
-
            style:background-color={issueStatusBackgroundColor[
-
              issue.state.status
-
            ]}>
-
            {#if issue.state.status === "open"}
-
              <Icon name="issue" />
-
            {:else}
-
              <Icon name="issue-closed" />
-
            {/if}
-
          </div>
-
          <TextInput
-
            valid={updatedTitle.trim().length > 0}
-
            bind:value={updatedTitle}
-
            autofocus
-
            onSubmit={async () => {
-
              if (updatedTitle.trim().length > 0) {
-
                await editTitle(issue.id, updatedTitle);
-
              }
-
            }}
-
            onDismiss={() => {
-
              updatedTitle = issue.title;
-
              editingTitle = !editingTitle;
-
            }} />
-
          <div class="title-icons">
-
            <Icon
-
              name="checkmark"
-
              onclick={async () => {
-
                if (updatedTitle.trim().length > 0) {
-
                  await editTitle(issue.id, updatedTitle);
-
                }
-
              }} />
-
            <Icon
-
              name="cross"
-
              onclick={() => {
-
                updatedTitle = issue.title;
-
                editingTitle = !editingTitle;
-
              }} />
-
          </div>
-
        </div>
-
      {:else}
-
        <div class="title">
-
          <div class="global-flex" style:gap="0">
-
            <div
-
              class="global-counter status"
-
              style:color={issueStatusColor[issue.state.status]}
-
              style:background-color={issueStatusBackgroundColor[
-
                issue.state.status
-
              ]}>
-
              {#if issue.state.status === "open"}
-
                <Icon name="issue" />
-
              {:else}
-
                <Icon name="issue-closed" />
-
              {/if}
-
            </div>
-
            <InlineTitle content={issue.title} fontSize="medium" />
-
          </div>
-
          {#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), issue.body.author.did, )}
-
            <div class="title-icons">
-
              <Icon name="pen" onclick={() => (editingTitle = !editingTitle)} />
-
            </div>
-
          {/if}
-
        </div>
-
      {/if}
+
    <div class="global-flex" style:margin-bottom="1rem" style:gap="0.75rem">
+
      <div
+
        class="global-counter status"
+
        style:color={issueStatusColor[issue.state.status]}
+
        style:background-color={issueStatusBackgroundColor[issue.state.status]}>
+
        {#if issue.state.status === "open"}
+
          <Icon name="issue" />
+
        {:else}
+
          <Icon name="issue-closed" />
+
        {/if}
+
      </div>
+
      <EditableTitle
+
        {updateTitle}
+
        allowedToEdit={roles.isDelegateOrAuthor(
+
          config.publicKey,
+
          repo.delegates.map(delegate => delegate.did),
+
          issue.body.author.did,
+
        )}
+
        title={issue.title}
+
        cobId={issue.id} />
    </div>

    <Border variant="ghost" styleGap="0">
modified src/views/repo/Patch.svelte
@@ -34,7 +34,6 @@
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
@@ -51,6 +50,7 @@
  import Sidebar from "@app/components/Sidebar.svelte";
  import Tab from "@app/components/Tab.svelte";
  import TextInput from "@app/components/TextInput.svelte";
+
  import EditableTitle from "@app/components/EditableTitle.svelte";

  interface Props {
    repo: RepoInfo;
@@ -83,8 +83,6 @@

  let patches = $state(initialPatches);
  let status = $state(initialStatus);
-
  let editingTitle = $state(false);
-
  let updatedTitle = $state("");
  let labelSaveInProgress: boolean = $state(false);
  let assigneesSaveInProgress: boolean = $state(false);
  let tab: "patch" | "revisions" | "timeline" = $state(
@@ -97,8 +95,6 @@
    patch.id;

    tab = revisions.length > 1 ? "revisions" : "patch";
-
    editingTitle = false;
-
    updatedTitle = patch.title;
    selectedRevision = revisions.slice(-1)[0];
  });

@@ -117,25 +113,19 @@
  });
  const project = $derived(repo.payloads["xyz.radicle.project"]!);

-
  async function editTitle(rid: string, patchId: string, title: string) {
-
    if (patch.title === updatedTitle) {
-
      editingTitle = false;
-
      return;
-
    }
-

+
  async function updateTitle(newTitle: string) {
    try {
      await invoke("edit_patch", {
-
        rid,
-
        cobId: patchId,
+
        rid: repo.rid,
+
        cobId: patch.id,
        action: {
-
          id: patchId,
+
          id: patch.id,
          type: "edit",
-
          title,
+
          title: newTitle,
          target: "delegates",
        },
        opts: { announce: $nodeRunning && $announce },
      });
-
      editingTitle = false;
    } catch (error) {
      console.error("Editing title failed: ", error);
    } finally {
@@ -308,26 +298,8 @@
</script>

<style>
-
  .title {
-
    font-size: var(--font-size-medium);
-
    font-weight: var(--font-weight-medium);
-
    -webkit-user-select: text;
-
    user-select: text;
-
    display: flex;
-
    align-items: center;
-
    justify-content: space-between;
-
    word-break: break-word;
-
    min-height: 2.5rem;
-
  }
-
  .title-icons {
-
    display: flex;
-
    gap: 1rem;
-
    margin-left: 1rem;
-
    align-items: center;
-
  }
  .status {
    padding: 0;
-
    margin-right: 0.75rem;
    height: 2.5rem;
    width: 2.5rem;
  }
@@ -567,105 +539,48 @@
      }} />
  {:else}
    <div class="content">
-
      <div style:margin-bottom="1rem">
-
        {#if editingTitle}
-
          <div class="title">
-
            <div
-
              class="global-counter status"
-
              style:color={patchStatusColor[patch.state.status]}
-
              style:background-color={patchStatusBackgroundColor[
-
                patch.state.status
-
              ]}>
-
              <Icon
-
                name={patch.state.status === "open"
-
                  ? "patch"
-
                  : `patch-${patch.state.status}`} />
-
            </div>
-

-
            <TextInput
-
              valid={updatedTitle.trim().length > 0}
-
              bind:value={updatedTitle}
-
              autofocus
-
              onSubmit={async () => {
-
                if (updatedTitle.trim().length > 0) {
-
                  await editTitle(repo.rid, patch.id, updatedTitle);
-
                }
-
              }}
-
              onDismiss={() => {
-
                updatedTitle = patch.title;
-
                editingTitle = !editingTitle;
-
              }} />
-
            <div class="title-icons">
-
              <Icon
-
                name="checkmark"
-
                onclick={async () => {
-
                  if (updatedTitle.trim().length > 0) {
-
                    await editTitle(repo.rid, patch.id, updatedTitle);
-
                  }
-
                }} />
-
              <Icon
-
                name="cross"
-
                onclick={() => {
-
                  updatedTitle = patch.title;
-
                  editingTitle = !editingTitle;
-
                }} />
-
            </div>
-
          </div>
-
        {:else}
-
          <div class="title">
-
            <div class="global-flex" style:gap="0">
-
              <div
-
                class="global-counter status"
-
                style:color={patchStatusColor[patch.state.status]}
-
                style:background-color={patchStatusBackgroundColor[
-
                  patch.state.status
-
                ]}>
-
                <Icon
-
                  name={patch.state.status === "open"
-
                    ? "patch"
-
                    : `patch-${patch.state.status}`} />
-
              </div>
-
              <InlineTitle content={patch.title} fontSize="medium" />
-
            </div>
-
            <div
-
              class="global-flex txt-small"
-
              style:margin-left="auto"
-
              style:z-index="40"
-
              style:gap="0.75rem">
-
              {#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), patch.author.did, )}
-
                <div class="title-icons">
-
                  <Icon
-
                    name="pen"
-
                    onclick={() => (editingTitle = !editingTitle)} />
-
                </div>
-
              {/if}
-

-
              <Popover
-
                bind:expanded={checkoutPopoverExpanded}
-
                popoverPositionRight="0"
-
                popoverPositionTop="2.5rem">
-
                {#snippet toggle(onclick)}
-
                  <Button styleHeight="2rem" variant="secondary" {onclick}>
-
                    <Icon name="checkout" />Checkout<Icon name="chevron-down" />
-
                  </Button>
-
                {/snippet}
-
                {#snippet popover()}
-
                  <Border
-
                    styleAlignItems="flex-start"
-
                    styleBackgroundColor="var(--color-background-float)"
-
                    styleFlexDirection="column"
-
                    styleGap="0.5rem"
-
                    stylePadding="1rem"
-
                    styleWidth="max-content"
-
                    variant="ghost">
-
                    To checkout this patch in your working copy, run:
-
                    <Command command={checkoutCommand} styleWidth="100%" />
-
                  </Border>
-
                {/snippet}
-
              </Popover>
-
            </div>
-
          </div>
-
        {/if}
+
      <div class="global-flex" style:margin-bottom="1rem" style:gap="0.75rem">
+
        <div
+
          class="global-counter status"
+
          style:color={patchStatusColor[patch.state.status]}
+
          style:background-color={patchStatusBackgroundColor[
+
            patch.state.status
+
          ]}>
+
          <Icon
+
            name={patch.state.status === "open"
+
              ? "patch"
+
              : `patch-${patch.state.status}`} />
+
        </div>
+
        <EditableTitle
+
          {updateTitle}
+
          allowedToEdit={true}
+
          title={patch.title}
+
          cobId={patch.id} />
+
        <div style:margin-left="auto">
+
          <Popover
+
            bind:expanded={checkoutPopoverExpanded}
+
            popoverPositionRight="0"
+
            popoverPositionTop="2.5rem">
+
            {#snippet toggle(onclick)}
+
              <Button styleHeight="2rem" variant="secondary" {onclick}>
+
                <Icon name="checkout" />Checkout<Icon name="chevron-down" />
+
              </Button>
+
            {/snippet}
+
            {#snippet popover()}
+
              <Border
+
                styleAlignItems="flex-start"
+
                styleBackgroundColor="var(--color-background-float)"
+
                styleFlexDirection="column"
+
                styleGap="0.5rem"
+
                stylePadding="1rem"
+
                styleWidth="max-content"
+
                variant="ghost">
+
                To checkout this patch in your working copy, run:
+
                <Command command={checkoutCommand} styleWidth="100%" />
+
              </Border>
+
            {/snippet}
+
          </Popover>
+
        </div>
      </div>
      <Border variant="ghost" styleGap="0">
        <div class="metadata-section" style:min-width="8rem">