Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor project header modals
Rūdolfs Ošiņš committed 3 years ago
commit 65bf2ae38678caf6e7aa444f9a24bf4efd61f7f8
parent b47c83e25a4867d5c01fdd6d9752c65a953a49b2
12 files changed +198 -153
modified cypress/e2e/project.spec.ts
@@ -90,7 +90,7 @@ describe("Project view", () => {
    cy.get("div.stat.contributor-count").should("have.text", "1 contributor(s)");
    cy.get("div.stat.branch").should("have.class", "not-allowed").should("have.text", "main");
    cy.get("div.hash.desktop").should("have.text", "56e4e02");
-
    cy.get("div.clone").click();
+
    cy.get("div.clone-button").click();
  });

  it("Peer selector", () => {
modified src/Dropdown.svelte
@@ -3,7 +3,6 @@

  export let items: { key: string; value: string; badge: string | null }[];
  export let selected: string | null = null;
-
  export let visible = false;

  const dispatch = createEventDispatcher();
  const onSelect = (item: string) => {
@@ -32,16 +31,14 @@
  }
</style>

-
{#if visible}
-
  <div class="dropdown">
-
    {#each items as {key, value, badge}}
-
      {#if key && value}
-
        <div class="dropdown-item" class:selected={value === selected} on:click={() => onSelect(value)} title={value}>{@html key}
-
          {#if badge}
-
            <span class="badge primary">{badge}</span>
-
          {/if}
-
        </div>
-
      {/if}
-
    {/each}
-
  </div>
-
{/if}
+
<div class="dropdown">
+
  {#each items as {key, value, badge}}
+
    {#if key && value}
+
      <div class="dropdown-item" class:selected={value === selected} on:click={() => onSelect(value)} title={value}>{@html key}
+
        {#if badge}
+
          <span class="badge primary">{badge}</span>
+
        {/if}
+
      </div>
+
    {/if}
+
  {/each}
+
</div>
added src/Floating.svelte
@@ -0,0 +1,46 @@
+
<script lang="ts" context="module">
+
  import { writable } from "svelte/store";
+
  const focused = writable<HTMLDivElement | undefined>(undefined);
+

+
  export function closeFocused() {
+
    focused.set(undefined);
+
  }
+
</script>
+

+
<script lang="ts">
+
  export let disabled = false;
+

+
  let expanded = false;
+
  let thisComponent: HTMLDivElement;
+

+
  function clickOutside(ev: MouseEvent) {
+
    if (! $focused?.contains(ev.target as HTMLDivElement)) {
+
      closeFocused();
+
    }
+
  }
+

+
  function toggle() {
+
    if (! disabled) {
+
      expanded = !expanded;
+
      if ($focused === thisComponent) {
+
        closeFocused();
+
      } else {
+
        focused.set(thisComponent);
+
      }
+
    }
+
  }
+

+
  $: expanded = $focused === thisComponent;
+
</script>
+

+
<svelte:window on:click={clickOutside} />
+

+
<div bind:this={thisComponent}>
+
  <div on:click={toggle}>
+
    <slot name="toggle" />
+
  </div>
+

+
  {#if expanded}
+
    <slot name="modal" />
+
  {/if}
+
</div>
modified src/Header.svelte
@@ -11,6 +11,7 @@
  import { Profile, ProfileType } from "@app/profile";
  import Avatar from '@app/Avatar.svelte';
  import Search from '@app/Search.svelte';
+
  import Floating from "@app/Floating.svelte";
  import Icon from "./Icon.svelte";
  import MobileNavbar from "./MobileNavbar.svelte";
  import SeedDropdown from "./SeedDropdown.svelte";
@@ -20,11 +21,6 @@

  let sessionButtonHover = false;
  let mobileNavbarDisplayed = false;
-
  let seedDropdown = false;
-

-
  function toggleDropdown() {
-
    seedDropdown = !seedDropdown;
-
  }

  function toggleNavbar() {
    mobileNavbarDisplayed = !mobileNavbarDisplayed;
@@ -175,10 +171,14 @@

      {#if session && Object.keys(session.siwe).length > 0}
        <span class="seeds-container">
-
          <span class="nav-link" on:click={toggleDropdown}>
-
            Seeds
-
          </span>
-
          <SeedDropdown seeds={session.siwe} visible={seedDropdown} {config} />
+
          <Floating>
+
            <span slot="toggle" class="nav-link">
+
              Seeds
+
            </span>
+
            <svelte:fragment slot="modal">
+
              <SeedDropdown seeds={session.siwe} {config} />
+
            </svelte:fragment>
+
          </Floating>
        </span>
      {/if}
    </div>
modified src/SeedDropdown.svelte
@@ -4,9 +4,9 @@
  import type { Config } from "@app/config";
  import Dropdown from "@app/Dropdown.svelte";
  import type { SeedSession } from "@app/siwe";
+
  import { closeFocused } from "@app/Floating.svelte";

  export let seeds: { [key: string]: SeedSession };
-
  export let visible = false;
  export let config: Config;

  // When a user signs into a new seed we want to update the seed listing
@@ -24,9 +24,8 @@
  <Dropdown
    {items}
    selected={null}
-
    {visible}
    on:select={(item) => {
-
      visible = false;
+
      closeFocused();
      navigate(`/seeds/${item.detail}`);
    }}
  />
modified src/base/projects/BranchSelector.spec.ts
@@ -18,7 +18,6 @@ const defaultProps = {
  },
  branches: { "master": "e678629cd37c770c640a2cd997fc76303c815772" },
  revision: "e678629cd37c770c640a2cd997fc76303c815772",
-
  toggleDropdown: () => "branch"
};

describe('Logic', () => {
@@ -61,9 +60,9 @@ describe('Logic', () => {
          "feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
          "xyz": "debf82ef3623ec11751a993bda85bac2ff1c6f00",
        },
-
        branchesDropdown: true
      }
    });
+
    cy.get("div.commit div.stat.branch").click();
    cy.get("div.dropdown div.dropdown-item")
      .first()
      .should("contain.text", "feature-branch")
@@ -148,19 +147,21 @@ describe("Events", () => {
      props: {
        ...defaultProps,
        revision: "feature-branch",
-
        branchesDropdown: true,
        branches: {
          "feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
          "xyz": "debf82ef3623ec11751a993bda85bac2ff1c6f00",
        }
      }
    });
-
    const branchLabel = getByText("xyz");

-
    const mock = cy.spy();
-
    component.$on("branchChanged", mock);
+
    cy.get("div.commit div.stat.branch").click().then(() => {
+
      const branchLabel = getByText("xyz");

-
    fireEvent.click(branchLabel);
-
    expect(mock).to.have.been.calledOnce;
+
      const mock = cy.spy();
+
      component.$on("branchChanged", mock);
+

+
      fireEvent.click(branchLabel);
+
      expect(mock).to.have.been.calledOnce;
+
    });
  });
});
modified src/base/projects/BranchSelector.svelte
@@ -3,12 +3,11 @@
  import { ProjectInfo, Branches, getOid } from "@app/project";
  import { formatCommit } from "@app/utils";
  import Dropdown from "@app/Dropdown.svelte";
+
  import Floating from "@app/Floating.svelte";

  export let branches: Branches;
  export let project: ProjectInfo;
  export let revision: string;
-
  export let toggleDropdown: (input: string) => void;
-
  export let branchesDropdown = false;

  const dispatch = createEventDispatcher();
  const switchBranch = (name: string) => {
@@ -43,6 +42,7 @@
    color: var(--color-secondary);
    background-color: var(--color-secondary-background);
    border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
+
    user-select: none;
  }
  .commit .branch.not-allowed {
    cursor: not-allowed;
@@ -71,20 +71,20 @@
  <!-- Check for branches listing feature -->
  {#if branchList.length > 0}
    {#if branchLabel}
-
      <span>
+
      <Floating disabled={!showSelector}>
        <div
+
          slot="toggle"
          class="stat branch"
-
          class:not-allowed={!showSelector}
-
          on:click={() => showSelector && toggleDropdown("branch")}
-
        >
+
          class:not-allowed={!showSelector}>
          {branchLabel}
        </div>
-
        <Dropdown
-
          items={branchList}
-
          selected={branchLabel}
-
          visible={branchesDropdown}
-
          on:select={(e) => switchBranch(e.detail)} />
-
      </span>
+
        <svelte:fragment slot="modal">
+
          <Dropdown
+
            items={branchList}
+
            selected={branchLabel}
+
            on:select={(e) => switchBranch(e.detail)} />
+
        </svelte:fragment>
+
      </Floating>
      <div class="hash desktop">
        {formatCommit(commit)}
      </div>
added src/base/projects/CloneButton.svelte
@@ -0,0 +1,71 @@
+
<script lang="ts">
+
  import * as utils from "@app/utils";
+
  import Input from "@app/Input.svelte";
+
  import Floating from "@app/Floating.svelte";
+

+
  export let seedHost: string;
+
  export let urn: string;
+
</script>
+

+
<style>
+
  .clone-button {
+
    background-color: var(--color-yellow-background);
+
    border-radius: var(--border-radius-small);
+
    color: var(--color-yellow);
+
    cursor: pointer;
+
    font-family: var(--font-family-monospace);
+
    min-width: max-content;
+
    padding: 0.5rem 0.75rem;
+
    user-select: none;
+
  }
+
  .clone-button:hover {
+
    background-color: var(--color-foreground-background-lighter);
+
  }
+
  .dropdown {
+
    padding: 1rem;
+
    width: 24rem;
+
  }
+
  @media (max-width: 720px) {
+
    .dropdown {
+
      width: auto;
+
      left: 2rem;
+
      right: 2rem;
+
      z-index: 10;
+
    }
+
  }
+
  label {
+
    color: var(--color-foreground-faded);
+
    display: block;
+
    font-size: 0.75rem;
+
    padding: 0.5rem 0.5rem 0 0.25rem;
+
  }
+
</style>
+

+
<Floating>
+
  <div slot="toggle" class="clone-button">
+
    Clone
+
  </div>
+
  <svelte:fragment slot="modal">
+
    <div class="dropdown">
+
      <Input
+
        name="rad-clone-url"
+
        value="rad clone rad://{seedHost}/{utils.parseRadicleId(urn)}"
+
        class="yellow"
+
        clipboard />
+
      <label for="rad-clone-url">
+
        Use the <a
+
          target="_blank"
+
          href="https://radicle.network/get-started.html"
+
          class="link">Radicle CLI</a> to clone this project.
+
      </label>
+
      <br />
+
      <Input
+
        name="git-clone-url"
+
        value="https://{seedHost}/{utils.parseRadicleId(urn)}.git"
+
        class="yellow"
+
        clipboard />
+
      <label for="git-clone-url"
+
        >Use Git to clone this repository from the URL above.</label>
+
    </div>
+
  </svelte:fragment>
+
</Floating>
modified src/base/projects/Header.svelte
@@ -1,13 +1,12 @@
<script lang="ts">
  import type { Writable } from "svelte/store";
  import { navigate } from "svelte-routing";
-
  import * as utils from "@app/utils";
  import { Browser, ProjectContent, Project } from "@app/project";
  import AnchorBadge from "@app/base/profiles/AnchorBadge.svelte";
  import BranchSelector from "@app/base/projects/BranchSelector.svelte";
+
  import CloneButton from "@app/base/projects/CloneButton.svelte";
  import PeerSelector from "@app/base/projects/PeerSelector.svelte";
  import type { Tree } from "@app/project";
-
  import Input from "@app/Input.svelte";

  export let project: Project;
  export let tree: Tree;
@@ -21,14 +20,6 @@
  $: revision = browser.revision || commit;
  $: content = browser.content;

-
  let dropdownState: { [key: string]: boolean } = { clone: false, seed: false, branch: false, peer: false };
-
  function toggleDropdown(input: string) {
-
    Object.keys(dropdownState).map((key: string) => {
-
      if (input === key) dropdownState[key] = !dropdownState[key];
-
      else dropdownState[key] = false;
-
    });
-
  }
-

  // Switches between project views.
  const toggleContent = (input: ProjectContent, keepSourceInPath: boolean) => {
    project.navigateTo({
@@ -40,14 +31,13 @@
  };

  const updatePeer = (peer: string) => {
-
    dropdownState.peer = false;
    project.navigateTo({ peer, revision: null });
  };

  const updateRevision = (revision: string) => {
-
    dropdownState.branch = false;
    project.navigateTo({ revision });
  };
+

</script>

<style>
@@ -80,29 +70,6 @@
  .not-allowed.widget {
    color: var(--color-foreground-faded);
  }
-
  .clone {
-
    color: var(--color-yellow);
-
    background-color: var(--color-yellow-background);
-
    font-family: var(--font-family-monospace);
-
    padding: 0.5rem 0.75rem;
-
  }
-
  .dropdown {
-
    padding: 1rem;
-
    display: none;
-
  }
-
  .dropdown label {
-
    display: block;
-
    color: var(--color-foreground-faded);
-
    padding: 0.5rem 0.5rem 0 0.25rem;
-
    font-size: 0.75rem;
-
  }
-
  .clone-dropdown {
-
    width: 24rem;
-
  }
-
  .clone-dropdown.clone-dropdown-visible {
-
    position: absolute;
-
    display: block;
-
  }
  .stat {
    font-family: var(--font-family-monospace);
    padding: 0.5rem 0.75rem;
@@ -122,23 +89,13 @@
      margin-bottom: 1.5rem;
    }
  }
-
  @media (max-width: 720px) {
-
    .dropdown {
-
      width: auto;
-
      left: 2rem;
-
      right: 2rem;
-
      z-index: 10;
-
    }
-
  }
</style>

<header>
  {#if peers.length > 0}
    <PeerSelector
      {peers}
-
      {toggleDropdown}
      peer={browser.peer}
-
      bind:peersDropdown={dropdownState.peer}
      on:peerChanged={(event) => updatePeer(event.detail)} />
  {/if}

@@ -146,8 +103,6 @@
    {branches}
    {project}
    {revision}
-
    {toggleDropdown}
-
    bind:branchesDropdown={dropdownState.branch}
    on:branchChanged={(event) => updateRevision(event.detail)} />

  {#if !noAnchor}
@@ -161,36 +116,7 @@
  {/if}

  {#if seed.git.host}
-
    <span>
-
      <div
-
        class="clone clickable widget"
-
        on:click={() => toggleDropdown("clone")}>
-
        Clone
-
      </div>
-
      <div
-
        class="dropdown clone-dropdown"
-
        class:clone-dropdown-visible={dropdownState.clone}>
-
        <Input
-
          name="rad-clone-url"
-
          value="rad clone rad://{seed.git.host}/{utils.parseRadicleId(urn)}"
-
          class="yellow"
-
          clipboard />
-
        <label for="rad-clone-url">
-
          Use the <a
-
            target="_blank"
-
            href="https://radicle.network/get-started.html"
-
            class="link">Radicle CLI</a> to clone this project.
-
        </label>
-
        <br />
-
        <Input
-
          name="git-clone-url"
-
          value="https://{seed.git.host}/{utils.parseRadicleId(urn)}.git"
-
          class="yellow"
-
          clipboard />
-
        <label for="git-clone-url"
-
          >Use Git to clone this repository from the URL above.</label>
-
      </div>
-
    </span>
+
    <CloneButton seedHost={seed.git.host} {urn}/>
  {/if}
  <span>
    {#if seed.api.host}
modified src/base/projects/Patch/PatchTabBar.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
  import Dropdown from "@app/Dropdown.svelte";
+
  import Floating from "@app/Floating.svelte";
  import { PatchTab, Revision } from "@app/patch";
  import { formatCommit, formatTimestamp } from "@app/utils";
  import { createEventDispatcher } from "svelte";
@@ -23,11 +24,8 @@
  }));

  const onRevisionChange = ({ detail }: { detail: string }) => {
-
    showSelector = false;
    dispatch("revisionChanged", detail);
  };
-

-
  let showSelector = false;
</script>

<style>
@@ -82,16 +80,20 @@
      Changeset
    </div>
    <div class="revision-toggle">
-
      <button
-
        class:tab={revisions.length > 1}
-
        class="text-small revision-toggle"
-
        disabled={revisions.length <= 1}
-
        on:click={() => showSelector = !showSelector}>
-
        {formatRevisionName(revisions[revisionNumber], revisionNumber)}
-
      </button>
-
      <Dropdown
-
        items={revisionList} selected={revisionNumber.toString()} visible={showSelector}
-
        on:select={onRevisionChange} />
+
      <Floating disabled={revisions.length <= 1}>
+
        <button
+
          slot="toggle"
+
          class:tab={revisions.length > 1}
+
          class="text-small revision-toggle"
+
          disabled={revisions.length <= 1}>
+
          {formatRevisionName(revisions[revisionNumber], revisionNumber)}
+
        </button>
+
        <svelte:fragment slot="modal">
+
          <Dropdown
+
            items={revisionList} selected={revisionNumber.toString()}
+
            on:select={onRevisionChange} />
+
        </svelte:fragment>
+
      </Floating>
    </div>
  </div>
</div>
modified src/base/projects/PeerSelector.spec.ts
@@ -11,7 +11,6 @@ const defaultProps = {
      "delegate": true
    }
  ],
-
  toggleDropdown: () => console.log("toggle"),
};

describe('Logic', function () {
@@ -57,8 +56,9 @@ describe('Logic', function () {
describe("Layout", () => {
  it("should highlight the current peer", () => {
    render(PeerSelector, {
-
      props: { ...defaultProps, peersDropdown: true }
+
      props: { ...defaultProps }
    });
+
    cy.get("div.selector").click();
    cy.get("div.dropdown-item").should("have.class", "selected");
  });
});
@@ -69,7 +69,6 @@ describe('Events', () => {
    const { getByText, component } = render(PeerSelector, {
      props: {
        ...defaultProps,
-
        peersDropdown: true,
        peers: [
          {
            "id": "hyy841u4phudmr8s5rg1jjwd1ct7x7438wmjwtsm464y8uyxyhyi6c",
@@ -85,11 +84,13 @@ describe('Events', () => {
      }
    });

-
    const peer = getByText("cloudhead");
-
    const mock = cy.spy();
-
    component.$on("peerChanged", mock);
+
    cy.get("div.selector").click().then(() => {
+
      const peer = getByText("cloudhead");
+
      const mock = cy.spy();
+
      component.$on("peerChanged", mock);

-
    fireEvent.click(peer);
-
    expect(mock).to.have.been.calledOnce;
+
      fireEvent.click(peer);
+
      expect(mock).to.have.been.calledOnce;
+
    });
  });
});
modified src/base/projects/PeerSelector.svelte
@@ -4,11 +4,10 @@
  import Dropdown from "@app/Dropdown.svelte";
  import { formatSeedId } from "@app/utils";
  import type { Peer } from "@app/project";
+
  import Floating from "@app/Floating.svelte";

  export let peer: string | null = null;
  export let peers: Peer[];
-
  export let toggleDropdown: (input: string) => void;
-
  export let peersDropdown = false;

  let meta: Peer | undefined;
  // List of items to be created for the Dropdown component.
@@ -43,6 +42,7 @@
    color: var(--color-secondary);
    background-color: var(--color-secondary-background);
    border-radius: var(--border-radius-small);
+
    user-select: none;
  }
  .selector .peer.not-allowed {
    cursor: not-allowed;
@@ -66,9 +66,9 @@
  }
</style>

-
<div class="selector">
-
  <span>
-
    <div on:click={() => toggleDropdown("peer")} class="stat peer" class:not-allowed={!peers}>
+
<Floating>
+
  <div slot="toggle" class="selector">
+
    <div class="stat peer" class:not-allowed={!peers}>
      <Icon name="fork" width={15} height={15} />
      {#if meta}
        <span class="peer-id">
@@ -84,11 +84,13 @@
        </span>
      {/if}
    </div>
+
  </div>
+

+
  <svelte:fragment slot="modal">
    <Dropdown
      {items}
      selected={peer}
-
      visible={peersDropdown}
      on:select={(e) => switchPeer(e.detail)}
    />
-
  </span>
-
</div>
+
  </svelte:fragment>
+
</Floating>