mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 11:20:36 +08:00
Fetch cobs by selected or default state
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
@@ -370,12 +370,18 @@ export class Client {
|
||||
|
||||
public async getAllIssues(
|
||||
id: string,
|
||||
query?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
state?: string;
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<Issue[]> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "GET",
|
||||
path: `projects/${id}/issues`,
|
||||
query,
|
||||
options,
|
||||
},
|
||||
issuesSchema,
|
||||
@@ -441,12 +447,18 @@ export class Client {
|
||||
|
||||
public async getAllPatches(
|
||||
id: string,
|
||||
query?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
state?: string;
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<Patch[]> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "GET",
|
||||
path: `projects/${id}/patches`,
|
||||
query,
|
||||
options,
|
||||
},
|
||||
patchesSchema,
|
||||
|
||||
@@ -12,48 +12,79 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import * as utils from "@app/lib/utils";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { sessionStore } from "@app/lib/session";
|
||||
|
||||
import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
|
||||
import IssueTeaser from "@app/views/projects/Issue/IssueTeaser.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import TabBar from "@app/components/TabBar.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
|
||||
export let issues: Issue[];
|
||||
export let status: IssueStatus;
|
||||
export let projectId: string;
|
||||
export let state: IssueStatus;
|
||||
export let baseUrl: BaseUrl;
|
||||
export let issueCounters: { open: number; closed: number };
|
||||
|
||||
let options: Tab<IssueStatus>[];
|
||||
const perPage = 10;
|
||||
|
||||
function groupIssues(issues: Issue[]): {
|
||||
open: Issue[];
|
||||
closed: Issue[];
|
||||
} {
|
||||
return issues.reduce(
|
||||
(acc, issue) => {
|
||||
acc[issue.state.status].push(issue);
|
||||
return acc;
|
||||
},
|
||||
{ open: [] as Issue[], closed: [] as Issue[] },
|
||||
);
|
||||
// Keeping it true, to avoid an initial flash
|
||||
// of EmptyState Placeholder
|
||||
let refresh = true;
|
||||
let loading = false;
|
||||
let page = 0;
|
||||
let error: any;
|
||||
let issues: Issue[] = [];
|
||||
|
||||
const api = new HttpdClient(baseUrl);
|
||||
|
||||
async function loadIssues(): Promise<void> {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await api.project.getAllIssues(projectId, {
|
||||
state,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
issues = [...issues, ...response];
|
||||
page += 1;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
loading = false;
|
||||
refresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
function switchState(e: CustomEvent<IssueStatus>): void {
|
||||
refresh = true;
|
||||
// Update state to be used in the query
|
||||
state = e.detail;
|
||||
// Reset page to 0 to load the first page for a new state
|
||||
page = 0;
|
||||
// Remove all existing patches with old state
|
||||
issues = [];
|
||||
loadIssues();
|
||||
router.updateProjectRoute({
|
||||
search: `state=${state}`,
|
||||
});
|
||||
}
|
||||
|
||||
const stateOptions: IssueStatus[] = ["open", "closed"];
|
||||
$: options = stateOptions.map<{
|
||||
value: IssueStatus;
|
||||
title: string;
|
||||
disabled: boolean;
|
||||
}>((s: IssueStatus) => ({
|
||||
const options = stateOptions.map<Tab<IssueStatus>>(s => ({
|
||||
value: s,
|
||||
title: `${issueCounters[s]} ${s}`,
|
||||
disabled: issueCounters[s] === 0,
|
||||
}));
|
||||
$: filteredIssues = groupIssues(issues)[status];
|
||||
$: sortedIssues = filteredIssues.sort(
|
||||
({ discussion: t1 }, { discussion: t2 }) =>
|
||||
t2[0].timestamp - t1[0].timestamp,
|
||||
);
|
||||
|
||||
$: showMoreButton =
|
||||
!loading &&
|
||||
!error &&
|
||||
issueCounters[state] &&
|
||||
issues.length < issueCounters[state];
|
||||
|
||||
loadIssues();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -74,6 +105,14 @@
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
.loader {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
.more {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.issues {
|
||||
@@ -85,13 +124,7 @@
|
||||
<div class="issues">
|
||||
<div class="section-header">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<TabBar
|
||||
{options}
|
||||
on:select={e =>
|
||||
router.updateProjectRoute({
|
||||
search: `state=${e.detail}`,
|
||||
})}
|
||||
active={status} />
|
||||
<TabBar {options} on:select={switchState} active={state} />
|
||||
</div>
|
||||
<HeaderToggleLabel
|
||||
disabled={!$sessionStore || !utils.isLocal(baseUrl.hostname)}
|
||||
@@ -107,10 +140,13 @@
|
||||
New issue
|
||||
</HeaderToggleLabel>
|
||||
</div>
|
||||
|
||||
{#if filteredIssues.length}
|
||||
<div class="issues-list">
|
||||
{#each sortedIssues as issue}
|
||||
<div class="issues-list">
|
||||
{#if refresh}
|
||||
<div class="loader">
|
||||
<Loading center />
|
||||
</div>
|
||||
{:else}
|
||||
{#each issues as issue}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="teaser"
|
||||
@@ -124,12 +160,21 @@
|
||||
}}>
|
||||
<IssueTeaser {issue} />
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(state)} issues</div>
|
||||
<div slot="body">No issues matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(status)} issues</div>
|
||||
<div slot="body">No issues matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
<div class="more">
|
||||
{#if loading}
|
||||
<Loading small={page !== 0} center />
|
||||
{/if}
|
||||
|
||||
{#if showMoreButton}
|
||||
<Button variant="foreground" on:click={loadIssues}>More</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,67 +1,90 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { PatchState } from "@httpd-client";
|
||||
import type { Patch, PatchState } from "@httpd-client";
|
||||
|
||||
export type PatchStatus = PatchState["status"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Patch } from "@httpd-client";
|
||||
import type { Tab } from "@app/components/TabBar.svelte";
|
||||
import type { BaseUrl } from "@httpd-client";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
|
||||
import PatchTeaser from "./Patch/PatchTeaser.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import TabBar from "@app/components/TabBar.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
|
||||
export let patches: Patch[];
|
||||
export let status: PatchStatus;
|
||||
export let baseUrl: BaseUrl;
|
||||
export let projectId: string;
|
||||
export let projectPatches: {
|
||||
export let state: PatchStatus;
|
||||
export let baseUrl: BaseUrl;
|
||||
export let patchCounters: {
|
||||
draft: number;
|
||||
open: number;
|
||||
archived: number;
|
||||
merged: number;
|
||||
};
|
||||
|
||||
let options: Tab<PatchStatus>[];
|
||||
const perPage = 10;
|
||||
|
||||
function groupPatches(patches: Patch[]): {
|
||||
open: Patch[];
|
||||
draft: Patch[];
|
||||
archived: Patch[];
|
||||
merged: Patch[];
|
||||
} {
|
||||
return patches.reduce(
|
||||
(acc, patch) => {
|
||||
acc[patch.state.status].push(patch);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
open: [] as Patch[],
|
||||
draft: [] as Patch[],
|
||||
archived: [] as Patch[],
|
||||
merged: [] as Patch[],
|
||||
},
|
||||
);
|
||||
// Keeping it true, to avoid an initial flash
|
||||
// of EmptyState Placeholder
|
||||
let refresh = true;
|
||||
let loading = false;
|
||||
let page = 0;
|
||||
let error: any;
|
||||
let patches: Patch[] = [];
|
||||
|
||||
const api = new HttpdClient(baseUrl);
|
||||
|
||||
async function loadPatches(): Promise<void> {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await api.project.getAllPatches(projectId, {
|
||||
state,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
patches = [...patches, ...response];
|
||||
page += 1;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
loading = false;
|
||||
refresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
function switchState(e: CustomEvent<PatchStatus>): void {
|
||||
refresh = true;
|
||||
// Update state to be used in the query
|
||||
state = e.detail;
|
||||
// Reset page to 0 to load the first page for a new state
|
||||
page = 0;
|
||||
// Remove all existing patches with old state
|
||||
patches = [];
|
||||
loadPatches();
|
||||
router.updateProjectRoute({
|
||||
search: `state=${state}`,
|
||||
});
|
||||
}
|
||||
|
||||
const stateOptions: PatchStatus[] = ["draft", "open", "archived", "merged"];
|
||||
$: options = stateOptions.map<{
|
||||
value: PatchStatus;
|
||||
title: string;
|
||||
disabled: boolean;
|
||||
}>((s: PatchStatus) => ({
|
||||
const options = stateOptions.map<Tab<PatchStatus>>(s => ({
|
||||
value: s,
|
||||
title: `${projectPatches[s]} ${s}`,
|
||||
disabled: projectPatches[s] === 0,
|
||||
title: `${patchCounters[s]} ${s}`,
|
||||
disabled: patchCounters[s] === 0,
|
||||
}));
|
||||
$: filteredPatches = groupPatches(patches)[status];
|
||||
$: sortedPatches = filteredPatches.sort(
|
||||
({ revisions: [r1] }, { revisions: [r2] }) => r2.timestamp - r1.timestamp,
|
||||
);
|
||||
$: showMoreButton =
|
||||
!loading &&
|
||||
!error &&
|
||||
patchCounters[state] &&
|
||||
patches.length < patchCounters[state];
|
||||
|
||||
loadPatches();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -73,6 +96,14 @@
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.loader {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
.more {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
min-height: 3rem;
|
||||
}
|
||||
.teaser:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--color-background);
|
||||
}
|
||||
@@ -86,17 +117,15 @@
|
||||
|
||||
<div class="patches">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<TabBar
|
||||
{options}
|
||||
on:select={e =>
|
||||
router.updateProjectRoute({
|
||||
search: `state=${e.detail}`,
|
||||
})}
|
||||
active={status} />
|
||||
<TabBar {options} on:select={switchState} active={state} />
|
||||
</div>
|
||||
{#if filteredPatches.length}
|
||||
<div class="patches-list">
|
||||
{#each sortedPatches as patch}
|
||||
<div class="patches-list">
|
||||
{#if refresh}
|
||||
<div class="loader">
|
||||
<Loading center />
|
||||
</div>
|
||||
{:else}
|
||||
{#each patches as patch}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="teaser"
|
||||
@@ -110,12 +139,21 @@
|
||||
}}>
|
||||
<PatchTeaser {baseUrl} {projectId} {patch} />
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(state)} patches</div>
|
||||
<div slot="body">No patches matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(status)} patches</div>
|
||||
<div slot="body">No issues matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
<div class="more">
|
||||
{#if loading}
|
||||
<Loading small={page !== 0} center />
|
||||
{/if}
|
||||
|
||||
{#if showMoreButton}
|
||||
<Button variant="foreground" on:click={loadPatches}>More</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -235,19 +235,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if activeRoute.params.view.resource === "issues"}
|
||||
{#await api.project.getAllIssues(project.id)}
|
||||
<Loading center />
|
||||
{:then issues}
|
||||
<Issues
|
||||
{baseUrl}
|
||||
issueCounters={project.issues}
|
||||
status={issueFilter}
|
||||
{issues} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<ErrorMessage message="Couldn't load issues." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
<Issues
|
||||
{baseUrl}
|
||||
projectId={project.id}
|
||||
issueCounters={project.issues}
|
||||
state={issueFilter} />
|
||||
{:else if activeRoute.params.view.resource === "issue"}
|
||||
{#await api.project.getIssueById(project.id, activeRoute.params.view.params.issue)}
|
||||
<Loading center />
|
||||
@@ -264,20 +256,11 @@
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "patches"}
|
||||
{#await api.project.getAllPatches(project.id)}
|
||||
<Loading center />
|
||||
{:then patches}
|
||||
<Patches
|
||||
{patches}
|
||||
status={patchFilter}
|
||||
projectId={project.id}
|
||||
{baseUrl}
|
||||
projectPatches={project.patches} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<ErrorMessage message="Couldn't load patches." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
<Patches
|
||||
{baseUrl}
|
||||
projectId={project.id}
|
||||
state={patchFilter}
|
||||
patchCounters={project.patches} />
|
||||
{:else if activeRoute.params.view.resource === "patch"}
|
||||
{#await api.project.getPatchById(project.id, activeRoute.params.view.params.patch)}
|
||||
<Loading center />
|
||||
|
||||
Reference in New Issue
Block a user