Refactor project browser

Extracts the header part of the project browser into a separate
component, some props and functions not necessary anymore in `Browser`
are passed into `Header` or into the superior project `View` and the
clickable parts of the header part are managed by Svelte Events.

This prepares the stage for commit listing and commit details.

Signed-off-by: Sebastian Martinez <sebastinez@me.com>
This commit is contained in:
Sebastian Martinez
2021-12-17 22:42:30 +01:00
parent b3929fef74
commit 4b2c14e42e
3 changed files with 321 additions and 264 deletions
+2 -258
View File
@@ -1,8 +1,6 @@
<script lang="ts">
import { navigate } from 'svelte-routing';
import type { Config } from '@app/config';
import type { Profile } from '@app/profile';
import { ethers } from "ethers";
import * as proj from '@app/project';
import Loading from '@app/Loading.svelte';
import * as utils from '@app/utils';
@@ -23,18 +21,12 @@
export let commit: string;
export let config: Config;
export let path: string;
export let profile: Profile | null = null;
export let anchors: string | null = null;
export let org: string | null = null;
export let user: string | null = null;
export let project: proj.Info;
export let tree: proj.Tree;
// When the component is loaded the first time, the blob is yet to be loaded.
let state: State = { status: Status.Loading, path };
// Whether the clone dropdown is visible.
let cloneDropdown = false;
// Whether the seed dropdown is visible.
let seedDropdown = false;
// Whether the mobile file tree is visible.
let mobileFileTree = false;
@@ -116,149 +108,10 @@
// This is reactive to respond to path changes that don't originate from this
// component, eg. when using the browser's "back" button.
$: getBlob = loadBlob(path);
$: getAnchor = anchors ? getAllAnchors(anchors, urn) : null;
$: loadingPath = state.status == Status.Loading ? state.path : null;
</script>
<style>
main > header {
font-size: 0.75rem;
padding: 0 2rem 0 8rem;
margin-bottom: 2rem;
display: flex;
align-items: center;
justify-content: left;
flex-wrap: wrap;
gap: 0.5rem;
}
main > header > * {
border-radius: 0.25rem;
min-width: max-content;
}
.commit {
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-monospace);
}
.commit .branch {
padding: 0.5rem 0.75rem;
color: var(--color-secondary);
background-color: var(--color-secondary-background);
border-radius: inherit;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.commit .hash {
display: inline-block;
color: var(--color-secondary);
background-color: var(--color-secondary-background);
padding: 0.5rem 0.75rem;
border-radius: inherit;
}
.branch + .hash {
background-color: var(--color-secondary-background-darker);
border-radius: inherit;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.anchor {
display: inline-flex;
}
.anchor-widget {
display: flex;
padding: 0.5rem 0.75rem;
border-radius: inherit;
color: var(--color-tertiary);
background-color: var(--color-tertiary-background);
cursor: pointer;
}
.anchor-widget.not-allowed {
cursor: not-allowed;
}
.anchor-widget.not-anchored {
color: var(--color-foreground-faded);
background-color: var(--color-foreground-background);
}
.anchor-label {
font-family: var(--font-family-monospace);
margin-right: 0.5rem;
}
.anchor-label:last-child {
margin-right: 0;
}
.anchor-latest {
cursor: default;
}
.seed {
cursor: pointer;
border-radius: inherit;
}
.seed:hover {
background-color: var(--color-foreground-background-lighter);
}
.clone {
color: var(--color-primary);
background-color: var(--color-primary-background);
font-family: var(--font-family-monospace);
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
user-select: none;
}
.clone:hover {
background-color: var(--color-primary-background-lighter);
}
.dropdown {
background-color: var(--color-foreground-background);
padding: 1rem;
margin-top: 0.5rem;
border-radius: 0.25rem;
display: none;
position: absolute;
}
.clone-dropdown.clone-dropdown-visible {
display: block;
}
.clone-dropdown input {
color: var(--color-primary);
background: var(--color-primary-background);
}
.seed-dropdown input {
color: var(--color-foreground-90);
background: var(--color-foreground-background-lighter);
}
.seed-dropdown.seed-dropdown-visible {
display: block;
}
.dropdown input {
font-size: 0.75rem;
font-family: var(--font-family-monospace);
padding: 0.5rem;
border: none;
outline: none;
width: 24rem;
text-overflow: ellipsis !important;
border-radius: 0.25rem;
}
.dropdown label {
display: block;
color: var(--color-foreground-faded);
padding: 0.5rem 0.5rem 0 0.25rem;
font-size: 0.75rem;
}
.stat {
font-family: var(--font-family-monospace);
padding: 0.5rem 0.75rem;
background: var(--color-foreground-background);
}
.center-content {
margin: 0 auto;
}
@@ -305,12 +158,9 @@
}
@media (max-width: 960px) {
main > header, .container {
.container {
padding-left: 2rem;
}
main > header {
margin-bottom: 1.5rem;
}
}
@media (max-width: 720px) {
@@ -325,10 +175,6 @@
.source-tree {
padding-top: 1rem;
}
.dropdown {
left: 32px;
z-index: 10;
}
.container {
flex-direction: column;
}
@@ -343,99 +189,6 @@
</style>
<main>
{#await proj.getTree(urn, commit, "/", config)}
<!-- Loading -->
{:then tree}
<header>
<div class="commit">
{#if commit === project.head}
<div class="branch">
{project.meta.defaultBranch}
</div>
<div class="hash">
{utils.formatCommit(commit)}
</div>
{:else}
<div class="hash desktop">
{commit}
</div>
<div class="hash mobile">
{utils.formatCommit(commit)}
</div>
{/if}
</div>
<div class="anchor">
{#if anchors}
{#await getAnchor}
<Loading small margins />
{:then anchor}
{#if anchor}
<!-- commit is head and latest anchor -->
{#if commit == anchor[0] && commit === project.head}
<span class="anchor-widget anchor-latest">
<span class="anchor-label" title="{anchors}">latest 🔐</span>
</span>
<!-- commit is not head but latest anchor -->
{:else if commit == anchor[0] && commit !== project.head}
<span class="anchor-widget" on:click={() => navigateBrowser(project.head)}>
<span class="anchor-label" title="{anchors}">latest 🔐</span>
</span>
<!-- commit is not head a stale anchor -->
{:else if anchor?.includes(commit)}
<span class="anchor-widget" on:click={() => navigateBrowser(anchor[0])}>
<span class="anchor-label" title="{anchors}">stale 🔒</span>
</span>
<!-- commit is not anchored, could be head or any other commit -->
{:else}
<span class="anchor-widget not-anchored" on:click={() => navigateBrowser(anchor[0])}>
<span class="anchor-label">not anchored 🔓</span>
</span>
{/if}
{:else}
<!-- commit is not head and neither an anchor, and there are no anchors available -->
<span class="anchor-widget not-anchored not-allowed">
<span class="anchor-label">not anchored 🔓</span>
</span>
{/if}
{/await}
{/if}
</div>
{#if config.seed.git.host}
<span>
<div class="clone" on:click={() => cloneDropdown = !cloneDropdown}>
Clone
</div>
<div class="dropdown clone-dropdown" class:clone-dropdown-visible={cloneDropdown}>
<input readonly name="clone-url" value="https://{config.seed.git.host}/{utils.parseRadicleId(urn)}"/>
<label for="clone-url">Use Git to clone this repository from the URL above.</label>
</div>
</span>
{/if}
<span>
{#if config.seed.api.host}
<div class="stat seed" on:click={() => seedDropdown = !seedDropdown} title="Project data is fetched from this seed">
<span>{config.seed.api.host}</span>
</div>
{/if}
<div class="dropdown seed-dropdown" class:seed-dropdown-visible={seedDropdown}>
{#if config.seed.link.id && config.seed.link.host}
<input readonly
name="clone-url"
value={utils.formatSeedAddress(config.seed.link.id, config.seed.link.host, config)}/>
<label for="seed-url">Bootstrap your Radicle node with this seed.</label>
{:else if profile}
<label for="#">Seed ID is not set for {profile.name}.</label>
{/if}
</div>
</span>
<div class="stat">
<strong>{tree.stats.commits}</strong> commit(s)
</div>
<div class="stat">
<strong>{tree.stats.contributors}</strong> contributor(s)
</div>
</header>
<!-- Mobile navigation -->
<nav class="mobile">
<button class="small browse secondary center-content" on:click={toggleMobileFileTree}>
@@ -477,13 +230,4 @@
</div>
{/if}
</div>
{:catch err}
<div class="container center-content">
<div class="error error-message text-xsmall">
<!-- TODO: Differentiate between (1) commit doesn't exist and (2) failed
to fetch - this needs a change to the backend. -->
API request to <code class="text-xsmall">{err.url}</code> failed
</div>
</div>
{/await}
</main>
+292
View File
@@ -0,0 +1,292 @@
<script lang="ts">
import type { Config } from '@app/config';
import * as utils from '@app/utils';
import Loading from '@app/Loading.svelte';
import { Org } from '@app/base/orgs/Org';
import type { Info, Tree } from '@app/project';
import type { Profile } from '@app/profile';
import { createEventDispatcher } from "svelte";
export let config: Config;
export let anchors: string | null = null;
export let urn: string;
export let commit: string;
export let project: Info;
export let profile: Profile | null = null;
export let tree: Tree;
// Whether the clone dropdown is visible.
let cloneDropdown = false;
// Whether the seed dropdown is visible.
let seedDropdown = false;
const dispatch = createEventDispatcher();
const onClick = ({ detail: newCommit }: { detail: string }): void => {
dispatch("commitChange", newCommit);
};
$: getAnchor = anchors ? Org.getAnchor(anchors, urn, config) : null;
</script>
<style>
header {
font-size: 0.75rem;
padding: 0 2rem 0 8rem;
margin-bottom: 2rem;
display: flex;
align-items: center;
justify-content: left;
flex-wrap: wrap;
gap: 0.5rem;
}
header > * {
border-radius: 0.25rem;
min-width: max-content;
}
.commit {
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-monospace);
}
.commit .branch {
padding: 0.5rem 0.75rem;
color: var(--color-secondary);
background-color: var(--color-secondary-background);
border-radius: inherit;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.commit .hash {
display: inline-block;
color: var(--color-secondary);
background-color: var(--color-secondary-background);
padding: 0.5rem 0.75rem;
border-radius: inherit;
}
.branch + .hash {
background-color: var(--color-secondary-background-darker);
border-radius: inherit;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.anchor {
display: inline-flex;
}
.anchor-widget {
display: flex;
padding: 0.5rem 0.75rem;
border-radius: inherit;
color: var(--color-tertiary);
background-color: var(--color-tertiary-background);
cursor: pointer;
}
.anchor-widget.not-allowed {
cursor: not-allowed;
}
.anchor-widget.not-anchored {
color: var(--color-foreground-faded);
background-color: var(--color-foreground-background);
}
.anchor-label {
font-family: var(--font-family-monospace);
margin-right: 0.5rem;
}
.anchor-label:last-child {
margin-right: 0;
}
.anchor-latest {
cursor: default;
}
.seed {
cursor: pointer;
border-radius: inherit;
}
.seed:hover {
background-color: var(--color-foreground-background-lighter);
}
.clone {
color: var(--color-primary);
background-color: var(--color-primary-background);
font-family: var(--font-family-monospace);
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
user-select: none;
}
.clone:hover {
background-color: var(--color-primary-background-lighter);
}
.dropdown {
background-color: var(--color-foreground-background);
padding: 1rem;
margin-top: 0.5rem;
border-radius: 0.25rem;
display: none;
position: absolute;
}
.clone-dropdown.clone-dropdown-visible {
display: block;
}
.clone-dropdown input {
color: var(--color-primary);
background: var(--color-primary-background);
}
.seed-dropdown input {
color: var(--color-foreground-90);
background: var(--color-foreground-background-lighter);
}
.seed-dropdown.seed-dropdown-visible {
display: block;
}
.dropdown input {
font-size: 0.75rem;
font-family: var(--font-family-monospace);
padding: 0.5rem;
border: none;
outline: none;
width: 24rem;
text-overflow: ellipsis !important;
border-radius: 0.25rem;
}
.dropdown label {
display: block;
color: var(--color-foreground-faded);
padding: 0.5rem 0.5rem 0 0.25rem;
font-size: 0.75rem;
}
.stat {
font-family: var(--font-family-monospace);
padding: 0.5rem 0.75rem;
background: var(--color-foreground-background);
}
@media (max-width: 960px) {
header {
padding-left: 2rem;
}
header {
margin-bottom: 1.5rem;
}
}
@media (max-width: 720px) {
.dropdown {
left: 32px;
z-index: 10;
}
}
</style>
<header>
<div class="commit">
{#if commit === project.head}
<div class="branch">
{project.meta.defaultBranch}
</div>
<div class="hash">
{utils.formatCommit(commit)}
</div>
{:else}
<div class="hash desktop">
{commit}
</div>
<div class="hash mobile">
{utils.formatCommit(commit)}
</div>
{/if}
</div>
<div class="anchor">
{#if anchors}
{#await getAnchor}
<Loading small margins />
{:then anchor}
{#if anchor === commit}
{#if commit === project.head}
<span class="anchor-widget anchor-latest">
<span class="anchor-label" title={anchors}>anchored 🔒</span>
</span>
{:else}
<span
class="anchor-widget"
on:click={() => onClick({ detail: project.head })}
>
<span class="anchor-label" title={anchors}>anchored 🔒</span>
</span>
{/if}
{:else if anchor}
<span
class="anchor-widget not-anchored"
on:click={() => onClick({ detail: anchor })}
>
<span class="anchor-label">not anchored 🔓</span>
</span>
{:else}
<span class="anchor-widget not-anchored not-allowed">
<span class="anchor-label">not anchored 🔓</span>
</span>
{/if}
{/await}
{/if}
</div>
{#if config.seed.git.host}
<span>
<div class="clone" on:click={() => (cloneDropdown = !cloneDropdown)}>
Clone
</div>
<div
class="dropdown clone-dropdown"
class:clone-dropdown-visible={cloneDropdown}
>
<input
readonly
name="clone-url"
value="https://{config.seed.git.host}/{utils.parseRadicleId(urn)}"
/>
<label for="clone-url"
>Use Git to clone this repository from the URL above.</label
>
</div>
</span>
{/if}
<span>
{#if config.seed.api.host}
<div
class="stat seed"
on:click={() => (seedDropdown = !seedDropdown)}
title="Project data is fetched from this seed"
>
<span>{config.seed.api.host}</span>
</div>
{/if}
<div
class="dropdown seed-dropdown"
class:seed-dropdown-visible={seedDropdown}
>
{#if config.seed.link.id && config.seed.link.host}
<input
readonly
name="clone-url"
value={utils.formatSeedAddress(
config.seed.link.id,
config.seed.link.host,
config
)}
/>
<label for="seed-url">Bootstrap your Radicle node with this seed.</label
>
{:else if profile}
<label for="#">Seed ID is not set for {profile.name}.</label>
{/if}
</div>
</span>
<div class="stat">
<strong>{tree.stats.commits}</strong> commit(s)
</div>
<div class="stat">
<strong>{tree.stats.contributors}</strong> contributor(s)
</div>
</header>
+27 -6
View File
@@ -10,6 +10,7 @@
import { formatOrg } from '@app/utils';
import Browser from './Browser.svelte';
import Header from './Header.svelte';
export let urn: string;
export let org = "";
@@ -46,6 +47,10 @@
: `/users/${profile.nameOrAddress}`;
};
const onCommitChange = ({ detail: newCommit }: { detail: string }): void => {
commit = newCommit;
};
$: if (projectInfo) {
const baseName = parentName
? `${parentName}/${projectInfo.meta.name}`
@@ -142,12 +147,28 @@
<div class="urn">{urn}</div>
<div class="description">{result.project.meta.description}</div>
</header>
<Browser {urn} {org} {user} {path}
anchors={result.profile?.anchorsAccount ?? org}
profile={result.profile}
project={result.project}
commit={commit || result.project.head}
config={result.config} />
{#await proj.getTree(urn, commit, "/", config)}
<!-- Loading -->
{:then tree}
<Header {urn} {tree}
anchors={result.profile?.anchorsAccount ?? org}
commit={commit || result.project.head}
config={result.config}
project={result.project}
on:commitChange={onCommitChange}
/>
<Browser {urn} {org} {user} {path} {tree}
commit={commit || result.project.head}
config={result.config} />
{:catch err}
<div class="container center-content">
<div class="error error-message text-xsmall">
<!-- TODO: Differentiate between (1) commit doesn't exist and (2) failed
to fetch - this needs a change to the backend. -->
API request to <code class="text-xsmall">{err.url}</code> failed
</div>
</div>
{/await}
{:catch}
<Modal subtle>
<span slot="title">🏜️</span>