Remove search functionality

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2023-10-26 13:22:01 +02:00
parent 16a1821fff
commit 809daadd04
11 changed files with 28 additions and 350 deletions
+1 -4
View File
@@ -1,8 +1,6 @@
<script lang="ts">
import Link from "@app/components/Link.svelte";
import Connect from "@app/App/Header/Connect.svelte";
import Search from "@app/App/Header/Search.svelte";
import Link from "@app/components/Link.svelte";
</script>
<style>
@@ -44,7 +42,6 @@
alt="Radicle logo"
src="/radicle.svg" />
</Link>
<Search />
</div>
<div class="right">
-133
View File
@@ -1,133 +0,0 @@
<script lang="ts" strictEvents>
import debounce from "lodash/debounce";
import { createEventDispatcher } from "svelte";
import * as Search from "@app/lib/search";
import * as modal from "@app/lib/modal";
import * as router from "@app/lib/router";
import { searchPlaceholder } from "@app/lib/shared";
import { unreachable } from "@app/lib/utils";
import Icon from "@app/components/Icon.svelte";
import SearchResultsModal from "@app/modals/SearchResultsModal.svelte";
import TextInput from "@app/components/TextInput.svelte";
const dispatch = createEventDispatcher<{
finished: null;
}>();
export let input = "";
let loading = false;
let shaking = false;
let expanded = false;
// Clears search input on user navigation.
router.activeRouteStore.subscribe(() => (input = ""));
function shake() {
shaking = true;
debounce(() => (shaking = false), 500)();
}
async function search() {
if (!valid) {
return;
}
loading = true;
const query = input;
const searchResult = await Search.searchProjectsAndProfiles(input);
if (searchResult.type === "nothing") {
shake();
} else if (searchResult.type === "error") {
shake();
} else if (searchResult.type === "projects") {
input = "";
if (searchResult.results.length === 1) {
const { project, baseUrl } = searchResult.results[0];
void router.push({
resource: "project.source",
project: project.id,
node: baseUrl,
});
} else {
modal.show({
component: SearchResultsModal,
props: {
results: searchResult.results,
query,
},
});
}
dispatch("finished");
expanded = false;
} else {
unreachable(searchResult);
}
loading = false;
}
$: valid = input !== "";
</script>
<style>
.search-bar {
display: flex;
}
.shaking {
animation: horizontal-shaking 0.35s;
}
@keyframes horizontal-shaking {
0% {
transform: translateX(0);
}
25% {
transform: translateX(5px);
}
50% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}
.search {
transition: all 0.2s;
width: 11rem;
color: var(--color-fill-secondary);
}
.expanded {
width: 25.5rem;
}
@media (max-width: 720px) {
.expanded {
width: 11rem;
}
}
</style>
<div class="search" class:expanded>
<div class="search-bar" class:shaking>
<TextInput
valid={input !== ""}
{loading}
disabled={loading}
bind:value={input}
on:focus={() => (expanded = true)}
on:blur={() => {
if (input === "") expanded = false;
}}
on:submit={search}
placeholder={searchPlaceholder}>
<svelte:fragment slot="left">
<Icon name="magnifying-glass" />
</svelte:fragment>
</TextInput>
</div>
</div>
-9
View File
@@ -3,7 +3,6 @@
import ColorPaletteModal from "@app/modals/ColorPaletteModal.svelte";
import HotkeysModal from "@app/modals/HotkeysModal.svelte";
import { searchPlaceholder } from "@app/lib/shared";
const onKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
@@ -15,14 +14,6 @@
case "?":
modal.toggle({ component: HotkeysModal, props: {} });
break;
case "/": {
event.preventDefault();
const searchInput: HTMLElement | null = document.querySelector(
`*[placeholder="${searchPlaceholder}"]`,
);
searchInput?.focus();
break;
}
case "d":
if (import.meta.env.PROD) {
return;
+25
View File
@@ -0,0 +1,25 @@
import type { BaseUrl, Project } from "@httpd-client";
import { HttpdClient } from "@httpd-client";
import { isFulfilled } from "@app/lib/utils";
export interface ProjectBaseUrl {
project: Project;
baseUrl: BaseUrl;
}
export async function getProjectsFromNodes(
params: { id: string; baseUrl: BaseUrl }[],
): Promise<ProjectBaseUrl[]> {
const projectPromises = params.map(async param => {
const api = new HttpdClient(param.baseUrl);
const project = await api.project.getById(param.id);
return {
project,
baseUrl: param.baseUrl,
};
});
const results = await Promise.allSettled(projectPromises);
return results.filter(isFulfilled).map(r => r.value);
}
-66
View File
@@ -1,66 +0,0 @@
import type { BaseUrl, Project } from "@httpd-client";
import * as utils from "@app/lib/utils";
import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
import { isFulfilled } from "@app/lib/utils";
export interface ProjectBaseUrl {
project: Project;
baseUrl: BaseUrl;
}
type SearchResult =
| { type: "nothing" }
| { type: "error"; message: string }
| { type: "projects"; results: ProjectBaseUrl[] };
export async function searchProjectsAndProfiles(
query: string,
): Promise<SearchResult> {
try {
const pinned = config.nodes.pinned.map(node => ({
id: query,
baseUrl: node.baseUrl,
}));
if (utils.isRepositoryId(query)) {
const results = await getProjectsFromNodes(pinned);
if (results.length === 0) {
return { type: "nothing" };
} else {
return {
type: "projects",
results,
};
}
}
return { type: "nothing" };
} catch (error) {
let message = "An unknown error occoured while searching.";
if (error instanceof Error) {
message = error.message;
}
return { type: "error", message };
}
}
export async function getProjectsFromNodes(
params: { id: string; baseUrl: BaseUrl }[],
): Promise<ProjectBaseUrl[]> {
const projectPromises = params.map(async param => {
const api = new HttpdClient(param.baseUrl);
const project = await api.project.getById(param.id);
return {
project,
baseUrl: param.baseUrl,
};
});
const results = await Promise.allSettled(projectPromises);
return results.filter(isFulfilled).map(r => r.value);
}
-4
View File
@@ -1,4 +0,0 @@
// This file is shared between tests and the UI code. It should only contain
// basic JS primitives, so that it works in both browser and node environments.
export const searchPlaceholder = "Enter an RID";
-5
View File
@@ -40,11 +40,6 @@
<KeyHint>?</KeyHint>
</div>
<div class="pair">
<div class="description">Search</div>
<KeyHint>/</KeyHint>
</div>
{#if import.meta.env.DEV}
<div class="pair">
<div class="description">Color palette</div>
-55
View File
@@ -1,55 +0,0 @@
<script lang="ts" strictEvents>
import type { ProjectBaseUrl } from "@app/lib/search";
import * as modal from "@app/lib/modal";
import { formatRepositoryId } from "@app/lib/utils";
import Modal from "@app/components/Modal.svelte";
import Icon from "@app/components/Icon.svelte";
import Link from "@app/components/Link.svelte";
export let query: string;
export let results: ProjectBaseUrl[];
</script>
<style>
.results {
text-align: left;
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin: 0.5rem 0;
}
</style>
<Modal title={`Results for "${query}"`}>
<Icon name="magnifying-glass" size="48" slot="icon" />
<span slot="body" class="results">
{#if results.length > 0}
<ul>
{#each results as result}
<li>
<Link
on:afterNavigate={modal.hide}
route={{
resource: "project.source",
node: result.baseUrl,
project: result.project.id,
}}>
<span title={result.baseUrl.hostname}>
<span>{result.project.name}</span>
<span class="id">
&nbsp;{formatRepositoryId(result.project.id)}
</span>
</span>
</Link>
</li>
{/each}
</ul>
{/if}
</span>
</Modal>
+2 -2
View File
@@ -1,12 +1,12 @@
import type { LoadErrorRoute } from "@app/lib/router/definitions";
import type { ProjectBaseUrl } from "@app/lib/search";
import type { ProjectBaseUrl } from "@app/lib/projects";
import type { WeeklyActivity } from "@app/lib/commit";
import { get } from "svelte/store";
import { api, httpdStore } from "@app/lib/httpd";
import { config } from "@app/lib/config";
import { getProjectsFromNodes } from "@app/lib/search";
import { getProjectsFromNodes } from "@app/lib/projects";
import { loadProjectActivity } from "@app/lib/commit";
export interface ProjectBaseUrlActivity extends ProjectBaseUrl {
-39
View File
@@ -1,39 +0,0 @@
import { searchPlaceholder } from "@app/lib/shared";
import { test, expect } from "@tests/support/fixtures.js";
test("global hotkeys", async ({ page }) => {
await page.goto("/");
await page.locator("body").press(`/`);
await expect(page.locator(".search.expanded")).toBeVisible();
// Delaying the input speed a bit to imitate a real user.
await page.keyboard.type("searchquery", { delay: 100 });
// Keyboard hint shows up in the search bar.
{
await expect(page.getByText("⏎")).toBeVisible();
await expect(page.getByPlaceholder(searchPlaceholder)).toHaveValue(
"searchquery",
);
}
// Other hotkeys don't trigger while input is focussed.
{
await page.keyboard.type("?");
await expect(page.getByPlaceholder(searchPlaceholder)).toHaveValue(
"searchquery?",
);
await expect(
page.locator(".modal").getByText("Keyboard shortcuts"),
).not.toBeVisible();
}
// Hitting `Esc` defocuses the input.
{
await page.locator("body").press("Escape");
await expect(page.getByPlaceholder(searchPlaceholder)).toHaveValue(
"searchquery?",
);
await expect(page.getByText("⏎")).toBeHidden();
await expect(page.getByPlaceholder(searchPlaceholder)).not.toBeFocused();
}
});
-33
View File
@@ -1,33 +0,0 @@
import { searchPlaceholder } from "@app/lib/shared";
import {
test,
expect,
sourceBrowsingRid,
sourceBrowsingUrl,
} from "@tests/support/fixtures.js";
test("navigate to existing project", async ({ page }) => {
await page.goto("/");
const searchInput = page.getByPlaceholder(searchPlaceholder);
await searchInput.click();
await searchInput.fill(sourceBrowsingRid);
await searchInput.press("Enter");
await expect(page).toHaveURL(sourceBrowsingUrl);
await expect(searchInput).not.toHaveValue(sourceBrowsingRid);
});
test("navigate to a project that does not exist", async ({ page }) => {
await page.goto("/");
const searchInput = page.getByPlaceholder(searchPlaceholder);
await searchInput.click();
const nonExistantId = "rad:zAAAAAAAAAAAAAAAAAAAAAAAAAAA";
await searchInput.fill(nonExistantId);
await searchInput.press("Enter");
await page.waitForSelector(".search-bar.shaking");
await expect(page).toHaveURL("/");
await expect(searchInput).toHaveValue(nonExistantId);
});