mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 19:30:38 +08:00
Refactor search logic
- type events that are handed between components
- simplify the `resolve` function interface
- before we had return values and exceptions, now just a single union
type as return value
- move navigation to the Search component to reduce its
responsibilities
- flatten and simplify multi-level branching
- shuffle around code
- move the `resolve` function to the `Search` component module,
because it's only used there
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
+10
-11
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from "@app/config";
|
||||
import type { ResolvedSearch } from "@app/resolver";
|
||||
import type { ProjectsAndProfiles } from "@app/Search.svelte";
|
||||
import type { Session } from "@app/session";
|
||||
|
||||
import { link } from "svelte-routing";
|
||||
@@ -27,14 +27,9 @@
|
||||
export let config: Config;
|
||||
|
||||
let query: string;
|
||||
let results: ResolvedSearch;
|
||||
let results: ProjectsAndProfiles | null = null;
|
||||
|
||||
let sessionButtonHover = false;
|
||||
let searchResultsDisplayed = false;
|
||||
|
||||
function toggleSearchResults() {
|
||||
searchResultsDisplayed = !searchResultsDisplayed;
|
||||
}
|
||||
|
||||
$: address = session && session.address;
|
||||
$: tokenBalance = session && session.tokenBalance;
|
||||
@@ -177,7 +172,6 @@
|
||||
{config}
|
||||
on:search={e => {
|
||||
({ query, results } = e.detail);
|
||||
toggleSearchResults();
|
||||
}} />
|
||||
</div>
|
||||
<div class="nav">
|
||||
@@ -259,7 +253,6 @@
|
||||
}}
|
||||
on:search={e => {
|
||||
({ query, results } = e.detail);
|
||||
toggleSearchResults();
|
||||
}} />
|
||||
</div>
|
||||
<a
|
||||
@@ -276,7 +269,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if searchResultsDisplayed}
|
||||
<SearchResults {config} {results} {query} on:close={toggleSearchResults} />
|
||||
{#if results}
|
||||
<SearchResults
|
||||
{config}
|
||||
{results}
|
||||
{query}
|
||||
on:close={() => {
|
||||
results = null;
|
||||
}} />
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
+176
-22
@@ -1,42 +1,196 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" context="module">
|
||||
import type { Host } from "@app/api";
|
||||
import type { ProjectInfo } from "@app/project";
|
||||
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import * as utils from "@app/utils";
|
||||
import { Profile } from "@app/profile";
|
||||
import { Project } from "@app/project";
|
||||
|
||||
export interface ProjectsAndProfiles {
|
||||
projects: { info: ProjectInfo; seed: Host }[];
|
||||
profiles: Profile[];
|
||||
}
|
||||
|
||||
type SearchResult =
|
||||
| { type: "nothing" }
|
||||
| { type: "error"; message: string }
|
||||
| { type: "singleProfile"; id: string }
|
||||
| { type: "singleProject"; seedHost: string; id: string }
|
||||
| { type: "projectsAndProfiles"; projectsAndProfiles: ProjectsAndProfiles };
|
||||
|
||||
async function searchProjectsAndProfiles(
|
||||
query: string,
|
||||
config: Config,
|
||||
): Promise<SearchResult> {
|
||||
try {
|
||||
// The query is a plain Ethereum address.
|
||||
if (ethers.utils.isAddress(query)) {
|
||||
return { type: "singleProfile", id: query };
|
||||
}
|
||||
|
||||
const projectOnSeeds = Object.keys(config.seeds.pinned).map(seed => ({
|
||||
nameOrUrn: query,
|
||||
seed,
|
||||
}));
|
||||
|
||||
// The query is a radicle project URN.
|
||||
if (utils.isRadicleId(query)) {
|
||||
const projects = await Project.getMulti(projectOnSeeds);
|
||||
|
||||
if (projects.length === 1) {
|
||||
return {
|
||||
type: "singleProject",
|
||||
seedHost: projects[0].seed.host,
|
||||
id: query,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "projectsAndProfiles",
|
||||
projectsAndProfiles: { projects, profiles: [] },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// The query is either a project or a profile name.
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const projectsAndProfiles: ProjectsAndProfiles = {
|
||||
projects: [],
|
||||
profiles: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const projects = await Project.getMulti(projectOnSeeds);
|
||||
projectsAndProfiles.projects.push(...projects);
|
||||
} catch {
|
||||
// TODO: collect errors and forward to user.
|
||||
}
|
||||
|
||||
try {
|
||||
let params: string[];
|
||||
if (utils.isENSName(normalizedQuery, config)) {
|
||||
params = [normalizedQuery];
|
||||
} else {
|
||||
params = [
|
||||
`${normalizedQuery}.${config.registrar.domain}`,
|
||||
`${normalizedQuery}.eth`,
|
||||
];
|
||||
}
|
||||
const profiles = await Profile.getMulti(params, config);
|
||||
projectsAndProfiles.profiles.push(...profiles);
|
||||
} catch {
|
||||
// TODO: collect errors and forward to user.
|
||||
}
|
||||
|
||||
const projectCount = projectsAndProfiles.projects.length;
|
||||
const profileCount = projectsAndProfiles.profiles.length;
|
||||
|
||||
if (profileCount === 1 && projectCount === 0) {
|
||||
return {
|
||||
type: "singleProfile",
|
||||
id: projectsAndProfiles.profiles[0].address,
|
||||
};
|
||||
}
|
||||
|
||||
if (profileCount === 0 && projectCount === 1) {
|
||||
return {
|
||||
type: "singleProject",
|
||||
seedHost: projectsAndProfiles.projects[0].seed.host,
|
||||
id: query,
|
||||
};
|
||||
}
|
||||
|
||||
if (profileCount > 0 || projectCount > 0) {
|
||||
return {
|
||||
type: "projectsAndProfiles",
|
||||
projectsAndProfiles,
|
||||
};
|
||||
}
|
||||
|
||||
return { type: "nothing" };
|
||||
} catch (error) {
|
||||
let message = "An unknown error occoured while searching.";
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
return { type: "error", message };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Config } from "@app/config";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import debounce from "lodash/debounce";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { navigate } from "svelte-routing";
|
||||
|
||||
import { resolve } from "@app/resolver";
|
||||
import Loading from "@app/Loading.svelte";
|
||||
import TextInput from "@app/TextInput.svelte";
|
||||
import { unreachable } from "@app/utils";
|
||||
|
||||
export let config: Config;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
finished: boolean;
|
||||
search: { query: string; results: ProjectsAndProfiles };
|
||||
}>();
|
||||
|
||||
let input = "";
|
||||
let searching = false;
|
||||
let shake = false;
|
||||
let shaking = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const handleKeydown = async (event: KeyboardEvent) => {
|
||||
function shake() {
|
||||
shaking = true;
|
||||
debounce(() => (shaking = false), 500)();
|
||||
}
|
||||
|
||||
async function search(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
if (input === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
searching = true;
|
||||
resolve(input, config)
|
||||
.then(results => {
|
||||
const query = input;
|
||||
input = "";
|
||||
searching = false;
|
||||
if (results) dispatch("search", { query, results });
|
||||
})
|
||||
.catch(() => {
|
||||
searching = false;
|
||||
shake = true;
|
||||
debounce(() => (shake = false), 500)();
|
||||
dispatch("finished");
|
||||
|
||||
const query = input;
|
||||
const searchResult = await searchProjectsAndProfiles(input, config);
|
||||
|
||||
if (searchResult.type === "nothing") {
|
||||
searching = false;
|
||||
shake();
|
||||
} else if (searchResult.type === "error") {
|
||||
// TODO: show some kind of notification to the user.
|
||||
shake();
|
||||
} else if (searchResult.type === "singleProfile") {
|
||||
input = "";
|
||||
navigate(`/${searchResult.id}`, { replace: true });
|
||||
} else if (searchResult.type === "singleProject") {
|
||||
input = "";
|
||||
navigate(`/seeds/${searchResult.seedHost}/${searchResult.id}`, {
|
||||
replace: true,
|
||||
});
|
||||
} else if (searchResult.type === "projectsAndProfiles") {
|
||||
// TODO: show some kind of notification about any errors to the user.
|
||||
input = "";
|
||||
dispatch("search", {
|
||||
query,
|
||||
results: searchResult.projectsAndProfiles,
|
||||
});
|
||||
} else {
|
||||
unreachable(searchResult);
|
||||
}
|
||||
searching = false;
|
||||
dispatch("finished");
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.horizontal-shake {
|
||||
.shaking {
|
||||
animation: horizontal-shaking 0.35s;
|
||||
}
|
||||
@keyframes horizontal-shaking {
|
||||
@@ -58,12 +212,12 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class:horizontal-shake={shake}>
|
||||
<div class:shaking>
|
||||
<TextInput
|
||||
variant="dashed"
|
||||
disabled={searching}
|
||||
bind:value={input}
|
||||
on:keydown={handleKeydown}
|
||||
on:keydown={search}
|
||||
placeholder="Search a name or address…">
|
||||
<svelte:fragment slot="right">
|
||||
{#if searching}
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
const getProjects =
|
||||
config.projects.pinned.length > 0
|
||||
? Project.getMulti(config.projects.pinned)
|
||||
? Project.getMulti(
|
||||
config.projects.pinned.map(project => ({
|
||||
nameOrUrn: project.urn,
|
||||
seed: project.seed,
|
||||
})),
|
||||
)
|
||||
: Promise.resolve([]);
|
||||
|
||||
const onClick = (project: ProjectInfo, seed: Host) => {
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import Address from "@app/Address.svelte";
|
||||
import Button from "@app/Button.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { ResolvedSearch } from "@app/resolver";
|
||||
import type { ProjectsAndProfiles } from "@app/Search.svelte";
|
||||
|
||||
export let query: string;
|
||||
export let results: ResolvedSearch;
|
||||
export let results: ProjectsAndProfiles;
|
||||
export let config: Config;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -63,10 +63,10 @@
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if results.ens.length > 0}
|
||||
{#if results.profiles.length > 0}
|
||||
<p class="highlight txt-medium">ENS names</p>
|
||||
<ul>
|
||||
{#each results.ens as profile}
|
||||
{#each results.profiles as profile}
|
||||
<li>
|
||||
<Address address={profile.address} {profile} {config} resolve />
|
||||
</li>
|
||||
|
||||
+2
-2
@@ -485,14 +485,14 @@ export class Project implements ProjectInfo {
|
||||
}
|
||||
|
||||
static async getMulti(
|
||||
projs: { urn: Urn; seed: string }[],
|
||||
projs: { nameOrUrn: Urn; seed: string }[],
|
||||
): Promise<{ info: ProjectInfo; seed: Host }[]> {
|
||||
const promises = [];
|
||||
|
||||
for (const proj of projs) {
|
||||
const seed = { host: proj.seed, port: null };
|
||||
promises.push(
|
||||
Project.getInfo(proj.urn, seed).then(info => {
|
||||
Project.getInfo(proj.nameOrUrn, seed).then(info => {
|
||||
return { info, seed };
|
||||
}),
|
||||
);
|
||||
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
import type { Host } from "@app/api";
|
||||
import type { Config } from "@app/config";
|
||||
import type { ProjectInfo } from "@app/project";
|
||||
|
||||
import { navigate } from "svelte-routing";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import * as utils from "@app/utils";
|
||||
import { Project } from "@app/project";
|
||||
import { NotFoundError } from "@app/error";
|
||||
import { Profile, ProfileType } from "@app/profile";
|
||||
|
||||
export interface IProject {
|
||||
info: ProjectInfo;
|
||||
seed: Host;
|
||||
}
|
||||
|
||||
export interface ResolvedSearch {
|
||||
projects: IProject[];
|
||||
ens: Profile[];
|
||||
}
|
||||
|
||||
export async function resolve(
|
||||
q: string | null,
|
||||
config: Config,
|
||||
): Promise<ResolvedSearch | null> {
|
||||
const results: ResolvedSearch = {
|
||||
projects: [],
|
||||
ens: [],
|
||||
};
|
||||
|
||||
try {
|
||||
if (q) {
|
||||
const seeds = Object.keys(config.seeds.pinned).map(seed => ({
|
||||
urn: q as string,
|
||||
seed,
|
||||
}));
|
||||
|
||||
if (ethers.utils.isAddress(q)) {
|
||||
navigate(`/${q}`, { replace: true });
|
||||
return null;
|
||||
|
||||
// ========= Projects =========
|
||||
} else if (utils.isRadicleId(q)) {
|
||||
const projects = await Project.getMulti(seeds);
|
||||
if (projects.length === 1) {
|
||||
navigate(`/seeds/${projects[0].seed.host}/${q}`, {
|
||||
replace: true,
|
||||
});
|
||||
return null;
|
||||
} else {
|
||||
results.projects.push(...projects);
|
||||
}
|
||||
} else {
|
||||
const projects = await Project.getMulti(seeds);
|
||||
results.projects.push(...projects);
|
||||
|
||||
// ========= ENS Names =========
|
||||
const normalizedQuery = q.toLowerCase();
|
||||
let profile: Profile | null;
|
||||
try {
|
||||
profile = await Profile.get(
|
||||
normalizedQuery,
|
||||
ProfileType.Minimal,
|
||||
config,
|
||||
);
|
||||
} catch (e) {
|
||||
profile = null;
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
if (results.projects.length === 0) {
|
||||
navigate(`/${profile.address}`, { replace: true });
|
||||
return null;
|
||||
} else {
|
||||
results.ens.push(profile);
|
||||
}
|
||||
} else {
|
||||
let profiles: Profile[];
|
||||
try {
|
||||
profiles = await Profile.getMulti(
|
||||
[
|
||||
`${normalizedQuery}.${config.registrar.domain}`,
|
||||
`${normalizedQuery}.eth`,
|
||||
],
|
||||
config,
|
||||
);
|
||||
} catch (e) {
|
||||
profiles = [];
|
||||
}
|
||||
|
||||
if (profiles.length > 1) {
|
||||
results.ens.push(...profiles);
|
||||
} else if (profiles.length === 1) {
|
||||
if (results.projects.length === 0) {
|
||||
navigate(`/${profiles[0].address}`, { replace: true });
|
||||
return null;
|
||||
} else {
|
||||
results.ens.push(...profiles);
|
||||
}
|
||||
} else {
|
||||
if (results.projects.length === 1) {
|
||||
navigate(`/seeds/${projects[0].seed.host}/${q}`, {
|
||||
replace: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.projects.length > 0 || results.ens.length > 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
throw new NotFoundError("No search results found");
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundError) {
|
||||
throw new NotFoundError(e.message);
|
||||
} else {
|
||||
throw Error("Not able to resolve search query");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user