Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Sort projects by name on the home screen
Open rudolfs opened 1 year ago
8 files changed +115 -14 31e27339 8a3e480b
modified crates/radicle-types/src/repo.rs
@@ -94,6 +94,10 @@ impl ProjectPayload {
    pub fn new(data: ProjectPayloadData, meta: ProjectPayloadMeta) -> Self {
        Self { data, meta }
    }
+

+
    pub fn name(&self) -> &str {
+
        &self.data.name
+
    }
}

impl TryFrom<identity::doc::Payload> for ProjectPayloadData {
modified crates/radicle-types/src/traits/repo.rs
@@ -57,7 +57,13 @@ pub trait Repo: Profile {
            entries.push(repo_info)
        }

-
        entries.sort_by(|a, b| b.last_commit_timestamp.cmp(&a.last_commit_timestamp));
+
        entries.sort_by_key(|repo_info| {
+
            repo_info
+
                .payloads
+
                .project
+
                .as_ref()
+
                .map(|p| p.name().to_lowercase())
+
        });

        Ok::<_, Error>(entries)
    }
modified package-lock.json
@@ -38,9 +38,11 @@
        "eslint-config-prettier": "^10.1.1",
        "eslint-plugin-svelte": "^3.3.2",
        "execa": "^9.5.2",
+
        "fuzzysort": "^3.1.0",
        "get-port": "^7.1.0",
        "happy-dom": "^17.4.4",
        "hast-util-to-dom": "^4.0.1",
+
        "keyux": "^0.11.1",
        "lodash": "^4.17.21",
        "marked": "^15.0.7",
        "marked-emoji": "^2.0.0",
@@ -2830,6 +2832,13 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
+
    "node_modules/fuzzysort": {
+
      "version": "3.1.0",
+
      "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz",
+
      "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==",
+
      "dev": true,
+
      "license": "MIT"
+
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3260,6 +3269,22 @@
        "katex": "cli.js"
      }
    },
+
    "node_modules/keyux": {
+
      "version": "0.11.1",
+
      "resolved": "https://registry.npmjs.org/keyux/-/keyux-0.11.1.tgz",
+
      "integrity": "sha512-FIPQNnbtYuGdVU/vL0X9fcJwtkuzOwhzceUezMZMgTwGDNA2tmO4A/uA4tt1hdb0Cu3S3o+dNm5ifA+/reNX2A==",
+
      "dev": true,
+
      "funding": [
+
        {
+
          "type": "github",
+
          "url": "https://github.com/sponsors/ai"
+
        }
+
      ],
+
      "license": "MIT",
+
      "engines": {
+
        "node": "^18.0.0 || >=20.0.0"
+
      }
+
    },
    "node_modules/keyv": {
      "version": "4.5.4",
      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
modified package.json
@@ -53,9 +53,11 @@
    "eslint-config-prettier": "^10.1.1",
    "eslint-plugin-svelte": "^3.3.2",
    "execa": "^9.5.2",
+
    "fuzzysort": "^3.1.0",
    "get-port": "^7.1.0",
    "happy-dom": "^17.4.4",
    "hast-util-to-dom": "^4.0.1",
+
    "keyux": "^0.11.1",
    "lodash": "^4.17.21",
    "marked": "^15.0.7",
    "marked-emoji": "^2.0.0",
modified src/components/RepoCard.svelte
@@ -12,9 +12,10 @@
    repo: RepoInfo;
    selfDid: string;
    onclick?: () => void;
+
    focussed?: boolean;
  }

-
  const { repo, selfDid, onclick }: Props = $props();
+
  const { repo, selfDid, onclick, focussed }: Props = $props();

  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
@@ -34,7 +35,7 @@
</style>

<Border
-
  variant="ghost"
+
  variant={focussed ? "secondary" : "ghost"}
  styleCursor="pointer"
  styleWidth="100%"
  stylePadding="8px 12px"
modified src/components/TextInput.svelte
@@ -16,6 +16,7 @@
    onDismiss?: () => void;
    valid?: boolean;
    oninput?: FormEventHandler<HTMLInputElement>;
+
    keyShortcuts?: string;
  }

  /* eslint-disable prefer-const */
@@ -31,6 +32,7 @@
    onDismiss,
    valid = true,
    oninput,
+
    keyShortcuts,
  }: Props = $props();
  /* eslint-enable prefer-const */

@@ -93,6 +95,7 @@
  variant={valid ? (focussed ? "secondary" : "ghost") : "danger"}
  styleWidth="100%">
  <input
+
    aria-keyshortcuts={keyShortcuts}
    onfocus={() => {
      focussed = true;
    }}
modified src/main.ts
@@ -1,6 +1,10 @@
import { mount } from "svelte";
+
import { hotkeyKeyUX, hotkeyMacCompat, startKeyUX } from "keyux";
import App from "./App.svelte";

const app = mount(App, { target: document.body });

+
const mac = hotkeyMacCompat();
+
startKeyUX(window, [hotkeyKeyUX([mac])]);
+

export default app;
modified src/views/home/Repos.svelte
@@ -6,17 +6,22 @@
  import type { RepoCount } from "@bindings/repo/RepoCount";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

+
  import fuzzysort from "fuzzysort";
+

  import * as router from "@app/lib/router";
-
  import { didFromPublicKey } from "@app/lib/utils";
+
  import { didFromPublicKey, modifierKey } from "@app/lib/utils";
  import { dynamicInterval } from "@app/lib/interval";
  import { invoke } from "@app/lib/invoke";
  import { onMount } from "svelte";

+
  import Border from "@app/components/Border.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import HomeSidebar from "@app/components/HomeSidebar.svelte";
+
  import Icon from "@app/components/Icon.svelte";
  import Layout from "@app/views/repo/Layout.svelte";
  import Onboarding from "@app/views/home/Onboarding.svelte";
  import RepoCard from "@app/components/RepoCard.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";

  interface Props {
    activeTab?: HomeReposTab;
@@ -63,6 +68,22 @@
      invoke<Config>("config"),
    ]);
  }
+

+
  let searchInput = $state("");
+

+
  const searchableRepos = $derived(
+
    repos
+
      .flatMap(r => {
+
        if (r.payloads["xyz.radicle.project"]) {
+
          return { repo: r, name: r.payloads["xyz.radicle.project"].data.name };
+
        }
+
      })
+
      .filter((item): item is NonNullable<typeof item> => item !== undefined),
+
  );
+

+
  const searchResults = $derived(
+
    fuzzysort.go(searchInput, searchableRepos, { key: "name", all: true }),
+
  );
</script>

<style>
@@ -82,7 +103,6 @@
    padding-right: 1.5rem;
    align-items: center;
    min-height: 40px;
-
    margin-bottom: 0.5rem;
  }
</style>

@@ -100,24 +120,60 @@
      {notificationCount} />
  {/snippet}
  <div class="container">
-
    <div class="header">Repositories</div>
+
    <div class="global-flex" style:margin-bottom="0.5rem">
+
      <div class="header">Repositories</div>
+
      <div style:margin-left="auto">
+
        <TextInput
+
          onSubmit={async () => {
+
            if (searchResults.length === 1) {
+
              await router.push({
+
                resource: "repo.issues",
+
                rid: searchResults[0].obj.repo.rid,
+
                status: "open",
+
              });
+
            }
+
          }}
+
          onDismiss={() => {
+
            searchInput = "";
+
          }}
+
          placeholder={`Filter repositories ${modifierKey()} + f`}
+
          keyShortcuts="ctrl+f"
+
          bind:value={searchInput} />
+
      </div>
+
    </div>
    {#if repos.length > 0}
-
      <div class="repo-grid">
-
        {#each repos as repo}
-
          {#if repo.payloads["xyz.radicle.project"]}
+
      {#if searchResults.length > 0}
+
        <div class="repo-grid">
+
          {#each searchResults as result}
            <RepoCard
-
              {repo}
+
              focussed={searchResults.length === 1}
+
              repo={result.obj.repo}
              selfDid={didFromPublicKey(config.publicKey)}
              onclick={() => {
                void router.push({
                  resource: "repo.issues",
-
                  rid: repo.rid,
+
                  rid: result.obj.repo.rid,
                  status: "open",
                });
              }} />
-
          {/if}
-
        {/each}
-
      </div>
+
          {/each}
+
        </div>
+
      {:else}
+
        <Border
+
          variant="ghost"
+
          styleAlignItems="center"
+
          styleJustifyContent="center">
+
          <div
+
            class="global-flex"
+
            style:height="74px"
+
            style:justify-content="center">
+
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
              <Icon name="none" />
+
              No matching repositories.
+
            </div>
+
          </div>
+
        </Border>
+
      {/if}
    {:else}
      <Onboarding {reload} />
    {/if}