Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Fetch cobs by selected or default state
Sebastian Martinez committed 3 years ago
commit d77b6041a6c411e994e85dd696b4e38a1b3a2024
parent 970431824802da293005c6702fc89a1dc192b67a
4 files changed +202 -124
modified httpd-client/lib/project.ts
@@ -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,
modified src/views/projects/Issues.svelte
@@ -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>[];
-

-
  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[] },
-
    );
+
  const perPage = 10;
+

+
  // 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>
modified src/views/projects/Patches.svelte
@@ -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>[];
-

-
  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[],
-
      },
-
    );
+
  const perPage = 10;
+

+
  // 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>
modified src/views/projects/View.svelte
@@ -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 />