Browse Source

feat(PRPage): add pr status

- add pr details
- gather status events when loading PRFull
- add StatusSelector component that enables sending new status events
master
DanConwayDev 2 years ago
parent
commit
e3ddb30205
No known key found for this signature in database
GPG Key ID: 68E15486D73F75E1
  1. 47
      src/lib/components/prs/PRDetails.svelte
  2. 157
      src/lib/components/prs/StatusSelector.svelte
  3. 15
      src/lib/components/prs/type.ts
  4. 7
      src/lib/kinds.ts
  5. 53
      src/lib/stores/PR.ts
  6. 4
      src/lib/stores/repo.ts
  7. 3
      src/lib/wrappers/EventCard.svelte
  8. 6
      src/lib/wrappers/ReposRecent.svelte
  9. 16
      src/lib/wrappers/Thread.svelte
  10. 19
      src/routes/repo/[repo_id]/pr/[pr_id]/+page.svelte

47
src/lib/components/prs/PRDetails.svelte

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
<script lang="ts">
import { full_defaults } from "./type";
import UserHeader from "../users/UserHeader.svelte";
import StatusSelector from "./StatusSelector.svelte";
export let { summary, status, labels, loading } = { ...full_defaults };
</script>
<div class="max-w-md">
<div>
{#if loading}
<div class="h-5 my-3 skeleton w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div>
<div class="badge skeleton my-2 w-40 block"></div>
{:else}
<h4>Author</h4>
<UserHeader user={summary.author} />
{/if}
</div>
<div>
{#if loading}
<div class="h-5 my-3 skeleton w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div>
<div class="badge skeleton my-2 w-40 block"></div>
{:else}
<h4>Status</h4>
<StatusSelector
{status}
repo_id={summary.repo_id}
pr_id={summary.id}
/>
{/if}
</div>
<div>
{#if loading}
<div class="badge skeleton w-20"></div>
<div class="badge skeleton w-20"></div>
{:else}
<h4>Labels</h4>
{#each labels as label}
<div class="badge badge-secondary mr-2">{label}</div>
{/each}
{/if}
</div>
</div>

157
src/lib/components/prs/StatusSelector.svelte

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
<script lang="ts">
import { ndk } from "$lib/stores/ndk";
import { NDKEvent, type NDKTag } from "@nostr-dev-kit/ndk";
import type { PRStatus } from "./type";
import { selected_pr_full } from "$lib/stores/PR";
import { load } from "../../../routes/repo/[repo_id]/+page";
import { patch_kind } from "$lib/kinds";
export let status: PRStatus = "Draft";
export let repo_id: string = "";
export let pr_id: string = "";
let loading = false;
async function changeStatus(new_status: PRStatus) {
let event = new NDKEvent(ndk);
// TODO: use random custom kind for status instead of NIP32?
event.kind = patch_kind;
event.tags.push(["t", new_status]);
event.tags.push(["e", pr_id]);
event.tags.push(["r", `r-${repo_id}`]);
event.sign();
loading = true;
// TODO send to repo relays, current user relay and pr event pubkey relays
try {
// TODO: check if we are signed in a signer is in ndk
// let res = await event.publish();
selected_pr_full.update((pr_full) => {
if (pr_full.summary.id !== pr_id) return pr_full;
return {
...pr_full,
status: new_status,
};
});
loading = false;
} catch {}
}
</script>
{#if loading}
<div class="skeleton w-28 h-8 rounded-md"></div>
{:else}
<div class="dropdown">
<div
tabIndex={0}
role="button"
class:btn-success={status === "Open"}
class:btn-primary={status === "Merged"}
class:btn-neutral={status === "Draft" || status === "Closed"}
class="btn btn-success btn-sm mr-6 align-middle"
>
{#if status === "Open"}
<!-- http://icon-sets.iconify.design/octicon/git-pull-request-16/ -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
class="h-5 w-5 pt-1 flex-none fill-success-content"
><path
d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25m5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354M3.75 2.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m0 9.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m8.25.75a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0"
/>
</svg>
Open
{:else if status === "Merged"}
<!-- https://icon-sets.iconify.design/octicon/git-merge-16/ -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-primary-content"
><path
d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218M4.25 13.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m8.5-4.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M5 3.25a.75.75 0 1 0 0 .005z"
/></svg
>
Merged
{:else if status === "Closed"}
<!-- https://icon-sets.iconify.design/octicon/git-pull-request-closed-16/ -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-neutral-content"
><path
d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75m-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97l.97-.97a.748.748 0 0 1 1.265.332a.75.75 0 0 1-.205.729l-.97.97l.97.97a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018l-.97-.97l-.97.97a.749.749 0 0 1-1.275-.326a.749.749 0 0 1 .215-.734l.97-.97l-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5"
/></svg
>
Closed
{:else if status === "Draft"}
<!-- https://icon-sets.iconify.design/octicon/git-pull-request-draft-16// -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-neutral-content"
><path
d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 14a2.25 2.25 0 1 1 0-4.5a2.25 2.25 0 0 1 0 4.5M2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5M14 7.5a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0m0-4.25a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0"
/></svg
>
Draft
{/if}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-5 w-5s flex-none fill-success-content"
><path
fill="currentColor"
d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0"
/></svg
>
</div>
<ul
tabIndex={0}
class="dropdown-content z-[1] menu p-2 ml-0 shadow bg-base-300 rounded-box w-52"
>
{#if status !== "Draft"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Draft");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Draft</button
>
</li>
{/if}
{#if status !== "Open"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Open");
}}
class="btn btn-success btn-sm mx-2 align-middle"
>Open</button
>
</li>
{/if}
{#if status !== "Merged"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Merged");
}}
class="btn btn-primary btn-sm mx-2 align-middle"
>Merged</button
>
</li>
{/if}
{#if status !== "Closed"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Closed");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Closed</button
>
</li>
{/if}
</ul>
</div>
{/if}

15
src/lib/components/prs/type.ts

@ -37,9 +37,22 @@ export const summaries_defaults: PRSummaries = { @@ -37,9 +37,22 @@ export const summaries_defaults: PRSummaries = {
loading: true,
};
export type PRStatus = "Draft" | "Open" | "Merged" | "Closed";
export function isPRStatus(potential_status: string | undefined): potential_status is PRStatus {
return !!potential_status
&& (
potential_status == "Draft"
|| potential_status == "Open"
|| potential_status == "Merged"
|| potential_status == "Closed"
)
}
export interface PRFull {
summary: PRSummary;
pr_event: NDKEvent | undefined;
status: PRStatus;
labels: string[];
events: Event[];
loading: boolean;
}
@ -47,6 +60,8 @@ export interface PRFull { @@ -47,6 +60,8 @@ export interface PRFull {
export const full_defaults: PRFull = {
summary: { ...summary_defaults },
pr_event: undefined,
status: "Open",
labels: [],
events: [],
loading: true,
};

7
src/lib/kinds.ts

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
export let pr_status_kind = 19871987;
export let repo_kind = 30317;
export let pr_kind = 318;
export let patch_kind = 317;

53
src/lib/stores/PR.ts

@ -3,9 +3,9 @@ import { writable, type Unsubscriber, type Writable } from "svelte/store" @@ -3,9 +3,9 @@ import { writable, type Unsubscriber, type Writable } from "svelte/store"
import { ndk } from "./ndk";
import type { User } from "$lib/components/users/type";
import { ensureUser } from "./users";
import { type PRFull, full_defaults } from "$lib/components/prs/type";
import { type PRFull, full_defaults, isPRStatus, type PRStatus } from "$lib/components/prs/type";
import { pr_kind, pr_status_kind } from "$lib/kinds";
let pr_kind: number = 318;
export let selected_pr_full: Writable<PRFull> = writable({ ...full_defaults });
@ -13,12 +13,21 @@ let selected_repo_id: string = ""; @@ -13,12 +13,21 @@ let selected_repo_id: string = "";
let selected_pr_id: string = "";
let pr_summary_author_unsubsriber: Unsubscriber | undefined;
export let selected_pr_replies: Writable<NDKEvent[]> = writable([]);
let selected_pr_status_date = 0;
export let ensurePRFull = (repo_id: string, pr_id: string) => {
if (selected_pr_id == pr_id) return;
if (pr_id == "") return selected_pr_full.set({ ...full_defaults });
if (pr_id == "") {
selected_pr_full.set({ ...full_defaults });
return;
}
selected_repo_id = repo_id;
selected_pr_id = pr_id;
selected_pr_status_date = 0;
selected_pr_replies.set([]);
selected_pr_full.update(full => {
return {
@ -90,6 +99,44 @@ export let ensurePRFull = (repo_id: string, pr_id: string) => { @@ -90,6 +99,44 @@ export let ensurePRFull = (repo_id: string, pr_id: string) => {
...full.summary,
loading: false,
},
};
});
});
let sub_replies = ndk.subscribe({
"#e": [pr_id],
});
sub_replies.on("event", (event: NDKEvent) => {
if (event.kind == pr_status_kind
&& event.created_at && selected_pr_status_date < event.created_at
&& event.getMatchingTags("t").length === 1
&& event.getMatchingTags("t")[0].length > 1
) {
let potential_status = event.getMatchingTags("t")[0][1];
if (isPRStatus(potential_status)) {
selected_pr_status_date = event.created_at;
selected_pr_full.update(full => {
return {
...full,
status: potential_status as PRStatus,
};
});
}
}
selected_pr_replies.update(replies => {
return [
...replies,
event,
];
});
});
sub.on("eose", () => {
selected_pr_full.update(full => {
return {
...full,
loading: false,
};
});

4
src/lib/stores/repo.ts

@ -5,9 +5,7 @@ import type { Repo } from "$lib/components/repo/type"; @@ -5,9 +5,7 @@ import type { Repo } from "$lib/components/repo/type";
import { defaults } from "$lib/components/repo/type";
import type { User } from "$lib/components/users/type";
import { ensureUser, users } from "./users";
let repo_kind: number = 30317;
import { repo_kind } from "$lib/kinds";
export let selected_repo: Writable<Repo> = writable({ ...defaults });
let selected_repo_id: string = "";

3
src/lib/wrappers/EventCard.svelte

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
import Kind317 from "$lib/components/events/content/Kind317.svelte";
import type { User } from "$lib/components/users/type";
import { defaults as user_defaults } from "$lib/components/users/type";
import { patch_kind } from "$lib/kinds";
import { ensureUser } from "$lib/stores/users";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { onDestroy } from "svelte";
@ -20,7 +21,7 @@ @@ -20,7 +21,7 @@
</script>
<EventWrapper author={$author}>
{#if event.kind == 317}
{#if event.kind == patch_kind}
<Kind317 content={event.content} tags={event.tags} />
{:else}
{event.content}

6
src/lib/wrappers/ReposRecent.svelte

@ -1,21 +1,21 @@ @@ -1,21 +1,21 @@
<script lang="ts">
import type { Args } from "$lib/components/RepoSummaryCard.svelte";
import ReposSummaryList from "$lib/components/ReposSummaryList.svelte";
import { repo_kind } from "$lib/kinds";
import { ndk } from "$lib/stores/ndk";
export let limit: number = 5;
let repos: Args[] = [];
let loading: boolean = true;
let kind: number = 30317;
let sub = ndk.subscribe({
kinds: [kind],
kinds: [repo_kind],
limit,
});
sub.on("event", (event) => {
if (repos.length < limit) {
try {
if (event.kind == kind)
if (event.kind == repo_kind)
repos = [
...repos,
{

16
src/lib/wrappers/Thread.svelte

@ -7,18 +7,26 @@ @@ -7,18 +7,26 @@
import { onDestroy } from "svelte";
import EventCard from "./EventCard.svelte";
import ThreadWrapper from "$lib/components/events/ThreadWrapper.svelte";
import { writable } from "svelte/store";
export let event: NDKEvent;
let replies = ndk.storeSubscribe({
"#e": [event.id],
});
export let replies: NDKEvent[] | undefined = undefined;
let replies_store = replies
? writable(replies)
: ndk.storeSubscribe({
"#e": [event.id],
});
$: {
if (replies) replies_store.set(replies);
}
</script>
<EventCard {event} />
<ThreadWrapper>
{#each $replies as event}
{#each $replies_store as event}
<EventCard {event} />
{/each}
</ThreadWrapper>

19
src/routes/repo/[repo_id]/pr/[pr_id]/+page.svelte

@ -1,9 +1,14 @@ @@ -1,9 +1,14 @@
<script lang="ts">
import { ensureSelectedRepo, selected_repo } from "$lib/stores/repo";
import { ensurePRFull, selected_pr_full } from "$lib/stores/PR";
import {
ensurePRFull,
selected_pr_full,
selected_pr_replies,
} from "$lib/stores/PR";
import PrHeader from "$lib/components/prs/PRHeader.svelte";
import RepoHeader from "$lib/components/repo/RepoHeader.svelte";
import Thread from "$lib/wrappers/Thread.svelte";
import PrDetails from "$lib/components/prs/PRDetails.svelte";
export let data: {
repo_id: string;
@ -26,10 +31,18 @@ @@ -26,10 +31,18 @@
{$selected_pr_full.summary.descritpion}
</div>
{#if $selected_pr_full.pr_event}
<Thread event={$selected_pr_full.pr_event} />
<Thread
event={$selected_pr_full.pr_event}
replies={$selected_pr_replies}
/>
{/if}
</div>
<div class="w-1/3 mx-2 prose">
<div>placeholder for status, tags, contributors</div>
<PrDetails
summary={$selected_pr_full.summary}
status={$selected_pr_full.status}
labels={$selected_pr_full.labels}
loading={$selected_pr_full.loading}
/>
</div>
</div>

Loading…
Cancel
Save