mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-29 08:10:59 +08:00
Add infinite scrolling for patches listing
This commit is contained in:
@@ -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 PaginatedQuery<T> = { cursor: number; more: boolean; content: T };
|
||||
@@ -34,17 +34,25 @@ mod query {
|
||||
Draft,
|
||||
Archived,
|
||||
Merged,
|
||||
All,
|
||||
}
|
||||
|
||||
impl PatchStatus {
|
||||
pub fn matches(&self, patch: &patch::State) -> bool {
|
||||
match self {
|
||||
Self::Open => matches!(patch, patch::State::Open { .. }),
|
||||
Self::Draft => matches!(patch, patch::State::Draft),
|
||||
Self::Archived => matches!(patch, patch::State::Archived),
|
||||
Self::Merged => matches!(patch, patch::State::Merged { .. }),
|
||||
Self::All => true,
|
||||
impl From<patch::Status> for PatchStatus {
|
||||
fn from(value: patch::Status) -> Self {
|
||||
match value {
|
||||
patch::Status::Archived => Self::Archived,
|
||||
patch::Status::Draft => Self::Draft,
|
||||
patch::Status::Merged => Self::Merged,
|
||||
patch::Status::Open => Self::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PatchStatus> for patch::Status {
|
||||
fn from(value: PatchStatus) -> Self {
|
||||
match value {
|
||||
PatchStatus::Archived => Self::Archived,
|
||||
PatchStatus::Draft => Self::Draft,
|
||||
PatchStatus::Merged => Self::Merged,
|
||||
PatchStatus::Open => Self::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,59 @@ use radicle::git;
|
||||
use radicle::identity::RepoId;
|
||||
use radicle::patch::cache::Patches;
|
||||
use radicle::storage::ReadStorage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::cob::query;
|
||||
use crate::error::Error;
|
||||
use crate::types::cobs;
|
||||
use crate::AppState;
|
||||
|
||||
use crate::cob::query;
|
||||
|
||||
#[derive(Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct PaginatedQuery<T> {
|
||||
pub cursor: usize,
|
||||
pub more: bool,
|
||||
pub content: T,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_patches(
|
||||
ctx: tauri::State<AppState>,
|
||||
pub async fn list_patches(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: RepoId,
|
||||
status: query::PatchStatus,
|
||||
) -> Result<Vec<cobs::Patch>, Error> {
|
||||
status: Option<query::PatchStatus>,
|
||||
skip: Option<usize>,
|
||||
take: Option<usize>,
|
||||
) -> Result<PaginatedQuery<Vec<cobs::Patch>>, Error> {
|
||||
let cursor = skip.unwrap_or(0);
|
||||
let take = take.unwrap_or(20);
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let patches = ctx.profile.patches(&repo)?;
|
||||
let mut patches: Vec<_> = patches
|
||||
.list()?
|
||||
.filter_map(|r| {
|
||||
let (id, patch) = r.ok()?;
|
||||
(status.matches(patch.state())).then_some((id, patch))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
patches.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let patches = patches
|
||||
let cache = ctx.profile.patches(&repo)?;
|
||||
let patches = match status {
|
||||
None => cache.list()?.collect::<Vec<_>>(),
|
||||
Some(s) => cache.list_by_status(&s.into())?.collect::<Vec<_>>(),
|
||||
};
|
||||
let more = cursor + take < patches.len();
|
||||
|
||||
let mut patches = patches
|
||||
.into_iter()
|
||||
.map(|(id, patch)| cobs::Patch::new(id, patch, aliases))
|
||||
.filter_map(|p| {
|
||||
p.map(|(id, patch)| cobs::Patch::new(id, patch, aliases))
|
||||
.ok()
|
||||
})
|
||||
.skip(cursor)
|
||||
.take(take)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(patches)
|
||||
patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp()));
|
||||
|
||||
Ok::<_, Error>(PaginatedQuery {
|
||||
cursor,
|
||||
more,
|
||||
content: patches,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -119,6 +119,10 @@ impl Patch {
|
||||
revision_count: patch.revisions().count(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.timestamp.as_millis()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
patchStatusColor,
|
||||
} from "@app/lib/utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMount } from "svelte";
|
||||
import { push } from "@app/lib/router";
|
||||
|
||||
import DiffStatBadge from "./DiffStatBadge.svelte";
|
||||
@@ -16,6 +17,16 @@
|
||||
import InlineTitle from "./InlineTitle.svelte";
|
||||
import NodeId from "./NodeId.svelte";
|
||||
|
||||
let stats: Stats | undefined = undefined;
|
||||
|
||||
onMount(async () => {
|
||||
stats = await invoke<Stats>("diff_stats", {
|
||||
rid,
|
||||
base: patch.base,
|
||||
head: patch.head,
|
||||
});
|
||||
});
|
||||
|
||||
export let patch: Patch;
|
||||
export let rid: string;
|
||||
</script>
|
||||
@@ -81,9 +92,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="global-flex">
|
||||
{#await invoke<Stats>( "diff_stats", { rid, base: patch.base, head: patch.head }, ) then stats}
|
||||
{#if stats}
|
||||
<DiffStatBadge {stats} />
|
||||
{/await}
|
||||
{/if}
|
||||
{#each patch.labels as label}
|
||||
<div class="global-counter txt-small">{label}</div>
|
||||
{/each}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
|
||||
<Layout>
|
||||
<svelte:fragment slot="breadcrumbs">
|
||||
<Link route={{ resource: "home" }}>
|
||||
<NodeId
|
||||
|
||||
@@ -100,9 +100,7 @@
|
||||
</div>
|
||||
|
||||
<div class="global-flex txt-small" style:margin="0.5rem 0">
|
||||
<Link
|
||||
variant="tab"
|
||||
route={{ resource: "repo.patches", rid: repo.rid, status: "all" }}>
|
||||
<Link variant="tab" route={{ resource: "repo.patches", rid: repo.rid }}>
|
||||
<div class="global-flex"><Icon name="patch" />Patches</div>
|
||||
<div class="global-counter">
|
||||
{project.meta.patches.draft +
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Header from "@app/components/Header.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
|
||||
export let loadMore: (() => Promise<void>) | undefined = undefined;
|
||||
|
||||
let hidden = false;
|
||||
let listElement: HTMLElement;
|
||||
let loading = false;
|
||||
|
||||
onMount(() => {
|
||||
if (listElement && loadMore) {
|
||||
listElement.addEventListener("scroll", async () => {
|
||||
if (
|
||||
listElement.scrollTop + listElement.clientHeight >=
|
||||
listElement.scrollHeight - 600 &&
|
||||
loading === false
|
||||
) {
|
||||
loading = true;
|
||||
void loadMore().finally(() => (loading = false));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -64,7 +85,7 @@
|
||||
<slot name="sidebar" />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content" bind:this={listElement}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
|
||||
<Layout>
|
||||
<svelte:fragment slot="breadcrumbs">
|
||||
<Link route={{ resource: "home" }}>
|
||||
<NodeId
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from "@bindings/Config";
|
||||
import type { PaginatedQuery } from "@bindings/PaginatedQuery";
|
||||
import type { Patch } from "@bindings/Patch";
|
||||
import type { PatchStatus } from "./router";
|
||||
import type { RepoInfo } from "@bindings/RepoInfo";
|
||||
|
||||
import Layout from "./Layout.svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import Layout from "./Layout.svelte";
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import CopyableId from "@app/components/CopyableId.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
@@ -15,9 +17,28 @@
|
||||
import RepoHeader from "@app/components/RepoHeader.svelte";
|
||||
|
||||
export let repo: RepoInfo;
|
||||
export let patches: Patch[];
|
||||
export let patches: PaginatedQuery<Patch[]>;
|
||||
export let config: Config;
|
||||
export let status: PatchStatus;
|
||||
export let status: PatchStatus | undefined = undefined;
|
||||
|
||||
$: items = patches.content;
|
||||
$: more = patches.more;
|
||||
$: cursor = patches.cursor;
|
||||
|
||||
async function loadMore() {
|
||||
if (more) {
|
||||
const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
|
||||
rid: repo.rid,
|
||||
skip: cursor + 20,
|
||||
status,
|
||||
take: 20,
|
||||
});
|
||||
|
||||
cursor = p.cursor;
|
||||
more = p.more;
|
||||
items = [...items, ...p.content];
|
||||
}
|
||||
}
|
||||
|
||||
$: project = repo.payloads["xyz.radicle.project"]!;
|
||||
</script>
|
||||
@@ -31,7 +52,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout>
|
||||
<Layout {loadMore}>
|
||||
<svelte:fragment slot="breadcrumbs">
|
||||
<Link route={{ resource: "home" }}>
|
||||
<NodeId
|
||||
@@ -77,8 +98,8 @@
|
||||
</div>
|
||||
<div class="global-flex txt-small" style:margin="0.5rem 0">
|
||||
<Link
|
||||
variant={status === "all" ? "active" : "tab"}
|
||||
route={{ resource: "repo.patches", rid: repo.rid, status: "all" }}>
|
||||
variant={status === undefined ? "active" : "tab"}
|
||||
route={{ resource: "repo.patches", rid: repo.rid }}>
|
||||
<div class="global-flex"><Icon name="patch" />Patches</div>
|
||||
<div class="global-counter">
|
||||
{project.meta.patches.draft +
|
||||
@@ -133,13 +154,13 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="list">
|
||||
{#each patches as patch}
|
||||
{#each items as patch}
|
||||
<PatchTeaser rid={repo.rid} {patch} />
|
||||
{/each}
|
||||
|
||||
{#if patches.length === 0}
|
||||
{#if patches.content.length === 0}
|
||||
<div class="txt-missing txt-small">
|
||||
{#if status === "all"}
|
||||
{#if status === undefined}
|
||||
No patches.
|
||||
{:else}
|
||||
No {status} patches.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Config } from "@bindings/Config";
|
||||
import type { PaginatedQuery } from "@bindings/PaginatedQuery";
|
||||
import type { Issue } from "@bindings/Issue";
|
||||
import type { Patch } from "@bindings/Patch";
|
||||
import type { RepoInfo } from "@bindings/RepoInfo";
|
||||
@@ -41,7 +42,7 @@ export interface LoadedRepoIssuesRoute {
|
||||
};
|
||||
}
|
||||
|
||||
export type PatchStatus = "all" | Patch["state"]["status"];
|
||||
export type PatchStatus = Patch["state"]["status"];
|
||||
|
||||
export interface RepoPatchRoute {
|
||||
resource: "repo.patch";
|
||||
@@ -63,7 +64,7 @@ export interface LoadedRepoPatchRoute {
|
||||
export interface RepoPatchesRoute {
|
||||
resource: "repo.patches";
|
||||
rid: string;
|
||||
status: PatchStatus;
|
||||
status?: PatchStatus;
|
||||
}
|
||||
|
||||
export interface LoadedRepoPatchesRoute {
|
||||
@@ -71,8 +72,8 @@ export interface LoadedRepoPatchesRoute {
|
||||
params: {
|
||||
repo: RepoInfo;
|
||||
config: Config;
|
||||
patches: Patch[];
|
||||
status: PatchStatus;
|
||||
patches: PaginatedQuery<Patch[]>;
|
||||
status?: PatchStatus;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,9 +95,8 @@ export async function loadPatch(
|
||||
rid: route.rid,
|
||||
});
|
||||
const config: Config = await invoke("config");
|
||||
const patches: Patch[] = await invoke("list_patches", {
|
||||
const patches: PaginatedQuery<Patch[]> = await invoke("list_patches", {
|
||||
rid: route.rid,
|
||||
status: "all",
|
||||
});
|
||||
const patch: Patch = await invoke("patch_by_id", {
|
||||
rid: route.rid,
|
||||
@@ -109,7 +109,7 @@ export async function loadPatch(
|
||||
|
||||
return {
|
||||
resource: "repo.patch",
|
||||
params: { repo, config, patch, patches, revisions },
|
||||
params: { repo, config, patch, patches: patches.content, revisions },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export async function loadPatches(
|
||||
rid: route.rid,
|
||||
});
|
||||
const config: Config = await invoke("config");
|
||||
const patches: Patch[] = await invoke("list_patches", {
|
||||
const patches: PaginatedQuery<Patch[]> = await invoke("list_patches", {
|
||||
rid: route.rid,
|
||||
status: route.status,
|
||||
});
|
||||
@@ -243,7 +243,7 @@ export function repoUrlToRoute(
|
||||
) {
|
||||
return { resource: "repo.patches", rid, status };
|
||||
} else {
|
||||
return { resource: "repo.patches", rid, status: "all" };
|
||||
return { resource: "repo.patches", rid };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user