mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-31 09:10:44 +08:00
Remove search functionality
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
{formatRepositoryId(result.project.id)}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</span>
|
||||
</Modal>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user