mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
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:
@@ -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>,
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<svelte:options customElement="radicle-clipboard" />
|
||||
|
||||
<script lang="ts">
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
|
||||
+21
-16
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -12,7 +12,7 @@
|
||||
<style>
|
||||
.teaser {
|
||||
align-items: center;
|
||||
min-height: 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.seeding {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user