diff --git a/src/domlink.ts b/src/domlink.ts index 9815110..b5f13dc 100644 --- a/src/domlink.ts +++ b/src/domlink.ts @@ -65,6 +65,27 @@ export class Row extends Container { this.wraps.classList.add("LRow"); } } +export class Table extends Node { + constructor() { + let table = document.createElement("table"); + table.classList.add("LTable"); + super(table); + } +} +export class TableRow extends Node { + constructor() { + let tr = document.createElement("tr"); + tr.classList.add("LTableRow"); + super(tr); + } +} +export class TableCell extends Node { + constructor() { + let td = document.createElement("td"); + td.classList.add("LTableCell"); + super(td); + } +} export class Text extends Node { private _text: string = ""; get text() { diff --git a/src/issuesearch.ts b/src/issuesearch.ts new file mode 100644 index 0000000..5a4db98 --- /dev/null +++ b/src/issuesearch.ts @@ -0,0 +1,148 @@ +import { Body, Button, Column, Input, Label, Row, Table, TableCell, TableRow } from "./domlink.js"; + +type issueRecord = { + body: string; + createdAt: string; + issueId: number; + owner: string; + repo: string; + title: string; +}; +type constellationRecordItem = { + did: string; + collection: string; + rkey: string; +}; +type constellationResponse = { + total: number; + linking_records: [constellationRecordItem]; + cursor: string; +}; +type slingshotResponse = { + cid: string; + uri: string; + value: T; +}; +type miniDoc = { + did: string; + handle: string; + pds: string; + signing_key: string; +}; +type repoRecord = { + knot: string; + name: string; + owner: string; + createdAt: string; +}; +type recordListingItem = { + uri: string; + cid: string; + value: T; +}; +type recordListing = { + cursor: string; + records: [recordListingItem]; +}; + +async function xcall(host: string, method: string, params: Record | null = null): Promise { + let url = `${host}/xrpc/${method}`; + if (params) { + let usp = new URLSearchParams(); + for (let key in params) { + usp.append(key, params[key].toString()); + } + url += "?" + usp.toString(); + } + return await (await fetch(url)).json(); +} + +const SLINGSHOT = "https://slingshot.microcosm.blue"; +async function listIssues(repo: string) { + let repoUrl = new URL(repo); + let repoUrlSplat = repoUrl.pathname.split("/"); + let repoOwnerHandle = repoUrlSplat[1].substring(1); + let repoName = repoUrlSplat[2]; + let repoOwnerDoc = await xcall(SLINGSHOT, "com.bad-example.identity.resolveMiniDoc", {identifier: repoOwnerHandle}) as miniDoc; + let ownedRepos = await xcall(repoOwnerDoc.pds, "com.atproto.repo.listRecords", { repo: repoOwnerDoc.did, collection: "sh.tangled.repo" }) as recordListing; + let repoRecord = ownedRepos.records.find(x=>x.value.name == repoName)!; + let encodedRepoUri = encodeURIComponent(repoRecord.uri); + let view_issues_list = new Table().add( + new TableRow().with( + new TableCell().add("Handle"), + new TableCell().add("Issue#"), + new TableCell().add("Title") + ) + ); + Body.add(view_issues_list); + let irsp = await (await fetch(`https://constellation.microcosm.blue/links?target=${encodedRepoUri}&collection=sh.tangled.repo.issue&path=.repo`)).json() as constellationResponse; + let allIssueRefs: constellationRecordItem[] = irsp.linking_records; + let nextCursor = irsp.cursor; + while (allIssueRefs.length < irsp.total && nextCursor != null) { + let rsp = await (await fetch(`https://constellation.microcosm.blue/links?target=${encodedRepoUri}&collection=sh.tangled.repo.issue&path=.repo&cursor=${nextCursor}`)).json() as constellationResponse; + nextCursor = rsp.cursor; + allIssueRefs = allIssueRefs.concat(rsp.linking_records); + if (nextCursor == null) break; + } + let sortedIssues = (await Promise.all(allIssueRefs.map(async (issueRef) => { + let rsp = await xcall(SLINGSHOT, "com.atproto.repo.getRecord", { + repo: issueRef.did, + collection: issueRef.collection, + rkey: issueRef.rkey + }) as slingshotResponse; + let issue = rsp.value; + let doc; + try { + doc = await xcall(SLINGSHOT, "com.bad-example.identity.resolveMiniDoc", { identifier: issue.owner }) as miniDoc; + } catch (error) { + return null; + } + let handle = doc.handle; + return { + handle: handle, + issue: issue + }; + }))).sort((a,b)=>{ + let x = 0; + if (a && b) { + x = b.issue.issueId-a.issue.issueId + } + return x; + }); + sortedIssues.forEach(that =>{ + if (that) { + let view_issue_row = new TableRow(); + view_issue_row.with( + new TableCell().add(that.handle), + new TableCell().add(that.issue.issueId.toString()), + new TableCell().add(that.issue.title) + ); + view_issues_list.add(view_issue_row); + } + }); + return view_issues_list; +} + + +let repoUrl = new Input(); +let last: any; +let statusText = new Label("waiting"); +let runButton = new Button("GO!",async ()=>{ + (runButton.wraps as HTMLButtonElement).disabled = true; + if (last) { + Body.remove(last); + } + try { + last = await listIssues(repoUrl.value); + } catch (e) { + last = new Label("Failed"); + Body.add(last); + } + console.log(last); + (runButton.wraps as HTMLButtonElement).disabled = false; +}); +Body.add(new Row().with( + "List issues for repo: ", + repoUrl, + runButton +)); \ No newline at end of file diff --git a/static/issuesearch.css b/static/issuesearch.css new file mode 100644 index 0000000..e63ca69 --- /dev/null +++ b/static/issuesearch.css @@ -0,0 +1,12 @@ +tr:nth-child(1) { + font-weight: bold; +} +tr:nth-child(1)>* { + text-align: center !important; +} +td { + padding-right: 2ch; +} +td:nth-child(1) { + text-align: right; +} \ No newline at end of file diff --git a/static/issuesearch.html b/static/issuesearch.html new file mode 100644 index 0000000..00a1f8d --- /dev/null +++ b/static/issuesearch.html @@ -0,0 +1,15 @@ + + + + + + issuesearch + + + + + + + + +