Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add button to delete draft reviews
Merged did:key:z6Mki9XN...FvWF opened 9 months ago

Also introduces update functions for less error-prone local storage updates

For local storage updates to be persisted it had to be assigned for example with storage.value = storage.value. It was easy to forget this and it’s not immediately obvious why this is needed. (In fact, eslint complains.)

5 files changed +135 -70 ceca51c1 ea395ee8
modified src/components/Comment.svelte
@@ -163,7 +163,7 @@
        {/if}
        {#if deleteComment}
          <div class="icon-button">
-
            <Icon name="cross" onclick={deleteComment} />
+
            <Icon name="trash" onclick={deleteComment} />
          </div>
        {/if}
        {#if reactions && reactOnComment}
modified src/components/Icon.svelte
@@ -82,6 +82,7 @@
      | "stop"
      | "sun"
      | "thumb-up"
+
      | "trash"
      | "unresolve"
      | "user"
      | "warning";
@@ -1398,6 +1399,15 @@
    <path d="M7 6H8V7H7L7 6Z" />
    <path d="M7 10L8 10V11L7 11L7 10Z" />
    <path d="M7 12L8 12L8 13H7L7 12Z" />
+
  {:else if name === "trash"}
+
    <path d="M6.5 2L9.5 2V3L6.5 3V2Z" />
+
    <path d="M3.5 5H4.5L4.5 13H3.5L3.5 5Z" />
+
    <path d="M4.5 13L11.5 13V14L4.5 14V13Z" />
+
    <path d="M11.5 5L12.5 5L12.5 13H11.5L11.5 5Z" />
+
    <path d="M2.5 3L13.5 3V4L2.5 4L2.5 3Z" />
+
    <path d="M9.5 5H10.5L10.5 12L9.5 12V5Z" />
+
    <path d="M7.5 5L8.5 5V12H7.5L7.5 5Z" />
+
    <path d="M5.5 5H6.5L6.5 12H5.5L5.5 5Z" />
  {:else if name === "unresolve"}
    <path d="M7 11.5V12.5H6V11.5H7Z" />
    <path d="M8 10.5V11.5L7 11.5L7 10.5H8Z" />
modified src/components/Review.svelte
@@ -363,32 +363,46 @@
        {/if}
      </span>
      {#if "draft" in review}
-
        <div style:margin-inline-start="auto">
-
          <Button
-
            styleHeight="2.5rem"
-
            variant="secondary"
-
            title={canPublish
-
              ? undefined
-
              : "Add a summary or select a verdict to publish the review"}
-
            disabled={!canPublish || publishingInProgress}
-
            onclick={async () => {
-
              publishingInProgress = true;
-
              try {
-
                await draftReviewStorage.publish(review.id);
-
                await push({
-
                  resource: "repo.patch",
-
                  rid: repo.rid,
-
                  patch: patchId,
-
                  reviewId: undefined,
-
                  status: undefined,
-
                });
-
              } finally {
-
                publishingInProgress = false;
-
              }
-
            }}>
-
            <Icon name="checkout" />Publish
-
          </Button>
-
        </div>
+
        <div style:margin-inline-start="auto"></div>
+
        <Button
+
          styleHeight="2.5rem"
+
          variant="ghost"
+
          onclick={() => {
+
            draftReviewStorage.delete(review.id);
+
            void push({
+
              resource: "repo.patch",
+
              rid: repo.rid,
+
              patch: patchId,
+
              reviewId: undefined,
+
              status: undefined,
+
            });
+
          }}>
+
          <Icon name="trash" />Delete
+
        </Button>
+
        <Button
+
          styleHeight="2.5rem"
+
          variant="secondary"
+
          title={canPublish
+
            ? undefined
+
            : "Add a summary or select a verdict to publish the review"}
+
          disabled={!canPublish || publishingInProgress}
+
          onclick={async () => {
+
            publishingInProgress = true;
+
            try {
+
              await draftReviewStorage.publish(review.id);
+
              await push({
+
                resource: "repo.patch",
+
                rid: repo.rid,
+
                patch: patchId,
+
                reviewId: undefined,
+
                status: undefined,
+
              });
+
            } finally {
+
              publishingInProgress = false;
+
            }
+
          }}>
+
          <Icon name="checkout" />Publish
+
        </Button>
      {/if}
    </div>

modified src/lib/draftReviewStorage.ts
@@ -62,6 +62,8 @@ const draftReviewStoredSchema = z.object({
  ),
});

+
type DraftReviewStored = z.infer<typeof draftReviewStoredSchema>;
+

const storage = useLocalStorage(
  "repo.patches.draftReviews",
  z.record(z.string(), draftReviewStoredSchema),
@@ -89,19 +91,18 @@ export const draftReviewStorage = {
  },

  create(rid: string, revisionId: string): string {
-
    const draftReviews = storage.value;
-

    const id = crypto.randomUUID();
-
    draftReviews[id] = {
-
      id,
-
      rid,
-
      revision: revisionId,
-
      summary: "",
-
      labels: [],
-
      comments: [],
-
    };
-

-
    storage.value = draftReviews;
+
    storage.update(reviews => {
+
      reviews[id] = {
+
        id,
+
        rid,
+
        revision: revisionId,
+
        summary: "",
+
        labels: [],
+
        comments: [],
+
      };
+
      return reviews;
+
    });
    return id;
  },

@@ -109,20 +110,28 @@ export const draftReviewStorage = {
    id: string,
    props: { summary: string; verdict: Verdict | undefined; labels: string[] },
  ) {
-
    const draftPatches = storage.value;
-
    draftPatches[id].summary = props.summary;
-
    draftPatches[id].verdict = props.verdict;
-
    draftPatches[id].labels = props.labels;
-
    storage.value = draftPatches;
+
    updateStoredDraftReview(id, review => {
+
      return Object.assign(review, props);
+
    });
+
  },
+

+
  delete(id: string): DraftReviewStored | undefined {
+
    const review = storage.value[id];
+
    storage.update(reviews => {
+
      delete reviews[id];
+
      return reviews;
+
    });
+
    return review;
  },

  deleteComment(id: string, commentId: string) {
-
    const draftReviews = storage.value;
-
    const index = draftReviews[id].comments.findIndex(
-
      comment => comment.id === commentId,
-
    );
-
    draftReviews[id].comments.splice(index, 1);
-
    storage.value = draftReviews;
+
    updateStoredDraftReview(id, review => {
+
      const index = review.comments.findIndex(
+
        comment => comment.id === commentId,
+
      );
+
      review.comments.splice(index, 1);
+
      return review;
+
    });
  },

  addComment(
@@ -133,14 +142,15 @@ export const draftReviewStorage = {
      location: CodeLocation;
    },
  ): string {
-
    const draftPatches = storage.value;
    const commentId = crypto.randomUUID();
-
    draftPatches[id].comments.push({
-
      id: crypto.randomUUID(),
-
      body: comment.body,
-
      location: comment.location,
+
    updateStoredDraftReview(id, review => {
+
      review.comments.push({
+
        id: commentId,
+
        body: comment.body,
+
        location: comment.location,
+
      });
+
      return review;
    });
-
    storage.value = draftPatches;
    return commentId;
  },

@@ -152,20 +162,30 @@ export const draftReviewStorage = {
      embeds: Embed[];
    },
  ) {
-
    const draftPatches = storage.value;
-
    const storedComment = draftPatches[id].comments.find(
-
      comment => comment.id === commentId,
-
    );
-
    storedComment!.body = comment.body;
-
    storage.value = draftPatches;
+
    updateStoredDraftReview(id, review => {
+
      const storedComment = review.comments.find(
+
        comment => comment.id === commentId,
+
      );
+

+
      if (!storedComment) {
+
        throw new Error(
+
          `Comment ${commentId} does not exist for draft review ${id}`,
+
        );
+
      }
+

+
      storedComment!.body = comment.body;
+
      return review;
+
    });
  },

  async publish(id: string) {
-
    const draftReviewStored = storage.value[id];
-
    delete storage.value[id];
-
    // We need to explicitly persist the storage
-
    // eslint-disable-next-line no-self-assign
-
    storage.value = storage.value;
+
    const draftReviewStored = draftReviewStorage.delete(id);
+
    if (!draftReviewStored) {
+
      throw new Error(
+
        `Failed to publish draft review: Review ${id} does not exist`,
+
      );
+
    }
+

    await invoke<Patch>("create_patch_review", {
      args: {
        rid: draftReviewStored.rid,
@@ -182,6 +202,21 @@ export const draftReviewStorage = {
  },
};

+
function updateStoredDraftReview(
+
  id: string,
+
  update: (draftReviewStored: DraftReviewStored) => DraftReviewStored,
+
): void {
+
  storage.update(reviews => {
+
    const review = reviews[id];
+
    if (!review) {
+
      throw new Error(`Draft review ${id} does not exist`);
+
    }
+

+
    reviews[id] = update(review);
+
    return reviews;
+
  });
+
}
+

function draftReviewFromStored(
  draftReviewStored: z.infer<typeof draftReviewStoredSchema>,
  author: Author,
modified src/lib/useLocalStorage.svelte.ts
@@ -46,14 +46,20 @@ export default function useLocalStorage<
    }
  }

+
  function set(v: S) {
+
    value = v;
+
    if (!disableLocalStorage) localStorage.setItem(key, JSON.stringify(value));
+
  }
+

  return {
    get value() {
      return value;
    },
    set value(v: S) {
-
      value = v;
-
      if (!disableLocalStorage)
-
        localStorage.setItem(key, JSON.stringify(value));
+
      set(v);
+
    },
+
    update(fn: (v: S) => S) {
+
      set(fn(value));
    },
    clear() {
      value = initialValue;