Skip to content
This repository has been archived by the owner on Aug 1, 2022. It is now read-only.

Commit

Permalink
feat(ui): check if project exists when searching (#1030)
Browse files Browse the repository at this point in the history
Up until now we assumed the provided urn is not present in the monorepo.
This change alters the behaviour to first check if a project exists
locally - because locally sourced is best source. If it exists locally
the result box offers to navigate directly to it -- also possible with a
hit of enter. Should it not exist a track toggle is presented, which
when pressed will fire a request for the project and show a
notification.

    check for project locally first
    navigate to existing project
    request urn from the network on track toggle
    validate input to be a valid urn

Refs #984

Co-authored-by: Nuno Alexandre <hi@nunoalexandre.com>
  • Loading branch information
xla and NunoAlexandre committed Oct 14, 2020
1 parent 5c7ba2a commit 646c50e
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 199 deletions.
8 changes: 8 additions & 0 deletions proxy/api/src/http/error.rs
Expand Up @@ -72,6 +72,7 @@ pub struct Error {
}

/// Handler to convert [`error::Error`] to [`Error`] response.
#[allow(clippy::too_many_lines)]
pub async fn recover(err: Rejection) -> Result<impl Reply, Infallible> {
log::error!("{:?}", err);

Expand Down Expand Up @@ -119,6 +120,13 @@ pub async fn recover(err: Rejection) -> Result<impl Reply, Infallible> {
"ENTITY_EXISTS",
format!("the identity '{}' already exists", urn),
),
coco::state::Error::Storage(state::error::storage::Error::Blob(
state::error::blob::Error::NotFound(_),
)) => (
StatusCode::NOT_FOUND,
"NOT_FOUND",
"entity not found".to_string(),
),
coco::state::Error::Git(git_error) => (
StatusCode::BAD_REQUEST,
"GIT_ERROR",
Expand Down
6 changes: 6 additions & 0 deletions proxy/coco/src/state/error.rs
Expand Up @@ -98,3 +98,9 @@ impl Error {
pub mod storage {
pub use librad::git::storage::Error;
}

/// Re-export the underlying [`blob::Error`] so that consumers don't need to add `librad` as a
/// dependency to match on the variant. Instead, they can import `coco::state::error::blob`.
pub mod blob {
pub use librad::git::ext::blob::Error;
}
10 changes: 6 additions & 4 deletions ui/App.svelte
Expand Up @@ -22,7 +22,9 @@
import Onboarding from "./Screen/Onboarding.svelte";
import DesignSystemGuide from "./Screen/DesignSystemGuide.svelte";
import Discovery from "./Screen/Discovery.svelte";
import Modal from "./Modal";
import ModalNewProject from "./Modal/NewProject.svelte";
import ModalSearch from "./Modal/Search.svelte";
import ModalShortcuts from "./Modal/Shortcuts.svelte";
import NotFound from "./Screen/NotFound.svelte";
import Org from "./Screen/Org.svelte";
import OrgRegistration from "./Screen/OrgRegistration.svelte";
Expand Down Expand Up @@ -61,9 +63,9 @@
};
const modalRoutes = {
"/new-project": Modal.NewProject,
"/search": Modal.Search,
"/shortcuts": Modal.Shortcuts,
"/new-project": ModalNewProject,
"/search": ModalSearch,
"/shortcuts": ModalShortcuts,
};
$: switch ($store.status) {
Expand Down
11 changes: 4 additions & 7 deletions ui/DesignSystem/Component/TrackToggle.svelte
@@ -1,6 +1,8 @@
<script lang="ts">
<script lang="typescript">
import { createEventDispatcher } from "svelte";
import * as track from "../../src/track";
import { Icon } from "../Primitive";
import Hoverable from "./Hoverable.svelte";
Expand All @@ -12,19 +14,14 @@
let active: boolean = false;
enum TrackingEvent {
Track = "track",
Untrack = "untrack",
}
const down = () => {
active = true;
};
const up = () => {
active = false;
tracking = !tracking;
dispatch(tracking ? TrackingEvent.Track : TrackingEvent.Untrack);
dispatch(tracking ? track.Event.Track : track.Event.Untrack);
};
const dispatch = createEventDispatcher();
Expand Down
16 changes: 3 additions & 13 deletions ui/DesignSystem/Primitive/Input/Text.svelte
@@ -1,6 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
<script lang="typescript">
import Icon from "../Icon";
import Spinner from "../../Component/Spinner.svelte";
import KeyHint from "../../Component/KeyHint.svelte";
Expand All @@ -23,19 +21,11 @@
export let spellcheck: boolean = false;
export let autofocus: boolean = false;
const dispatch = createEventDispatcher();
// Can't use normal `autofocus` attribute on the `inputElement`:
// "Autofocus processing was blocked because a document's URL has a fragment".
// preventScroll is necessary for onboarding animations to work.
$: if (autofocus) inputElement && inputElement.focus({ preventScroll: true });
const onKeydown = (event: KeyboardEvent) => {
if (event.key === "Enter") {
dispatch("enter");
}
};
$: showHint = hint.length > 0 && value.length === 0;
</script>

Expand Down Expand Up @@ -118,7 +108,7 @@
.left-item-wrapper {
align-items: center;
display: flex;
height: 100%;
height: 2.5rem;
justify-content: center;
left: 0px;
padding-left: 0.5rem;
Expand Down Expand Up @@ -146,7 +136,7 @@
{disabled}
on:change
on:input
on:keydown={onKeydown}
on:keydown
bind:this={inputElement}
{spellcheck}
style={inputStyle} />
Expand Down
2 changes: 1 addition & 1 deletion ui/Hotkeys.svelte
@@ -1,4 +1,4 @@
<script lang="ts">
<script lang="typescript">
import { location, pop, push } from "svelte-spa-router";
import * as modal from "./src/modal";
Expand Down
File renamed without changes.
171 changes: 171 additions & 0 deletions ui/Modal/Search.svelte
@@ -0,0 +1,171 @@
<script lang="typescript">
import { push } from "svelte-spa-router";
import { createEventDispatcher } from "svelte";
import * as notification from "../src/notification";
import * as path from "../src/path";
import type { Project } from "../src/project";
import * as remote from "../src/remote";
import {
projectRequest as request,
projectSearch as store,
reset,
requestProject,
searchProject,
urnValidationStore,
} from "../src/search";
import { ValidationStatus } from "../src/validation";
import { Icon, Input } from "../DesignSystem/Primitive";
import { Remote, TrackToggle } from "../DesignSystem/Component";
let id: string;
let value: string;
const dispatch = createEventDispatcher();
const urnValidation = urnValidationStore();
const navigateToProject = (project: Project) => {
dispatch("hide");
push(path.projectSource(project.id));
};
const navigateToUntracked = () => {
dispatch("hide");
push(path.projectUntracked(value));
};
const onKeydown = (event: KeyboardEvent) => {
switch (event.code) {
case "Enter":
// Navigate to project directly if present.
if ($store.status === remote.Status.Success) {
// FIXME(xla): Once remote/Remote offer stronger type guarantees this needs to go.
navigateToProject(
($store as { status: remote.Status.Success; data: Project }).data
);
}
break;
case "Escape":
dispatch("hide");
break;
}
};
const onTrack = () => {
requestProject({ urn: value });
};
// Validate input entered, at the moment valid RadUrns are the only acceptable input.
$: if (value && value.length > 0) {
urnValidation.validate(value);
id = value.replace("rad:git:", "");
} else {
urnValidation.reset();
}
// To support quick pasting, request the urn once valid to get tracking information.
$: if ($urnValidation.status === ValidationStatus.Success) {
searchProject({ urn: value });
}
// Reset searches if the input became invalid.
$: if ($urnValidation.status !== ValidationStatus.Success) {
reset();
}
// Fire notification when a request has been created.
$: if ($request.status === remote.Status.Success) {
notification.info(
"You’ll be notified on your profile when this project has been found.",
false,
"View profile",
() => {
dispatch("hide");
push(path.profileProjects());
}
);
}
$: tracked = $store.status === remote.Status.Success;
$: untracked = $store.status === remote.Status.Error;
</script>

<style>
.container {
width: 26.5rem;
}
.search-bar {
margin-bottom: 1rem;
position: relative;
}
.result {
background: var(--color-background);
border-radius: 0.5rem;
height: 0;
overflow: hidden;
transition: height 0.3s linear;
}
.tracked {
height: 5rem;
}
.untracked {
height: 11rem;
}
.header {
align-items: center;
cursor: pointer;
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
.id {
color: var(--color-foreground-level-6);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

<div class="container">
<div class="search-bar">
<Input.Text
autofocus
bind:value
hint="v"
inputStyle="color: var(--color-foreground-level-6);"
on:keydown={onKeydown}
placeholder="Have a project id? Paste it here…"
showLeftItem
style="border: none; border-radius: 0.5rem;"
validation={$urnValidation}>
<div slot="left" style="display: flex;">
<Icon.MagnifyingGlass />
</div>
</Input.Text>
</div>

<div class="result" class:tracked class:untracked>
<Remote {store} let:data={project}>
<div style="padding: 1.5rem;">
<div
class="header typo-header-3"
on:click={_ev => navigateToProject(project)}>
<span class="id">{project.metadata.name}</span>
</div>
</div>

<div slot="error" style="padding: 1.5rem;">
<div class="header typo-header-3">
<span class="id" on:click={navigateToUntracked}>{id}</span>
<TrackToggle on:track={onTrack} style="margin-left: 1rem;" />
</div>

<p style="color: var(--color-foreground-level-6);">
You’re not following this project yet, so there’s nothing to show
here. Follow it and you’ll be notified as soon as it’s available.
</p>
</div>
</Remote>
</div>
</div>

0 comments on commit 646c50e

Please sign in to comment.