Implement repo home page

- by default open repos on the home view
- show a rendered README on the repo home page
- show repo metadata: visibility, canonical branch, threshold, delegates
  and allow list with aliases
- make layout responsive on repo home
- add copy button to code blocks in markdown
This commit is contained in:
Rūdolfs Ošiņš
2025-05-06 10:11:01 +02:00
parent 865497ce38
commit bb38192b7e
32 changed files with 705 additions and 60 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ use std::process::Command;
fn main() {
let output = Command::new("git")
.args(&["rev-parse", "--short", "HEAD"])
.args(["rev-parse", "--short", "HEAD"])
.output()
.expect("failed to execute git");
let git_head = String::from_utf8(output.stdout).unwrap();
@@ -35,6 +35,15 @@ pub fn repo_by_id(
ctx.repo_by_id(rid)
}
#[tauri::command]
pub fn repo_readme(
ctx: tauri::State<AppState>,
rid: RepoId,
sha: Option<git::Oid>,
) -> Result<Option<types::repo::Readme>, Error> {
ctx.repo_readme(rid, sha)
}
#[tauri::command]
pub async fn diff_stats(
ctx: tauri::State<'_, AppState>,
+1
View File
@@ -56,6 +56,7 @@ pub fn run() {
repo::list_repos,
repo::repo_by_id,
repo::repo_count,
repo::repo_readme,
startup::startup,
startup::version,
thread::create_issue_comment,
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Readme = { path: string; content: string; binary: boolean };
@@ -1,6 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "../cob/Author";
export type Visibility = { "type": "public" } | {
"type": "private";
allow?: Array<string>;
allow?: Array<Author>;
};
+20 -13
View File
@@ -37,6 +37,16 @@ pub struct RepoInfo {
pub last_commit_timestamp: i64,
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "repo/")]
pub struct Readme {
pub path: String,
pub content: String,
pub binary: bool,
}
#[derive(Default, Serialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export)]
@@ -47,25 +57,22 @@ pub enum Visibility {
Public,
/// Delegates plus the allowed DIDs.
Private {
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
#[ts(as = "Option<BTreeSet<String>>", optional)]
allow: BTreeSet<identity::Did>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<Vec<Author>>", optional)]
allow: Vec<Author>,
},
}
impl From<identity::Visibility> for Visibility {
fn from(value: identity::Visibility) -> Self {
match value {
identity::Visibility::Private { allow } => Self::Private { allow },
identity::Visibility::Public => Self::Public,
}
}
}
impl From<Visibility> for identity::Visibility {
fn from(value: Visibility) -> Self {
match value {
Visibility::Private { allow } => Self::Private { allow },
Visibility::Private { allow } => {
let did_set = allow
.iter()
.map(|author| *author.did())
.collect::<BTreeSet<identity::Did>>();
Self::Private { allow: did_set }
}
Visibility::Public => Self::Public,
}
}
+53 -1
View File
@@ -1,3 +1,4 @@
use base64::Engine;
use radicle_surf as surf;
use serde::{Deserialize, Serialize};
@@ -108,6 +109,49 @@ pub trait Repo: Profile {
})
}
fn repo_readme(
&self,
rid: identity::RepoId,
sha: Option<git::Oid>,
) -> Result<Option<repo::Readme>, Error> {
let profile = self.profile();
let repo = radicle_surf::Repository::open(storage::git::paths::repository(
&profile.storage,
&rid,
))?;
let paths = [
"README",
"README.md",
"README.markdown",
"README.txt",
"README.rst",
"README.org",
"Readme.md",
];
let oid = sha.map_or_else(|| repo.head(), Ok)?;
for path in paths
.iter()
.map(ToString::to_string)
.chain(paths.iter().map(|p| p.to_lowercase()))
{
if let Ok(blob) = repo.blob(oid, &path) {
let content = match std::str::from_utf8(blob.content()) {
Ok(s) => s.to_owned(),
Err(_) => base64::engine::general_purpose::STANDARD.encode(blob.content()),
};
return Ok(Some(repo::Readme {
path,
content,
binary: blob.is_binary(),
}));
}
}
Ok(None)
}
fn repo_by_id(&self, rid: identity::RepoId) -> Result<repo::RepoInfo, Error> {
let profile = self.profile();
let repo = profile.storage.repository(rid)?;
@@ -176,7 +220,15 @@ pub trait Repo: Profile {
payloads: repo::SupportedPayloads { project },
delegates,
threshold: doc.threshold(),
visibility: doc.visibility().clone().into(),
visibility: match doc.visibility().clone() {
identity::Visibility::Public => repo::Visibility::Public,
identity::Visibility::Private { allow } => repo::Visibility::Private {
allow: allow
.iter()
.map(|did| cobs::Author::new(did, &aliases))
.collect(),
},
},
rid: repo.id,
seeding,
last_commit_timestamp: commit.time().seconds() * 1000,
+48
View File
@@ -257,3 +257,51 @@ body {
0 calc(100% - 6px)
);
}
/*
Breakpoints
===========
mobile 0px - 719.98px
small desktop 720px - 1010.98px
medium desktop 1011px - 1349.98px
desktop 1350px - ∞ px
*/
@media (max-width: 719.98px) {
body {
min-width: 0;
}
.global-hide-on-mobile-down {
display: none !important;
}
}
@media (max-width: 1010.98px) {
.global-hide-on-small-desktop-down {
display: none !important;
}
}
@media (max-width: 1349.98px) {
.global-hide-on-medium-desktop-down {
display: none !important;
}
}
@media (min-width: 720px) {
.global-hide-on-small-desktop-up {
display: none !important;
}
}
@media (min-width: 1011px) {
.global-hide-on-medium-desktop-up {
display: none !important;
}
}
@media (min-width: 1350px) {
.global-hide-on-desktop-up {
display: none !important;
}
}
+5
View File
@@ -136,6 +136,11 @@ p {
.txt-missing {
color: var(--color-foreground-dim);
}
.txt-selectable {
-webkit-touch-callout: initial;
-webkit-user-select: text;
user-select: text;
}
.txt-emoji {
height: 1em;
width: 1em;
+1 -1
View File
@@ -2,6 +2,6 @@
set -e
npx tsc --noEmit
npx svelte-check --tsconfig tsconfig.json --fail-on-warnings --compiler-warnings missing-custom-element-compile-options:ignore
npx svelte-check --tsconfig tsconfig.json --fail-on-warnings --compiler-warnings options_missing_custom_element:ignore
npx eslint --cache --cache-location node_modules/.cache/eslint --max-warnings 0 .
npx prettier "**/*.@(ts|js|svelte|json|css|html|yml)" "!crates/**/*" --ignore-path .gitignore --check --cache
+3
View File
@@ -27,6 +27,7 @@
import Issues from "@app/views/repo/Issues.svelte";
import Patch from "@app/views/repo/Patch.svelte";
import Patches from "@app/views/repo/Patches.svelte";
import RepoHome from "@app/views/repo/RepoHome.svelte";
import Repos from "@app/views/home/Repos.svelte";
const activeRouteStore = router.activeRouteStore;
@@ -109,6 +110,8 @@
<Repos {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "inbox"}
<Inbox {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.home"}
<RepoHome {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.createIssue"}
<CreateIssue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issue"}
+2
View File
@@ -1,3 +1,5 @@
<svelte:options customElement="radicle-clipboard" />
<script lang="ts">
import debounce from "lodash/debounce";
+21 -16
View File
@@ -8,8 +8,9 @@
interface Props {
children: Snippet;
expanded: boolean;
leftHeader: Snippet;
expandable?: boolean;
expanded?: boolean;
leftHeader?: Snippet;
rightHeader?: Snippet;
sticky?: boolean;
}
@@ -17,10 +18,11 @@
/* eslint-disable prefer-const */
let {
children,
expanded,
expanded = true,
leftHeader,
rightHeader,
sticky = true,
expandable = true,
}: Props = $props();
/* eslint-enable prefer-const */
@@ -36,6 +38,7 @@
z-index: 2;
font-size: var(--font-size-small);
background-color: var(--color-background-default);
position: relative;
}
.header::after {
position: absolute;
@@ -84,19 +87,21 @@
<div class="header" class:sticky class:collapsed={!expanded} bind:this={header}>
<div class="left">
<NakedButton
stylePadding="0 4px"
variant="ghost"
onclick={async () => {
expanded = !expanded;
if (!expanded && header) {
await tick();
header.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}}>
<Icon name={expanded ? "chevron-down" : "chevron-right"} />
</NakedButton>
{@render leftHeader()}
{#if expandable}
<NakedButton
stylePadding="0 4px"
variant="ghost"
onclick={async () => {
expanded = !expanded;
if (!expanded && header) {
await tick();
header.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}}>
<Icon name={expanded ? "chevron-down" : "chevron-right"} />
</NakedButton>
{/if}
{@render leftHeader?.()}
</div>
{#if rightHeader}
<div
+13 -2
View File
@@ -58,8 +58,19 @@
<div class="container">
<div>
<div style:margin-bottom="1rem" style:padding-left="0.75rem">
<RepoTeaser name={project.data.name} seeding={repo.seeding} />
<div style:margin-bottom="0.75rem">
<Link
styleWidth="100%"
underline={false}
route={{ resource: "repo.home", rid: repo.rid }}>
<div
class="tab"
style:color="var(--color-foreground-contrast)"
style:padding-right="0.5rem"
style:padding-left="0.75rem">
<RepoTeaser name={project.data.name} seeding={repo.seeding} />
</div>
</Link>
</div>
<Border
+11 -2
View File
@@ -141,6 +141,15 @@
const treeChanges: Promise<void>[] = [];
for (const node of nodes) {
const preElement = node.parentElement as HTMLElement;
const copyButton = document.createElement("radicle-clipboard");
copyButton.setAttribute("text", node.textContent || "");
const preWrapper = document.createElement("div");
preWrapper.classList.add("pre-wrapper");
preElement.parentNode?.insertBefore(preWrapper, preElement);
preWrapper.appendChild(preElement);
preWrapper.appendChild(copyButton);
const className = Array.from(node.classList).find(name =>
name.startsWith(prefix),
);
@@ -217,8 +226,8 @@
.markdown :global(radicle-clipboard) {
display: none;
position: absolute;
right: 0.5rem;
top: 0.5rem;
right: 0.75rem;
top: 0.75rem;
}
.markdown :global(radicle-clipboard) {
+13 -2
View File
@@ -63,8 +63,19 @@
<div class="container">
<div>
<div style:margin-bottom="1rem" style:padding-left="0.75rem">
<RepoTeaser name={project.data.name} seeding={repo.seeding} />
<div style:margin-bottom="0.75rem">
<Link
styleWidth="100%"
underline={false}
route={{ resource: "repo.home", rid: repo.rid }}>
<div
class="tab"
style:color="var(--color-foreground-contrast)"
style:padding-right="0.5rem"
style:padding-left="0.75rem">
<RepoTeaser name={project.data.name} seeding={repo.seeding} />
</div>
</Link>
</div>
<div style:margin-bottom="0.5rem">
+105
View File
@@ -0,0 +1,105 @@
<script lang="ts">
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import Border from "./Border.svelte";
import Icon from "./Icon.svelte";
import Link from "./Link.svelte";
import RepoTeaser from "./RepoTeaser.svelte";
import Settings from "./Settings.svelte";
interface Props {
repo: RepoInfo;
}
const { repo }: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
.container {
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
.tab {
align-items: center;
background-color: var(--color-background-float);
clip-path: var(--1px-corner-fill);
display: flex;
font-size: var(--font-size-small);
justify-content: space-between;
padding: 0.5rem 0.25rem 0.5rem 0.5rem;
width: 100%;
}
.tab:not(.active) {
color: var(--color-foreground-dim);
}
.tab:not(.active):hover {
background-color: var(--color-fill-float-hover);
}
.active {
background-color: var(--color-background-default);
font-weight: var(--font-weight-semibold);
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
}
</style>
<div class="container">
<div>
<div style:margin-bottom="0.75rem">
<Border
variant="ghost"
styleBackgroundColor="var(--color-background-default)">
<div class="tab active" style:color="var(--color-foreground-contrast)">
<RepoTeaser name={project.data.name} seeding={repo.seeding} />
</div>
</Border>
</div>
<div style:margin-bottom="0.5rem">
<Link
styleWidth="100%"
underline={false}
route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
<div
class="tab"
style:color="var(--color-foreground-contrast)"
style:padding-left="0.75rem">
<div class="global-flex"><Icon name="issue" />Issues</div>
<div class="global-counter">
{project.meta.issues.open + project.meta.issues.closed}
</div>
</div>
</Link>
</div>
<div style:margin-top="0.5rem">
<Link
styleWidth="100%"
underline={false}
route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
<div
class="tab"
style:color="var(--color-foreground-contrast)"
style:padding-left="0.75rem">
<div class="global-flex"><Icon name="patch" />Patches</div>
<div class="global-counter">
{project.meta.patches.draft +
project.meta.patches.open +
project.meta.patches.archived +
project.meta.patches.merged}
</div>
</div>
</Link>
</div>
</div>
<Settings
compact={false}
popoverProps={{
popoverPositionBottom: "3rem",
popoverPositionLeft: "0",
}} />
</div>
+128
View File
@@ -0,0 +1,128 @@
<script lang="ts">
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import sortBy from "lodash/sortBy.js";
import { authorForNodeId } from "@app/lib/utils";
import Border from "@app/components/Border.svelte";
import Id from "@app/components/Id.svelte";
import NodeId from "@app/components/NodeId.svelte";
import VisibilityBadge from "@app/components/VisibilityBadge.svelte";
import Icon from "./Icon.svelte";
interface Props {
horizontal?: boolean;
repo: RepoInfo;
}
const { horizontal = false, repo }: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
<style>
.list {
flex-direction: column;
align-items: flex-start;
}
.horizontal {
flex-direction: row;
flex-wrap: wrap;
}
.metadata-divider {
width: 2px;
background-color: var(--color-fill-ghost);
height: calc(100% + 4px);
top: 0;
position: relative;
}
.metadata-divider-horizontal {
height: 2px;
background-color: var(--color-fill-ghost);
width: calc(100% + 4px);
top: 0;
left: -2px;
position: relative;
}
.metadata-section {
padding: 0.5rem;
font-size: var(--font-size-small);
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
white-space: nowrap;
}
.metadata-section-title {
margin-bottom: 0.5rem;
color: var(--color-foreground-dim);
}
</style>
<Border
variant="ghost"
styleGap="0"
styleFlexDirection="column"
styleAlignItems="flex-start">
<div class="metadata-section" style:flex="1" style:width="100%">
<div class="metadata-section-title">Visibility</div>
<VisibilityBadge type={repo.visibility.type} />
</div>
{#if repo.visibility.type === "private"}
<div class="metadata-divider-horizontal"></div>
<div class="metadata-section" style:flex="1" style:width="100%">
<div class="metadata-section-title">Allow</div>
{#if repo.visibility.allow}
<div class="global-flex list" class:horizontal>
{#each repo.visibility.allow as node}
<NodeId {...authorForNodeId(node)} />
{/each}
</div>
{:else}
<div class="global-flex txt-missing" style:gap="0.25rem">
<Icon name="none" /> None
</div>
{/if}
</div>
{/if}
<div class="metadata-divider-horizontal"></div>
<div class="metadata-section" style:flex="1" style:width="100%">
<div class="metadata-section-title">Delegates</div>
<div class="global-flex list" class:horizontal>
{#each sortBy(repo.delegates, d => {
return d.alias?.toLowerCase();
}) as delegate}
<NodeId {...authorForNodeId(delegate)} />
{/each}
</div>
</div>
<div class="metadata-divider-horizontal"></div>
<div
class="global-flex"
style:gap="0"
style:flex-direction="row"
style:width="100%">
<div class="metadata-section">
<div class="metadata-section-title">Default branch</div>
<span>
<span class="txt-selectable">{project.data.defaultBranch}</span>
->
<Id id={project.meta.head} variant="commit" />
</span>
</div>
<div class="metadata-divider"></div>
<div class="metadata-section">
<div class="metadata-section-title">Threshold</div>
{repo.threshold}
</div>
</div>
</Border>
+1 -1
View File
@@ -12,7 +12,7 @@
<style>
.teaser {
align-items: center;
min-height: 2.5rem;
width: 100%;
}
.seeding {
+10 -1
View File
@@ -39,7 +39,16 @@
<div class="global-flex" style:flex-direction="column" style:gap="0.5rem">
<div class="global-flex" style:height="2.5rem">
<Icon name="repo" />
<button
class="sidebar-button"
onclick={() => {
void router.push({
resource: "repo.home",
rid,
});
}}>
<Icon name="repo" />
</button>
</div>
{#if activeTab === "issues"}
<Border
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import type { Visibility } from "@bindings/repo/Visibility";
import capitalize from "lodash/capitalize.js";
import Icon from "@app/components/Icon.svelte";
interface Props {
type: Visibility["type"];
}
const { type }: Props = $props();
</script>
<style>
.badge {
gap: 0.375rem;
padding-right: 0.625rem;
}
.public {
background-color: var(--color-fill-diff-green-light);
color: var(--color-foreground-success);
}
.private {
background-color: var(--color-fill-private);
color: var(--color-foreground-yellow);
}
</style>
<span
class="global-counter badge"
class:public={type === "public"}
class:private={type === "private"}>
<Icon name={type === "public" ? "seedling" : "lock"} />
{capitalize(type)}
</span>
+1
View File
@@ -125,6 +125,7 @@ export function routeToPath(route: Route): string {
} else if (route.resource === "inbox") {
return "/inbox";
} else if (
route.resource === "repo.home" ||
route.resource === "repo.createIssue" ||
route.resource === "repo.issue" ||
route.resource === "repo.issues" ||
+3
View File
@@ -15,6 +15,7 @@ import {
loadIssues,
loadPatch,
loadPatches,
loadRepoHome,
} from "@app/views/repo/router";
export type HomeReposTab = "delegate" | "private" | "contributor";
@@ -153,6 +154,8 @@ export async function loadRoute(
config,
},
};
} else if (route.resource === "repo.home") {
return loadRepoHome(route);
} else if (route.resource === "repo.issue") {
return loadIssue(route);
} else if (route.resource === "repo.createIssue") {
+2 -4
View File
@@ -128,9 +128,8 @@
onSubmit={async () => {
if (searchResults.length === 1) {
await router.push({
resource: "repo.issues",
resource: "repo.home",
rid: searchResults[0].obj.repo.rid,
status: "open",
});
}
}}
@@ -161,9 +160,8 @@
selfDid={didFromPublicKey(config.publicKey)}
onclick={() => {
void router.push({
resource: "repo.issues",
resource: "repo.home",
rid: result.obj.repo.rid,
status: "open",
});
}} />
{/each}
-5
View File
@@ -17,7 +17,6 @@
import Icon from "@app/components/Icon.svelte";
import IssueTeaser from "@app/components/IssueTeaser.svelte";
import IssuesSecondColumn from "@app/components/IssuesSecondColumn.svelte";
import Sidebar from "@app/components/Sidebar.svelte";
import TextInput from "@app/components/TextInput.svelte";
interface Props {
@@ -92,10 +91,6 @@
<CopyableId id={repo.rid} />
{/snippet}
{#snippet sidebar()}
<Sidebar activeTab="issues" rid={repo.rid} />
{/snippet}
{#snippet secondColumn()}
<IssuesSecondColumn {status} {repo} />
{/snippet}
+2 -2
View File
@@ -565,9 +565,9 @@
<Popover
bind:expanded={checkoutPopoverExpanded}
popoverPositionRight="0"
popoverPositionTop="2.5rem">
popoverPositionTop="3rem">
{#snippet toggle(onclick)}
<Button styleHeight="2rem" variant="secondary" {onclick}>
<Button styleHeight="2.5rem" variant="secondary" {onclick}>
<Icon name="checkout" />Checkout<Icon name="chevron-down" />
</Button>
{/snippet}
-5
View File
@@ -18,7 +18,6 @@
import Layout from "./Layout.svelte";
import PatchTeaser from "@app/components/PatchTeaser.svelte";
import PatchesSecondColumn from "@app/components/PatchesSecondColumn.svelte";
import Sidebar from "@app/components/Sidebar.svelte";
import TextInput from "@app/components/TextInput.svelte";
interface Props {
@@ -125,10 +124,6 @@
<CopyableId id={repo.rid} />
{/snippet}
{#snippet sidebar()}
<Sidebar activeTab="patches" rid={repo.rid} />
{/snippet}
{#snippet secondColumn()}
<PatchesSecondColumn {project} {status} {repo} />
{/snippet}
+154
View File
@@ -0,0 +1,154 @@
<script lang="ts">
import type { Config } from "@bindings/config/Config";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { Readme } from "@bindings/repo/Readme";
import Border from "@app/components/Border.svelte";
import Button from "@app/components/Button.svelte";
import Command from "@app/components/Command.svelte";
import CopyableId from "@app/components/CopyableId.svelte";
import File from "@app/components/File.svelte";
import Icon from "@app/components/Icon.svelte";
import Layout from "./Layout.svelte";
import Markdown from "@app/components/Markdown.svelte";
import Path from "@app/components/Path.svelte";
import Popover from "@app/components/Popover.svelte";
import RepoHomeSecondColumn from "@app/components/RepoHomeSecondColumn.svelte";
import RepoMetadata from "@app/components/RepoMetadata.svelte";
interface Props {
config: Config;
readme: Readme | null;
repo: RepoInfo;
}
const { config, readme, repo }: Props = $props();
const project = $derived(repo.payloads["xyz.radicle.project"]!);
let checkoutPopoverExpanded: boolean = $state(false);
</script>
<style>
.content {
padding: 1rem 1rem 1rem 0;
}
.container {
display: grid;
grid-template-columns: 1fr min-content;
grid-template-areas: "main-content right-sidebar";
margin-top: 2rem;
}
</style>
<Layout
publicKey={config.publicKey}
hideSidebar
styleSecondColumnOverflow="visible">
{#snippet headerCenter()}
<CopyableId id={repo.rid} />
{/snippet}
{#snippet secondColumn()}
<RepoHomeSecondColumn {repo} />
{/snippet}
<div class="content">
<div class="global-flex">
<div
class="txt-medium txt-selectable"
style:font-weight="var(--font-weight-medium)">
{project.data.name}
</div>
<div class="global-flex txt-small" style:margin-left="auto">
<Popover
bind:expanded={checkoutPopoverExpanded}
popoverPositionRight="0"
popoverPositionTop="3rem">
{#snippet toggle(onclick)}
<Button styleHeight="2.5rem" variant="secondary" {onclick}>
<Icon name="checkout" />Checkout<Icon name="chevron-down" />
</Button>
{/snippet}
{#snippet popover()}
<Border
styleAlignItems="flex-start"
styleBackgroundColor="var(--color-background-float)"
styleFlexDirection="column"
styleGap="0.5rem"
stylePadding="1rem"
styleWidth="max-content"
variant="ghost">
To checkout a working copy of this repo, run:
<Command command={`rad checkout ${repo.rid}`} styleWidth="100%" />
</Border>
{/snippet}
</Popover>
</div>
</div>
{#if project.data.description !== ""}
<Markdown rid={repo.rid} breaks content={project.data.description} />
{/if}
<div class="global-hide-on-desktop-up" style:margin-top="1rem">
<RepoMetadata {repo} horizontal />
</div>
<div class="container">
<div style:grid-area="main-content" style:min-width="0">
{#if readme === null}
<Border
variant="ghost"
stylePadding="1rem"
styleAlignItems="center"
styleMinHeight="10rem">
<div
class="global-flex txt-missing"
style:width="100%"
style:justify-content="center">
<Icon name="none" />README not found
</div>
</Border>
{:else}
<File expandable={false} sticky={false}>
{#snippet leftHeader()}
<div style:margin-left="0.5rem">
<Path fullPath={readme.path} />
</div>
{/snippet}
<div style:padding="1rem">
{#if readme.binary}
<div
class="global-flex txt-missing"
style:width="100%"
style:justify-content="center">
<Icon name="binary" />Binary file
</div>
{:else if readme.content.trim() === ""}
<div
class="global-flex txt-missing"
style:width="100%"
style:justify-content="center">
<Icon name="none" />Empty file
</div>
{:else}
<Markdown rid={repo.rid} content={readme.content} />
{/if}
</div>
</File>
{/if}
</div>
<div
class="global-hide-on-medium-desktop-down"
style:grid-area="right-sidebar"
style:margin-left="1rem"
style:min-width="20rem">
<RepoMetadata {repo} />
</div>
</div>
</div>
</Layout>
+43 -2
View File
@@ -5,6 +5,7 @@ import type { Issue } from "@bindings/cob/issue/Issue";
import type { Operation } from "@bindings/cob/Operation";
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
import type { Patch } from "@bindings/cob/patch/Patch";
import type { Readme } from "@bindings/repo/Readme";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { Review } from "@bindings/cob/patch/Review";
import type { Revision } from "@bindings/cob/patch/Revision";
@@ -17,6 +18,11 @@ export type IssueStatus = "all" | Issue["state"]["status"];
export const DEFAULT_TAKE = 20;
export interface RepoHomeRoute {
resource: "repo.home";
rid: string;
}
export interface RepoIssueRoute {
resource: "repo.issue";
rid: string;
@@ -30,6 +36,15 @@ export interface RepoCreateIssueRoute {
status: IssueStatus;
}
export interface LoadedRepoHomeRoute {
resource: "repo.home";
params: {
repo: RepoInfo;
config: Config;
readme: Readme | null;
};
}
export interface LoadedRepoIssueRoute {
resource: "repo.issue";
params: {
@@ -110,12 +125,14 @@ export interface LoadedRepoPatchesRoute {
}
export type RepoRoute =
| RepoHomeRoute
| RepoCreateIssueRoute
| RepoIssueRoute
| RepoIssuesRoute
| RepoPatchRoute
| RepoPatchesRoute;
export type LoadedRepoRoute =
| LoadedRepoHomeRoute
| LoadedRepoCreateIssueRoute
| LoadedRepoIssueRoute
| LoadedRepoIssuesRoute
@@ -191,6 +208,25 @@ export async function loadPatches(
};
}
export async function loadRepoHome(
route: RepoHomeRoute,
): Promise<LoadedRepoHomeRoute> {
const [config, repo, readme] = await Promise.all([
invoke<Config>("config"),
invoke<RepoInfo>("repo_by_id", {
rid: route.rid,
}),
invoke<Readme | null>("repo_readme", {
rid: route.rid,
}),
]);
return {
resource: "repo.home",
params: { repo, config, readme },
};
}
export async function loadCreateIssue(
route: RepoCreateIssueRoute,
): Promise<LoadedRepoCreateIssueRoute> {
@@ -275,7 +311,10 @@ export function repoRouteToPath(route: RepoRoute): string {
const pathSegments = ["/repos", route.rid];
const searchParams = new URLSearchParams();
if (route.resource === "repo.issue") {
if (route.resource === "repo.home") {
const url = [...pathSegments, "home"].join("/");
return url;
} else if (route.resource === "repo.issue") {
let url = [...pathSegments, "issues", route.issue].join("/");
searchParams.set("status", route.status);
url += `?${searchParams}`;
@@ -322,7 +361,9 @@ export function repoUrlToRoute(
const resource = segments.shift();
if (rid) {
if (resource === "issues") {
if (resource === "home") {
return { resource: "repo.home", rid };
} else if (resource === "issues") {
const idOrAction = segments.shift();
if (idOrAction) {
const status = (searchParams.get("status") ?? "all") as IssueStatus;
+2
View File
@@ -10,6 +10,7 @@ test("navigate single issue", async ({ page }) => {
test("correct order of threads", async ({ page }) => {
await page.goto("/repos");
await page.getByRole("button", { name: "cobs" }).click();
await page.getByRole("link", { name: "icon-issue Issues" }).click();
await page.getByText("This title has **markdown**").click();
const body = page.locator(".issue-body");
await expect(body.getByText("This is a description")).toBeVisible();
@@ -30,6 +31,7 @@ test("correct order of threads", async ({ page }) => {
test("creation of top level comments", async ({ page }) => {
await page.goto("/repos");
await page.getByRole("button", { name: "cobs" }).click();
await page.getByRole("link", { name: "icon-issue Issues" }).click();
await page.getByRole("button", { name: "New" }).click();
await page
.getByPlaceholder("Title")
+1
View File
@@ -3,6 +3,7 @@ import { expect, test } from "@tests/support/fixtures.js";
test("navigate to repo issues", async ({ page }) => {
await page.goto("/repos");
await page.getByRole("button", { name: "cobs" }).click();
await page.getByRole("link", { name: "icon-issue Issues" }).click();
await page.getByText("This title has **markdown**").click();
await expect(
page.getByText("This title has **markdown**").nth(1),
+10 -1
View File
@@ -9,7 +9,16 @@ export default defineConfig({
include: ["tests/unit/**/*.test.ts"],
reporters: "verbose",
},
plugins: [svelte()],
plugins: [
svelte({
// Reference: https://github.com/sveltejs/vite-plugin-svelte/issues/270#issuecomment-1033190138
dynamicCompileOptions({ filename }) {
if (path.basename(filename) === "Clipboard.svelte") {
return { customElement: true };
}
},
}),
],
build: {
outDir: "build",
},