Browse Source

feat(ListPRs): add author name to summary

refactor to reflect new stardards for ui and data logic seperation
master
DanConwayDev 2 years ago
parent
commit
9ccf21c0a7
No known key found for this signature in database
GPG Key ID: 68E15486D73F75E1
  1. BIN
      __snapshots__/navbar--default.png
  2. BIN
      __snapshots__/prs-list-item--author-loading.png
  3. BIN
      __snapshots__/prs-list-item--long-and-no-spaces.png
  4. BIN
      __snapshots__/prs-list-item--long-details.png
  5. BIN
      __snapshots__/prs-list-item--short-details.png
  6. 56
      __snapshots__/prs-list-item.test.js.snap
  7. BIN
      __snapshots__/prs-list-list--default.png
  8. BIN
      __snapshots__/prs-list-list--no-title.png
  9. BIN
      __snapshots__/prs-list-list--partially-loaded.png
  10. 14
      __snapshots__/prs-list-list.test.js.snap
  11. BIN
      __snapshots__/repo-summary-list--default.png
  12. 2
      src/lib/components/prs/PRsList.stories.svelte
  13. 11
      src/lib/components/prs/PRsList.svelte
  14. 4
      src/lib/components/prs/PRsListItem.stories.svelte
  15. 30
      src/lib/components/prs/PRsListItem.svelte
  16. 32
      src/lib/components/prs/type.ts
  17. 27
      src/lib/components/prs/vectors.ts
  18. 6
      src/lib/components/users/type.ts
  19. 100
      src/lib/stores/PRs.ts
  20. 40
      src/lib/wrappers/OpenPRs.svelte
  21. 6
      src/routes/repo/[repo_id]/+page.svelte

BIN
__snapshots__/navbar--default.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
__snapshots__/prs-list-item--author-loading.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
__snapshots__/prs-list-item--long-and-no-spaces.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
__snapshots__/prs-list-item--long-details.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
__snapshots__/prs-list-item--short-details.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

56
__snapshots__/prs-list-item.test.js.snap

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PRs/List/Item Long Details smoke-test 1`] = ` exports[`PRs/List/Item Author Loading smoke-test 1`] = `
<li class="flex p-2 pt-4 hover:bg-neutral-700 cursor-pointer"> <li class="flex p-2 pt-4 hover:bg-neutral-700 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 16 16" viewbox="0 0 16 16"
@ -11,21 +11,32 @@ exports[`PRs/List/Item Long Details smoke-test 1`] = `
</svg> </svg>
<div class="ml-3 overflow-hidden grow text-xs text-neutral-content"> <div class="ml-3 overflow-hidden grow text-xs text-neutral-content">
<div class="text-sm text-base-content"> <div class="text-sm text-base-content">
rather long title that goes on and on and on and on and on and on... short title
</div> </div>
<ul class="pt-2"> <ul class="pt-2">
<li class="align-middle inline mr-3">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 pt-0 flex-none fill-base-content inline-block"
viewbox="0 0 16 16"
>
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z">
</path>
</svg>
1
</li>
<li class="inline mr-3"> <li class="inline mr-3">
opened a minute ago opened 3 months ago
</li> </li>
<li class="inline"> <li class="inline">
carole <div class="skeleton h-3 pb-2 w-20 inline-block">
</div>
</li> </li>
</ul> </ul>
</div> </div>
</li> </li>
`; `;
exports[`PRs/List/Item Long and No Spaces smoke-test 1`] = ` exports[`PRs/List/Item Long Details smoke-test 1`] = `
<li class="flex p-2 pt-4 hover:bg-neutral-700 cursor-pointer"> <li class="flex p-2 pt-4 hover:bg-neutral-700 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 16 16" viewbox="0 0 16 16"
@ -36,31 +47,21 @@ exports[`PRs/List/Item Long and No Spaces smoke-test 1`] = `
</svg> </svg>
<div class="ml-3 overflow-hidden grow text-xs text-neutral-content"> <div class="ml-3 overflow-hidden grow text-xs text-neutral-content">
<div class="text-sm text-base-content"> <div class="text-sm text-base-content">
LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameL... rather long title that goes on and on and on and on and on and on...
</div> </div>
<ul class="pt-2"> <ul class="pt-2">
<li class="align-middle inline mr-3">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 pt-0 flex-none fill-base-content inline-block"
viewbox="0 0 16 16"
>
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z">
</path>
</svg>
1
</li>
<li class="inline mr-3"> <li class="inline mr-3">
opened 3 months ago opened a minute ago
</li> </li>
<li class="inline"> <li class="inline">
steve DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
</li> </li>
`; `;
exports[`PRs/List/Item No Details smoke-test 1`] = ` exports[`PRs/List/Item Long and No Spaces smoke-test 1`] = `
<li class="flex p-2 pt-4 hover:bg-neutral-700 cursor-pointer"> <li class="flex p-2 pt-4 hover:bg-neutral-700 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 16 16" viewbox="0 0 16 16"
@ -71,13 +72,24 @@ exports[`PRs/List/Item No Details smoke-test 1`] = `
</svg> </svg>
<div class="ml-3 overflow-hidden grow text-xs text-neutral-content"> <div class="ml-3 overflow-hidden grow text-xs text-neutral-content">
<div class="text-sm text-base-content"> <div class="text-sm text-base-content">
Untitled LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameL...
</div> </div>
<ul class="pt-2"> <ul class="pt-2">
<li class="align-middle inline mr-3">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 pt-0 flex-none fill-base-content inline-block"
viewbox="0 0 16 16"
>
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z">
</path>
</svg>
1
</li>
<li class="inline mr-3"> <li class="inline mr-3">
opened opened 3 months ago
</li> </li>
<li class="inline"> <li class="inline">
DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -112,7 +124,7 @@ exports[`PRs/List/Item Short Details smoke-test 1`] = `
opened 7 days ago opened 7 days ago
</li> </li>
<li class="inline"> <li class="inline">
fred DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>

BIN
__snapshots__/prs-list-list--default.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

BIN
__snapshots__/prs-list-list--no-title.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
__snapshots__/prs-list-list--partially-loaded.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

14
__snapshots__/prs-list-list.test.js.snap

@ -35,7 +35,7 @@ exports[`PRs/List/List Default smoke-test 1`] = `
opened 7 days ago opened 7 days ago
</li> </li>
<li class="inline"> <li class="inline">
fred DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -57,7 +57,7 @@ exports[`PRs/List/List Default smoke-test 1`] = `
opened a minute ago opened a minute ago
</li> </li>
<li class="inline"> <li class="inline">
carole DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -89,7 +89,7 @@ exports[`PRs/List/List Default smoke-test 1`] = `
opened 3 months ago opened 3 months ago
</li> </li>
<li class="inline"> <li class="inline">
steve DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -185,7 +185,7 @@ exports[`PRs/List/List No Title smoke-test 1`] = `
opened 7 days ago opened 7 days ago
</li> </li>
<li class="inline"> <li class="inline">
fred DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -207,7 +207,7 @@ exports[`PRs/List/List No Title smoke-test 1`] = `
opened a minute ago opened a minute ago
</li> </li>
<li class="inline"> <li class="inline">
carole DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -251,7 +251,7 @@ exports[`PRs/List/List Partially Loaded smoke-test 1`] = `
opened 7 days ago opened 7 days ago
</li> </li>
<li class="inline"> <li class="inline">
fred DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>
@ -273,7 +273,7 @@ exports[`PRs/List/List Partially Loaded smoke-test 1`] = `
opened a minute ago opened a minute ago
</li> </li>
<li class="inline"> <li class="inline">
carole DanConwayDev
</li> </li>
</ul> </ul>
</div> </div>

BIN
__snapshots__/repo-summary-list--default.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

2
src/lib/components/PRsList.stories.svelte → src/lib/components/prs/PRsList.stories.svelte

@ -2,7 +2,7 @@
import type { Meta } from "@storybook/svelte"; import type { Meta } from "@storybook/svelte";
import PRsList from "./PRsList.svelte"; import PRsList from "./PRsList.svelte";
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from "@storybook/addon-svelte-csf";
import { PRsListItemArgsVectors as vectors } from "./PR.vectors"; import { PRsListItemArgsVectors as vectors } from "./vectors";
export const meta: Meta<PRsList> = { export const meta: Meta<PRsList> = {
title: "PRs/List/List", title: "PRs/List/List",

11
src/lib/components/PRsList.svelte → src/lib/components/prs/PRsList.svelte

@ -2,12 +2,11 @@
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { onMount } from "svelte"; import { onMount } from "svelte";
import PRsListItem, { import PRsListItem from "$lib/components/prs/PRsListItem.svelte";
type Args as PRsListItemArgs, import type { PRSummary } from "./type";
} from "$lib/components/PRsListItem.svelte";
export let title: string = ""; export let title: string = "";
export let prs: PRsListItemArgs[] = []; export let prs: PRSummary[] = [];
export let loading: boolean = false; export let loading: boolean = false;
</script> </script>
@ -21,8 +20,8 @@
<p class="prose">None</p> <p class="prose">None</p>
{/if} {/if}
<ul class=" divide-y divide-neutral-600"> <ul class=" divide-y divide-neutral-600">
{#each prs as { title, comments, author, created_at }} {#each prs as pr}
<PRsListItem {title} {comments} {author} {created_at} /> <PRsListItem {...pr} />
{/each} {/each}
{#if loading} {#if loading}
<PRsListItem loading={true} /> <PRsListItem loading={true} />

4
src/lib/components/PRsListItem.stories.svelte → src/lib/components/prs/PRsListItem.stories.svelte

@ -2,7 +2,7 @@
import type { Meta } from "@storybook/svelte"; import type { Meta } from "@storybook/svelte";
import PRsListItem from "./PRsListItem.svelte"; import PRsListItem from "./PRsListItem.svelte";
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from "@storybook/addon-svelte-csf";
import { PRsListItemArgsVectors as vectors } from "./PR.vectors"; import { PRsListItemArgsVectors as vectors } from "./vectors";
export const meta: Meta<PRsListItem> = { export const meta: Meta<PRsListItem> = {
title: "PRs/List/Item", title: "PRs/List/Item",
@ -21,6 +21,6 @@
<Story name="Long and No Spaces" args={vectors.LongNoSpaces} /> <Story name="Long and No Spaces" args={vectors.LongNoSpaces} />
<Story name="No Details" args={{}} /> <Story name="Author Loading" args={vectors.AuthorLoading} />
<Story name="loading" args={{ loading: true }} /> <Story name="loading" args={{ loading: true }} />

30
src/lib/components/PRsListItem.svelte → src/lib/components/prs/PRsListItem.svelte

@ -1,36 +1,26 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export interface Args {
title: string;
comments: number;
author: string;
created_at: number | undefined;
loading?: boolean;
}
export const defaults: Args = {
title: "",
comments: 0,
author: "",
created_at: 0,
loading: false,
};
</script> </script>
<script lang="ts"> <script lang="ts">
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { defaults } from "./type";
import { getName } from "../users/type";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export let { title, comments, author, created_at, loading } = defaults; export let { title, id, comments, author, created_at, loading } = defaults;
let short_title: string; let short_title: string;
let created_at_ago: string; let created_at_ago: string;
let author_name = "";
$: {
author_name = getName(author);
}
$: { $: {
if (title.length > 70) short_title = title.slice(0, 65) + "..."; if (title.length > 70) short_title = title.slice(0, 65) + "...";
else if (title.length == 0) short_title = "Untitled"; else if (title.length == 0) short_title = "Untitled";
else short_title = title; else short_title = title;
created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ""; created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : "";
} }
$: {
}
</script> </script>
<li <li
@ -92,7 +82,11 @@
opened {created_at_ago} opened {created_at_ago}
</li> </li>
<li class="inline"> <li class="inline">
{author} {#if author.loading}
<div class="skeleton h-3 pb-2 w-20 inline-block"></div>
{:else}
{author_name}
{/if}
</li> </li>
</ul> </ul>
{/if} {/if}

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

@ -0,0 +1,32 @@
import type { User } from "../users/type";
import { defaults as user_defaults } from "../users/type";
export interface PRSummary {
title: string;
id: string;
comments: number;
author: User;
created_at: number | undefined;
loading: boolean;
}
export const defaults: PRSummary = {
title: "",
id: "",
comments: 0,
author: { ...user_defaults },
created_at: 0,
loading: true,
};
export interface PRSummaries {
id: string;
summaries: PRSummary[];
loading: boolean;
}
export const summaries_defaults: PRSummaries = {
id: "",
summaries: [],
loading: true,
};

27
src/lib/components/PR.vectors.ts → src/lib/components/prs/vectors.ts

@ -1,26 +1,37 @@
import type { Args as PRListItemArgs } from "./PRsListItem.svelte";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import type { PRSummary } from "./type";
import { UserVectors } from "../users/vectors";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export let PRsListItemArgsVectors = { export let PRsListItemArgsVectors = {
Short: { Short: {
title: "short title", title: "short title",
author: "fred", author: { ...UserVectors.default },
created_at: dayjs().subtract(7, 'days').unix(), created_at: dayjs().subtract(7, 'days').unix(),
comments: 2, comments: 2,
} as PRListItemArgs, loading: false,
} as PRSummary,
Long: { Long: {
title: "rather long title that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on", title: "rather long title that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on",
author: "carole", author: { ...UserVectors.default },
created_at: dayjs().subtract(1, 'minute').unix(), created_at: dayjs().subtract(1, 'minute').unix(),
comments: 0, comments: 0,
} as PRListItemArgs, loading: false,
} as PRSummary,
LongNoSpaces: { LongNoSpaces: {
title: "LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName", title: "LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName",
author: "steve", author: { ...UserVectors.default },
created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(), created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(),
comments: 1, comments: 1,
} as PRListItemArgs, loading: false,
}; } as PRSummary,
AuthorLoading: {
title: "short title",
author: { ...UserVectors.loading },
created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(),
comments: 1,
loading: false,
} as PRSummary,
};

6
src/lib/components/users/type.ts

@ -7,6 +7,12 @@ export interface User {
profile?: NDKUserProfile; profile?: NDKUserProfile;
} }
export let defaults: User = {
loading: true,
hexpubkey: "",
npub: "",
}
export function getName(user: User, fallback_to_pubkey: boolean = false): string { export function getName(user: User, fallback_to_pubkey: boolean = false): string {
return user.profile ? ( return user.profile ? (
user.profile.name user.profile.name

100
src/lib/stores/PRs.ts

@ -0,0 +1,100 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { writable, type Unsubscriber, type Writable } from "svelte/store"
import { ndk } from "./ndk";
import type { Repo } from "$lib/components/repo/type";
import { defaults } from "$lib/components/prs/type";
import type { User } from "$lib/components/users/type";
import { ensureUser, users } from "./users";
import type { PRSummaries, PRSummary } from "$lib/components/prs/type";
export let pr_summaries: Writable<PRSummaries> = writable({
id: "",
summaries: [],
loading: false,
});
let pr_kind: number = 318;
let selected_repo_id: string = "";
let authors_unsubscribers: Unsubscriber[] = [];
export let ensurePRSummaries = (repo_id: string) => {
if (selected_repo_id == repo_id) return;
if (repo_id == "") return pr_summaries.set({
id: "",
summaries: [],
loading: false,
});
selected_repo_id = repo_id;
pr_summaries.update(prs => {
return {
...prs,
id: repo_id,
loading: true,
};
});
authors_unsubscribers.forEach(u => u());
authors_unsubscribers = [];
let sub = ndk.subscribe({
kinds: [pr_kind],
'#r': [`r-${repo_id}`],
limit: 50,
});
sub.on("event", (event: NDKEvent) => {
try {
if (event.kind == pr_kind
&& event.getMatchingTags("r").find(t => t[1] === `r-${repo_id}`)
) {
console.log(event);
pr_summaries.update(prs => {
return {
...prs,
summaries: [
...prs.summaries,
{
...defaults,
id: event.id,
title: event.tagValue("name") || "",
created_at: event.created_at,
comments: 0,
author: {
hexpubkey: event.pubkey,
loading: true,
npub: "",
},
loading: false,
}
],
}
});
authors_unsubscribers.push(
ensureUser(event.pubkey).subscribe((u: User) => {
pr_summaries.update(prs => {
console.log('test');
return {
...prs,
summaries: prs.summaries.map(o => ({
...o,
author: u,
})),
}
});
})
);
}
} catch { }
});
sub.on("eose", () => {
pr_summaries.update(prs => {
return {
...prs,
loading: false,
};
});
});
}

40
src/lib/wrappers/OpenPRs.svelte

@ -1,38 +1,14 @@
<script lang="ts"> <script lang="ts">
import PRsList from "$lib/components/PRsList.svelte"; import PRsList from "$lib/components/prs/PRsList.svelte";
import type { Args } from "$lib/components/PRsListItem.svelte"; import { ensurePRSummaries, pr_summaries } from "$lib/stores/PRs";
import { ndk } from "$lib/stores/ndk";
export let limit: number = 100;
let prs: Args[] = [];
export let loading: boolean = true;
let repo_kind: number = 30317;
let pr_kind: number = 318;
export let repo_id: string = ""; export let repo_id: string = "";
let sub = ndk.subscribe({ ensurePRSummaries(repo_id);
kinds: [pr_kind],
"#d": [repo_id],
limit,
});
sub.on("event", (event) => {
if (prs.length < limit) {
if (event.kind == pr_kind)
prs = [
...prs,
{
title: event.tagValue("name") || "",
author: event.pubkey,
created_at: event.created_at,
comments: 1,
},
];
} else if (loading == true) loading = false;
});
sub.on("eose", () => {
if (loading == true) loading = false;
});
</script> </script>
<PRsList title="Open PRs" {prs} {loading} /> <PRsList
title="Open PRs"
prs={$pr_summaries.summaries}
loading={$pr_summaries.loading}
/>

6
src/routes/repo/[repo_id]/+page.svelte

@ -9,12 +9,12 @@
ensureSelectedRepo(repo_id); ensureSelectedRepo(repo_id);
</script> </script>
<h1>{$selected_repo.name}</h1> <h1 class="mx-2 my-4">{$selected_repo.name}</h1>
<div class="flex"> <div class="flex">
<div class="w-2/3"> <div class="w-2/3 mx-2">
<OpenPRs {repo_id} /> <OpenPRs {repo_id} />
</div> </div>
<div class="w-1/3 prose"> <div class="w-1/3 mx-2 prose">
<RepoDetails {repo_id} /> <RepoDetails {repo_id} />
</div> </div>
</div> </div>

Loading…
Cancel
Save