Add infinite scrolling for patches listing

This commit is contained in:
Sebastian Martinez
2024-10-07 13:11:08 +02:00
parent 6b8c73c615
commit e51df19fed
11 changed files with 142 additions and 53 deletions
+3
View File
@@ -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 };
+17 -9
View File
@@ -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,
}
}
}
+41 -18
View File
@@ -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]
+4
View File
@@ -119,6 +119,10 @@ impl Patch {
revision_count: patch.revisions().count(),
}
}
pub fn timestamp(&self) -> u64 {
self.timestamp.as_millis()
}
}
#[derive(Serialize, TS)]
+13 -2
View File
@@ -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}
+1 -1
View File
@@ -52,7 +52,7 @@
}
</style>
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
<Layout>
<svelte:fragment slot="breadcrumbs">
<Link route={{ resource: "home" }}>
<NodeId
+1 -3
View File
@@ -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 +
+22 -1
View File
@@ -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>
+1 -1
View File
@@ -54,7 +54,7 @@
}
</style>
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
<Layout>
<svelte:fragment slot="breadcrumbs">
<Link route={{ resource: "home" }}>
<NodeId
+30 -9
View File
@@ -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.
+9 -9
View File
@@ -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 {