Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor issue/patch side inputs
Rūdolfs Ošiņš committed 3 years ago
commit 7682d9e2150f59a9b3c7f1decb1f3a46259e5b98
parent 069b0949d554d7673a05b27b7ee8bf9ffb9c0688
11 files changed +297 -242
deleted src/lib/cobs.ts
@@ -1,39 +0,0 @@
-
import { parseNodeId } from "@app/lib/utils";
-

-
// Formats COBs Object Ids
-
export function formatObjectId(id: string): string {
-
  return id.substring(0, 11);
-
}
-

-
export function stripDidPrefix(array: string[]): string[] {
-
  return array.map(id => id.replace("did:key:", ""));
-
}
-

-
export function validateTag(
-
  value: string,
-
  items: string[],
-
): { success: false; error: string } | { success: true } {
-
  if (value.trim().length > 0) {
-
    if (items.includes(value)) {
-
      return { success: false, error: "This tag is already added" };
-
    } else {
-
      return { success: true };
-
    }
-
  }
-
  return { success: false, error: "This tag is not valid" };
-
}
-

-
export function validateAssignee(
-
  value: string,
-
  items: string[],
-
): { success: false; error: string } | { success: true } {
-
  const nodeId = parseNodeId(value);
-
  if (nodeId) {
-
    if (items.includes(`${nodeId.prefix}${nodeId.pubkey}`)) {
-
      return { success: false, error: "This assignee is already added" };
-
    } else {
-
      return { success: true };
-
    }
-
  }
-
  return { success: false, error: "This assignee is not valid" };
-
}
modified src/lib/utils.ts
@@ -301,3 +301,12 @@ export function createAddRemoveArrays(
    remove: currentArray.filter(item => !newArray.includes(item)),
  };
}
+

+
// Formats COBs Object Ids
+
export function formatObjectId(id: string): string {
+
  return id.substring(0, 11);
+
}
+

+
export function stripDidPrefix(array: string[]): string[] {
+
  return array.map(id => id.replace("did:key:", ""));
+
}
added src/views/projects/Cob/AssigneeInput.svelte
@@ -0,0 +1,122 @@
+
<script lang="ts" strictEvents>
+
  import { createEventDispatcher } from "svelte";
+

+
  import { formatNodeId, parseNodeId } from "@app/lib/utils";
+

+
  import Avatar from "@app/components/Avatar.svelte";
+
  import Button from "@app/components/Button.svelte";
+
  import Chip from "@app/components/Chip.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  const dispatch = createEventDispatcher<{ save: string[] }>();
+

+
  export let action: "create" | "edit" | "view";
+
  export let edit: boolean = false;
+
  export let assignees: string[] = [];
+

+
  let updatedAssignees: string[] = assignees;
+
  let inputValue = "";
+
  let validationMessage: string | undefined = undefined;
+

+
  $: parsedNodeId = parseNodeId(inputValue);
+

+
  function addAssignee() {
+
    if (parsedNodeId) {
+
      if (updatedAssignees.includes(parsedNodeId.pubkey)) {
+
        validationMessage = "This assignee is already added";
+
      } else {
+
        updatedAssignees = [...updatedAssignees, parsedNodeId.pubkey];
+
        inputValue = "";
+
        if (action === "create") {
+
          dispatch("save", updatedAssignees);
+
        }
+
      }
+
    } else {
+
      validationMessage = "This assignee is not valid";
+
    }
+
  }
+

+
  function removeAssignee({ detail: key }: { detail: number }) {
+
    updatedAssignees = updatedAssignees.filter((_, i) => i !== key);
+
    if (action === "create") {
+
      dispatch("save", updatedAssignees);
+
    }
+
  }
+
</script>
+

+
<style>
+
  .metadata-section {
+
    margin-bottom: 4rem;
+
  }
+
  .metadata-section-header {
+
    display: flex;
+
    gap: 1rem;
+
    align-items: center;
+
    font-size: var(--font-size-small);
+
    margin-bottom: 0.75rem;
+
    color: var(--color-foreground-5);
+
  }
+
  .metadata-section-body {
+
    display: flex;
+
    flex-wrap: wrap;
+
    flex-direction: row;
+
    gap: 0.5rem;
+
    margin-bottom: 1.25rem;
+
  }
+
  .metadata-section-empty {
+
    color: var(--color-foreground-6);
+
  }
+
</style>
+

+
<div class="metadata-section">
+
  <div class="metadata-section-header">
+
    <span>Assignees</span>
+
    {#if action === "edit"}
+
      {#if !edit}
+
        <Button
+
          size="tiny"
+
          variant="text"
+
          on:click={() => {
+
            edit = !edit;
+
          }}>
+
          edit
+
        </Button>
+
      {:else}
+
        <Button
+
          size="tiny"
+
          variant="text"
+
          on:click={() => {
+
            dispatch("save", updatedAssignees);
+
            edit = !edit;
+
          }}>
+
          save
+
        </Button>
+
      {/if}
+
    {/if}
+
  </div>
+
  <div class="metadata-section-body">
+
    {#each updatedAssignees as assignee, key (assignee)}
+
      <Chip
+
        on:remove={removeAssignee}
+
        removeable={edit || action === "create"}
+
        {key}>
+
        <Avatar inline nodeId={assignee} />
+
        <span>{formatNodeId(assignee)}</span>
+
      </Chip>
+
    {:else}
+
      <div class="metadata-section-empty">No assignees</div>
+
    {/each}
+
  </div>
+
  {#if edit || action === "create"}
+
    <div style:margin-bottom="1rem">
+
      <TextInput
+
        bind:value={inputValue}
+
        valid={Boolean(parsedNodeId)}
+
        placeholder="Add assignee"
+
        variant="form"
+
        {validationMessage}
+
        on:submit={addAssignee}
+
        on:input={() => (validationMessage = undefined)} />
+
    </div>
+
  {/if}
+
</div>
modified src/views/projects/Cob/CobHeader.svelte
@@ -1,8 +1,10 @@
<script lang="ts" strictEvents>
+
  import { createEventDispatcher } from "svelte";
+

+
  import { formatObjectId } from "@app/lib/utils";
+

  import Button from "@app/components/Button.svelte";
  import TextInput from "@app/components/TextInput.svelte";
-
  import { createEventDispatcher } from "svelte";
-
  import { formatObjectId } from "@app/lib/cobs";

  export let action: "create" | "edit" | "view" = "view";
  export let id: string | undefined = undefined;
deleted src/views/projects/Cob/CobSideInput.svelte
@@ -1,112 +0,0 @@
-
<script lang="ts" strictEvents>
-
  import Button from "@app/components/Button.svelte";
-
  import Chip from "@app/components/Chip.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
-
  import { createEventDispatcher, onMount } from "svelte";
-

-
  const dispatch = createEventDispatcher<{ save: string[] }>();
-

-
  export let action: "create" | "edit" | "view" = "view";
-
  export let title: string;
-
  export let edit: boolean = false;
-
  export let items: string[] = [];
-
  export let validate: (item: string) => boolean;
-
  export let validateAdd: (
-
    item: string,
-
    items: string[],
-
  ) => { success: false; error: string } | { success: true };
-
  export let placeholder: string;
-

-
  function toggleEdit() {
-
    edit = !edit;
-
  }
-

-
  function handleAdd() {
-
    const result = validateAdd(value, newItems);
-
    if (result.success) {
-
      newItems = [...newItems, value];
-
      value = "";
-
    } else {
-
      caption = result.error;
-
    }
-
  }
-

-
  onMount(() => {
-
    newItems = items;
-
  });
-

-
  let newItems: string[] = [];
-
  let value = "";
-
  let caption: string | undefined = undefined;
-
</script>
-

-
<style>
-
  .metadata-section {
-
    margin-bottom: 4rem;
-
  }
-
  .metadata-section-header {
-
    display: flex;
-
    gap: 1rem;
-
    align-items: center;
-
    font-size: var(--font-size-small);
-
    margin-bottom: 0.75rem;
-
    color: var(--color-foreground-5);
-
  }
-
  .metadata-section-body {
-
    display: flex;
-
    flex-wrap: wrap;
-
    flex-direction: row;
-
    gap: 0.5rem;
-
    margin-bottom: 1.25rem;
-
  }
-
  .metadata-section-empty {
-
    color: var(--color-foreground-6);
-
  }
-
</style>
-

-
<div class="metadata-section">
-
  <div class="metadata-section-header">
-
    <span>{title}</span>
-
    {#if action === "edit"}
-
      {#if !edit}
-
        <Button size="tiny" variant="text" on:click={toggleEdit}>edit</Button>
-
      {:else}
-
        <Button
-
          size="tiny"
-
          variant="text"
-
          on:click={() => {
-
            dispatch("save", newItems);
-
            toggleEdit();
-
          }}>
-
          save
-
        </Button>
-
      {/if}
-
    {/if}
-
  </div>
-
  <div class="metadata-section-body">
-
    {#each newItems as item, key (item)}
-
      <Chip
-
        on:remove={({ detail: key }) => {
-
          newItems = newItems.filter((_, i) => i !== key);
-
        }}
-
        removeable={edit || action === "create"}
-
        {key}>
-
        <slot {item} />
-
      </Chip>
-
    {:else}
-
      <div class="metadata-section-empty">No {title.toLowerCase()}</div>
-
    {/each}
-
  </div>
-
  {#if edit || action === "create"}
-
    <div style:margin-bottom="1rem">
-
      <TextInput
-
        bind:value
-
        valid={validate(value)}
-
        {placeholder}
-
        variant="form"
-
        validationMessage={caption}
-
        on:submit={handleAdd}
-
        on:input={() => (caption = undefined)} />
-
    </div>
-
  {/if}
-
</div>
added src/views/projects/Cob/TagInput.svelte
@@ -0,0 +1,122 @@
+
<script lang="ts" strictEvents>
+
  import { createEventDispatcher } from "svelte";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Chip from "@app/components/Chip.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  const dispatch = createEventDispatcher<{ save: string[] }>();
+

+
  export let action: "create" | "edit" | "view" = "view";
+
  export let edit: boolean = false;
+
  export let tags: string[] = [];
+

+
  let updatedTags: string[] = tags;
+
  let inputValue = "";
+
  let validationMessage: string | undefined = undefined;
+

+
  $: sanitizedValue = inputValue.trim();
+

+
  function addTag() {
+
    if (sanitizedValue.length > 0) {
+
      if (updatedTags.includes(sanitizedValue)) {
+
        validationMessage = "This tag is already added";
+
      } else {
+
        updatedTags = [...updatedTags, sanitizedValue];
+
        inputValue = "";
+
        if (action === "create") {
+
          dispatch("save", updatedTags);
+
        }
+
      }
+
    } else {
+
      validationMessage = "This tag is not valid";
+
    }
+
  }
+

+
  function removeTag({ detail: key }: { detail: number }) {
+
    updatedTags = updatedTags.filter((_, i) => i !== key);
+
    if (action === "create") {
+
      dispatch("save", updatedTags);
+
    }
+
  }
+
</script>
+

+
<style>
+
  .metadata-section {
+
    margin-bottom: 4rem;
+
  }
+
  .metadata-section-header {
+
    display: flex;
+
    gap: 1rem;
+
    align-items: center;
+
    font-size: var(--font-size-small);
+
    margin-bottom: 0.75rem;
+
    color: var(--color-foreground-5);
+
  }
+
  .metadata-section-body {
+
    display: flex;
+
    flex-wrap: wrap;
+
    flex-direction: row;
+
    gap: 0.5rem;
+
    margin-bottom: 1.25rem;
+
  }
+
  .metadata-section-empty {
+
    color: var(--color-foreground-6);
+
  }
+
  .tag {
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
  }
+
</style>
+

+
<div class="metadata-section">
+
  <div class="metadata-section-header">
+
    <span>Tags</span>
+
    {#if action === "edit"}
+
      {#if !edit}
+
        <Button
+
          size="tiny"
+
          variant="text"
+
          on:click={() => {
+
            edit = !edit;
+
          }}>
+
          edit
+
        </Button>
+
      {:else}
+
        <Button
+
          size="tiny"
+
          variant="text"
+
          on:click={() => {
+
            dispatch("save", updatedTags);
+
            edit = !edit;
+
          }}>
+
          save
+
        </Button>
+
      {/if}
+
    {/if}
+
  </div>
+
  <div class="metadata-section-body">
+
    {#each updatedTags as tag, key (tag)}
+
      <Chip
+
        on:remove={removeTag}
+
        removeable={edit || action === "create"}
+
        {key}>
+
        <div class="tag">{tag}</div>
+
      </Chip>
+
    {:else}
+
      <div class="metadata-section-empty">No tags</div>
+
    {/each}
+
  </div>
+
  {#if edit || action === "create"}
+
    <div style:margin-bottom="1rem">
+
      <TextInput
+
        bind:value={inputValue}
+
        valid={sanitizedValue.length > 0}
+
        placeholder="Add tag"
+
        variant="form"
+
        {validationMessage}
+
        on:submit={addTag}
+
        on:input={() => (validationMessage = undefined)} />
+
    </div>
+
  {/if}
+
</div>
modified src/views/projects/Issue.svelte
@@ -6,17 +6,15 @@

  import * as utils from "@app/lib/utils";
  import { HttpdClient } from "@httpd-client";
-
  import { parseNodeId, formatNodeId } from "@app/lib/utils";
  import { sessionStore } from "@app/lib/session";
-
  import { validateAssignee, validateTag } from "@app/lib/cobs";

+
  import AssigneeInput from "./Cob/AssigneeInput.svelte";
  import Authorship from "@app/components/Authorship.svelte";
-
  import Avatar from "@app/components/Avatar.svelte";
  import Badge from "@app/components/Badge.svelte";
  import Button from "@app/components/Button.svelte";
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
-
  import CobSideInput from "./Cob/CobSideInput.svelte";
  import CobStateButton from "@app/views/projects/Cob/CobStateButton.svelte";
+
  import TagInput from "./Cob/TagInput.svelte";
  import Textarea from "@app/components/Textarea.svelte";
  import Thread from "@app/components/Thread.svelte";

@@ -101,7 +99,11 @@
      await api.project.updateIssue(
        projectId,
        issue.id,
-
        { type: "tag", add, remove },
+
        {
+
          type: "tag",
+
          add,
+
          remove,
+
        },
        $sessionStore.id,
      );
      issue = await api.project.getIssueById(projectId, issue.id);
@@ -120,7 +122,11 @@
      await api.project.updateIssue(
        projectId,
        issue.id,
-
        { type: "assign", add, remove },
+
        {
+
          type: "assign",
+
          add: utils.stripDidPrefix(add),
+
          remove: utils.stripDidPrefix(remove),
+
        },
        $sessionStore.id,
      );
      issue = await api.project.getIssueById(projectId, issue.id);
@@ -176,10 +182,6 @@
    margin: 0 0 2.5rem 0;
    gap: 1rem;
  }
-
  .tag {
-
    overflow: hidden;
-
    text-overflow: ellipsis;
-
  }

  @media (max-width: 960px) {
    .issue {
@@ -256,30 +258,10 @@
    </div>
  </div>
  <div class="metadata">
-
    <CobSideInput
-
      {action}
-
      title="Assignees"
-
      placeholder="Add assignee"
-
      items={[...issue.assignees]}
-
      on:save={saveAssignees}
-
      validate={item => Boolean(parseNodeId(item))}
-
      validateAdd={(item, items) => validateAssignee(item, items)}>
-
      <svelte:fragment let:item>
-
        <Avatar inline nodeId={item} />
-
        <span>{formatNodeId(item)}</span>
-
      </svelte:fragment>
-
    </CobSideInput>
-
    <CobSideInput
+
    <AssigneeInput
      {action}
-
      title="Tags"
-
      placeholder="Add tag"
-
      items={[...issue.tags]}
-
      on:save={saveTags}
-
      validate={item => item.trim().length > 0}
-
      validateAdd={(item, items) => validateTag(item, items)}>
-
      <svelte:fragment let:item>
-
        <div class="tag">{item}</div>
-
      </svelte:fragment>
-
    </CobSideInput>
+
      assignees={issue.assignees}
+
      on:save={saveAssignees} />
+
    <TagInput {action} tags={issue.tags} on:save={saveTags} />
  </div>
</div>
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
  import type { Issue } from "@httpd-client";

-
  import { formatObjectId } from "@app/lib/cobs";
+
  import { formatObjectId, formatTimestamp } from "@app/lib/utils";
+

  import Authorship from "@app/components/Authorship.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import { formatTimestamp } from "@app/lib/utils";
  import Badge from "@app/components/Badge.svelte";
+
  import Icon from "@app/components/Icon.svelte";

  export let issue: Issue;

modified src/views/projects/Issue/New.svelte
@@ -8,16 +8,15 @@
  import * as utils from "@app/lib/utils";
  import { HttpdClient } from "@httpd-client";
  import { sessionStore } from "@app/lib/session";
-
  import { stripDidPrefix, validateTag } from "@app/lib/cobs";

+
  import AssigneeInput from "@app/views/projects/Cob/AssigneeInput.svelte";
  import AuthenticationErrorModal from "@app/views/session/AuthenticationErrorModal.svelte";
  import Authorship from "@app/components/Authorship.svelte";
-
  import Avatar from "@app/components/Avatar.svelte";
  import Badge from "@app/components/Badge.svelte";
  import Button from "@app/components/Button.svelte";
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
-
  import CobSideInput from "@app/views/projects/Cob/CobSideInput.svelte";
  import Comment from "@app/components/Comment.svelte";
+
  import TagInput from "@app/views/projects/Cob/TagInput.svelte";

  export let session: StoredSession;
  export let projectId: string;
@@ -25,10 +24,12 @@
  export let baseUrl: BaseUrl;

  const dispatch = createEventDispatcher<{ create: string }>();
-
  const action: "edit" | "view" =
-
    $sessionStore && utils.isLocal(baseUrl.hostname) ? "edit" : "view";
-

  let preview: boolean = false;
+
  let action: "create" | "view";
+
  $: action =
+
    $sessionStore && utils.isLocal(baseUrl.hostname) && !preview
+
      ? "create"
+
      : "view";

  let issueTitle = "";
  let issueText = "";
@@ -44,7 +45,7 @@
        {
          title: issueTitle,
          description: issueText,
-
          assignees: stripDidPrefix(assignees),
+
          assignees: utils.stripDidPrefix(assignees),
          tags: tags,
        },
        session.id,
@@ -105,7 +106,7 @@
<main>
  <div class="form">
    <div class="editor">
-
      <CobHeader action={preview ? "view" : "create"} bind:title={issueTitle}>
+
      <CobHeader {action} bind:title={issueTitle}>
        <svelte:fragment slot="state">
          <Badge variant="positive">open</Badge>
          <Authorship
@@ -120,7 +121,7 @@
          on:submit={createIssue}
          authorId={session.publicKey}
          timestamp={Date.now()}
-
          action={preview ? "view" : "create"}
+
          {action}
          rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)} />
      </div>
      <div class="actions">
@@ -144,28 +145,13 @@
      </div>
    </div>
    <div class="metadata">
-
      <CobSideInput
+
      <AssigneeInput
        {action}
-
        title="Assignees"
-
        placeholder="Add assignee"
-
        on:save={({ detail: assignees }) => (assignees = assignees)}
-
        validate={item => Boolean(utils.parseNodeId(item))}
-
        validateAdd={(item, items) => validateTag(item, items)}>
-
        <svelte:fragment let:item>
-
          <Avatar inline nodeId={item} />
-
          <span>{utils.formatNodeId(item)}</span>
-
        </svelte:fragment>
-
      </CobSideInput>
-
      <CobSideInput
-
        title="Tags"
-
        placeholder="Add tag"
-
        on:save={({ detail: tags }) => (tags = tags)}
-
        validate={item => item.trim().length > 0}
-
        validateAdd={(item, items) => validateTag(item, items)}>
-
        <svelte:fragment let:item>
-
          <div class="tag">{item}</div>
-
        </svelte:fragment>
-
      </CobSideInput>
+
        on:save={({ detail: updatedAssignees }) =>
+
          (assignees = updatedAssignees)} />
+
      <TagInput
+
        {action}
+
        on:save={({ detail: updatedTags }) => (tags = updatedTags)} />
    </div>
  </div>
</main>
modified src/views/projects/Patch.svelte
@@ -29,20 +29,19 @@
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
  import { HttpdClient } from "@httpd-client";
-
  import { formatObjectId, validateTag } from "@app/lib/cobs";
  import { sessionStore } from "@app/lib/session";

  import Authorship from "@app/components/Authorship.svelte";
  import Badge from "@app/components/Badge.svelte";
  import Changeset from "./SourceBrowser/Changeset.svelte";
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
-
  import CobSideInput from "./Cob/CobSideInput.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
  import Dropdown from "@app/components/Dropdown.svelte";
  import Floating from "@app/components/Floating.svelte";
  import HeaderToggleLabel from "./HeaderToggleLabel.svelte";
  import TabBar from "@app/components/TabBar.svelte";
+
  import TagInput from "./Cob/TagInput.svelte";
  import ThreadComponent from "@app/components/Thread.svelte";

  export let projectId: string;
@@ -182,10 +181,6 @@
  .highlight {
    color: var(--color-foreground-6);
  }
-
  .tag {
-
    overflow: hidden;
-
    text-overflow: ellipsis;
-
  }

  @media (max-width: 1092px) {
    .patch {
@@ -220,8 +215,8 @@
            <Dropdown
              items={enumeratedRevisions.map(([r, i]) => {
                return {
-
                  key: `Revision ${i} (${formatObjectId(r.id)})`,
-
                  title: `Revision ${i} (${formatObjectId(r.id)})`,
+
                  key: `Revision ${i} (${utils.formatObjectId(r.id)})`,
+
                  title: `Revision ${i} (${utils.formatObjectId(r.id)})`,
                  value: r.id,
                  badge: null,
                };
@@ -370,17 +365,6 @@
    {/if}
  </div>
  <div class="metadata">
-
    <CobSideInput
-
      {action}
-
      title="Tags"
-
      placeholder="Add tag"
-
      items={patch.tags}
-
      on:save={saveTags}
-
      validate={item => item.trim().length > 0}
-
      validateAdd={(item, items) => validateTag(item, items)}>
-
      <svelte:fragment let:item>
-
        <div class="tag">{item}</div>
-
      </svelte:fragment>
-
    </CobSideInput>
+
    <TagInput {action} tags={patch.tags} on:save={saveTags} />
  </div>
</div>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -3,8 +3,7 @@
  import type { Patch } from "@httpd-client";

  import { HttpdClient } from "@httpd-client";
-
  import { formatObjectId } from "@app/lib/cobs";
-
  import { formatTimestamp } from "@app/lib/utils";
+
  import { formatObjectId, formatTimestamp } from "@app/lib/utils";

  import Authorship from "@app/components/Authorship.svelte";
  import Badge from "@app/components/Badge.svelte";