Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add more patch write actions
Open rudolfs opened 1 year ago

See individual commits for more info.

check check-e2e

👉 Workflow runs 👉 Branch on GitHub

3 files changed +218 -13 74470eca ff8b133e
added src/components/PatchStateButton.svelte
@@ -0,0 +1,86 @@
+
<script lang="ts">
+
  import type { State } from "@bindings/cob/patch/State";
+

+
  import isEqual from "lodash/isEqual";
+

+
  import { closeFocused } from "@app/components/Popover.svelte";
+

+
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
+
  import DropdownList from "@app/components/DropdownList.svelte";
+
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+

+
  const {
+
    save,
+
    patchState,
+
  }: {
+
    save: (state: State) => Promise<void>;
+
    patchState: State;
+
  } = $props();
+

+
  const actions: { caption: string; state: State }[] = [
+
    {
+
      caption: "Reopen",
+
      state: { status: "open" },
+
    },
+
    { caption: "Convert to draft", state: { status: "draft" } },
+
    { caption: "Archive", state: { status: "archived" } },
+
  ];
+

+
  let selectedAction = $state(
+
    patchState.status === "open" ? actions[1] : actions[0],
+
  );
+

+
  // React to state changes that come from outside of this button.
+
  $effect(() => {
+
    selectedAction = patchState.status === "open" ? actions[1] : actions[0];
+
  });
+
</script>
+

+
<style>
+
  .main {
+
    display: flex;
+
    flex-direction: row;
+
    justify-content: center;
+
  }
+
</style>
+

+
<div class="main">
+
  <Button
+
    variant="secondary"
+
    flatRight
+
    onclick={() =>
+
      void save($state.snapshot(selectedAction["state"]) as State)}>
+
    {selectedAction["caption"]}
+
  </Button>
+

+
  <Popover
+
    popoverPadding="0"
+
    popoverPositionTop="2.5rem"
+
    popoverPositionRight="0">
+
    {#snippet toggle(onclick)}
+
      <Button flatLeft {onclick} variant="secondary">
+
        <Icon name="chevron-down" />
+
      </Button>
+
    {/snippet}
+
    {#snippet popover()}
+
      <Border variant="ghost">
+
        <DropdownList
+
          items={actions.filter(a => !isEqual(a.state, patchState))}>
+
          {#snippet item(action)}
+
            <DropdownListItem
+
              selected={isEqual(selectedAction, action)}
+
              onclick={() => {
+
                selectedAction = action;
+
                closeFocused();
+
              }}>
+
              {action.caption}
+
            </DropdownListItem>
+
          {/snippet}
+
        </DropdownList>
+
      </Border>
+
    {/snippet}
+
  </Popover>
+
</div>
modified src/views/repo/Issue.svelte
@@ -248,7 +248,7 @@
      }
      editingTitle = false;
    } catch (error) {
-
      console.error("Issue editing failed: ", error);
+
      console.error("Issue title editing failed: ", error);
    } finally {
      await reload();
    }
@@ -298,7 +298,7 @@
        issues[issueIndex].state = state;
      }
    } catch (error) {
-
      console.error("Editing reactions failed", error);
+
      console.error("Changing issue state failed", error);
    } finally {
      await reload();
    }
modified src/views/repo/Patch.svelte
@@ -34,8 +34,10 @@
  import LabelInput from "@app/components/LabelInput.svelte";
  import Layout from "./Layout.svelte";
  import PatchStateBadge from "@app/components/PatchStateBadge.svelte";
+
  import PatchStateButton from "@app/components/PatchStateButton.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";

  interface Props {
    repo: RepoInfo;
@@ -47,12 +49,22 @@
  }

  /* eslint-disable prefer-const */
-
  let { repo, patch, patches, revisions, config, status }: Props = $props();
+
  let {
+
    repo,
+
    patch,
+
    patches,
+
    revisions,
+
    config,
+
    status: initialStatus,
+
  }: Props = $props();
  /* eslint-enable prefer-const */

  let cursor = patches.cursor;
  let more = patches.more;
  let items = $state(patches.content);
+
  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" = $state("patch");
@@ -68,6 +80,8 @@
    patch.id;

    tab = "patch";
+
    editingTitle = false;
+
    updatedTitle = patch.title;
  });

  const project = $derived(repo.payloads["xyz.radicle.project"]!);
@@ -220,6 +234,53 @@
      await reload();
    }
  }
+

+
  async function saveState(state: Patch["state"]) {
+
    try {
+
      await invoke("edit_patch", {
+
        rid: repo.rid,
+
        cobId: patch.id,
+
        action: {
+
          type: "lifecycle",
+
          state,
+
        },
+
        opts: { announce: $nodeRunning && $announce },
+
      });
+
      if (initialStatus !== undefined) {
+
        status = state["status"];
+
      }
+
    } catch (error) {
+
      console.error("Changing patch state failed", error);
+
    } finally {
+
      await reload();
+
    }
+
  }
+

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

+
    try {
+
      await invoke("edit_patch", {
+
        rid: repo.rid,
+
        cobId: patch.id,
+
        action: {
+
          id,
+
          type: "edit",
+
          title,
+
          target: "delegates",
+
        },
+
        opts: { announce: $nodeRunning && $announce },
+
      });
+
      editingTitle = false;
+
    } catch (error) {
+
      console.error("Patch title editing failed: ", error);
+
    } finally {
+
      await reload();
+
    }
+
  }
</script>

<style>
@@ -230,9 +291,16 @@
    user-select: text;
    display: flex;
    align-items: center;
+
    justify-content: space-between;
    word-break: break-all;
    min-height: 40px;
  }
+
  .title-icons {
+
    display: flex;
+
    gap: 1rem;
+
    margin-left: 1rem;
+
    align-items: center;
+
  }
  .status {
    padding: 0;
    margin-right: 0.75rem;
@@ -320,17 +388,68 @@

  <div class="content">
    <div style:margin-bottom="0.5rem">
-
      <div class="title">
-
        <div
-
          class="global-counter status"
-
          style:color={patchStatusColor[patch.state.status]}
-
          style:background-color={patchStatusBackgroundColor[
-
            patch.state.status
-
          ]}>
-
          <Icon name="patch" />
+
      {#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" />
+
          </div>
+

+
          <TextInput
+
            valid={updatedTitle.trim().length > 0}
+
            bind:value={updatedTitle}
+
            autofocus
+
            onSubmit={async () => {
+
              if (updatedTitle.trim().length > 0) {
+
                await editTitle(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(patch.id, updatedTitle);
+
                }
+
              }} />
+
            <Icon
+
              name="cross"
+
              onclick={() => {
+
                updatedTitle = patch.title;
+
                editingTitle = !editingTitle;
+
              }} />
+
            <PatchStateButton patchState={patch.state} save={saveState} />
+
          </div>
        </div>
-
        <InlineTitle content={patch.title} fontSize="medium" />
-
      </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" />
+
            </div>
+
            <InlineTitle content={patch.title} fontSize="medium" />
+
          </div>
+
          {#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), patch.author.did, )}
+
            <div class="title-icons">
+
              <Icon name="pen" onclick={() => (editingTitle = !editingTitle)} />
+
              <PatchStateButton patchState={patch.state} save={saveState} />
+
            </div>
+
          {/if}
+
        </div>
+
      {/if}
    </div>

    <Border variant="ghost" styleGap="0">