From b83d761f6b73f8a2aeb279b9fbb054aee2def3cf Mon Sep 17 00:00:00 2001
From: hotsocket
Date: Sat, 23 Aug 2025 23:39:58 -0500
Subject: [PATCH] issue search; added Link(a), Updatable for Input
---
readme | 4 +
src/domlink.ts | 295 ++++++++++++++++++++++------------------
src/issuesearch.ts | 101 ++++++++++----
static/issuesearch.html | 16 +--
4 files changed, 253 insertions(+), 163 deletions(-)
diff --git a/readme b/readme
index d5f5df0..786f969 100644
--- a/readme
+++ b/readme
@@ -14,3 +14,7 @@ also i want to say that like this isnt a super serious project like fyi whoever
>i dont want to wrestle npm if i can help it.
lol lmao i really thought i could get away with a single pure js file
+
+hey! heads up! im just thinking that as i add things someone might go "wow i want to use that"
+i dont know how packaging works (yet). if you can somehow pull my code into your project as it stands, expect it to break.
+its not good for your health. just dont. not yet?
diff --git a/src/domlink.ts b/src/domlink.ts
index b5f13dc..8f124f7 100644
--- a/src/domlink.ts
+++ b/src/domlink.ts
@@ -1,158 +1,193 @@
let _LCounter = 0; // is there a point to this? idk. might be unnecessary overhead.
export class Node {
- wraps: HTMLElement;
- children: Node[] = [];
- id: string = "L" + _LCounter++;;
- constructor(wrap: HTMLElement) {
- this.wraps = wrap;
- this.wraps.id = this.id;
- }
- // could i make add and with one function? yeah.
- // will i do that? no.
- // why not? because it feels nice to have them separate.
- add(node: Node | string) {
- if (node != undefined) {
- let xnode: Node;
- if (typeof node === "string") {
- xnode = new Label(node);
- } else {
- xnode = node;
- }
- this.children.push(xnode);
- this.wraps.appendChild(xnode.wraps);
- } else {
- throw new Error("node undefined");
- }
- return this;
- }
- remove(node: Node) {
- let idx = this.children.indexOf(node);
- if (idx < 0) {
- throw new Error("node not a child of this node");
- }
- this.children.splice(idx, 1);
- this.wraps.removeChild(node.wraps);
- }
- with(...nodes: (Node | string)[]) {
- nodes.forEach(x=>{this.add(x)});
- return this
- }
- style(fn: (style: CSSStyleDeclaration) => void) {
- fn(this.wraps.style);
- return this;
- }
- class(cls: string) {
- this.wraps.classList.add(cls);
- return this;
- }
+ wraps: HTMLElement;
+ children: Node[] = [];
+ id: string = "L" + _LCounter++;;
+ constructor(wrap: HTMLElement) {
+ this.wraps = wrap;
+ this.wraps.id = this.id;
+ }
+ // could i make add and with one function? yeah.
+ // will i do that? no.
+ // why not? because it feels nice to have them separate.
+ add(node: Node | string) {
+ if (node != undefined) {
+ let xnode: Node;
+ if (typeof node === "string") {
+ xnode = new Label(node);
+ } else {
+ xnode = node;
+ }
+ this.children.push(xnode);
+ this.wraps.appendChild(xnode.wraps);
+ } else {
+ throw new Error("node undefined");
+ }
+ return this;
+ }
+ remove(node: Node) {
+ let idx = this.children.indexOf(node);
+ if (idx < 0) {
+ throw new Error("node not a child of this node");
+ }
+ this.children.splice(idx, 1);
+ this.wraps.removeChild(node.wraps);
+ }
+ with(...nodes: (Node | string)[]) {
+ nodes.forEach(x=>{this.add(x)});
+ return this
+ }
+ style(fn: (style: CSSStyleDeclaration) => void) {
+ fn(this.wraps.style);
+ return this;
+ }
+ class(cls: string) {
+ this.wraps.classList.add(cls);
+ return this;
+ }
+}
+type NodeEquivalent = (Node | string)
+export class Link extends Node {
+ constructor(around: NodeEquivalent | null) {
+ let a = document.createElement("a");
+ a.classList.add("LLink");
+ super(a);
+ if (around != null) {
+ this.add(around);
+ }
+ }
+ set target(x: string) {
+ (this.wraps as HTMLAnchorElement).href = x;
+ }
+ get target(): string {
+ return (this.wraps as HTMLAnchorElement).href;
+ }
+ to(tgt: string) {
+ this.target = tgt;
+ return this;
+ }
}
export class Container extends Node {
- constructor() {
- let div = document.createElement("div");
- div.classList.add("LContainer");
- super(div);
- }
+ constructor() {
+ let div = document.createElement("div");
+ div.classList.add("LContainer");
+ super(div);
+ }
}
export class Column extends Container {
- constructor() {
- super();
- this.wraps.classList.add("LColumn");
- }
+ constructor() {
+ super();
+ this.wraps.classList.add("LColumn");
+ }
}
export class Row extends Container {
- constructor() {
- super();
- this.wraps.classList.add("LRow");
- }
+ constructor() {
+ super();
+ this.wraps.classList.add("LRow");
+ }
}
export class Table extends Node {
- constructor() {
- let table = document.createElement("table");
- table.classList.add("LTable");
- super(table);
- }
+ 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);
- }
+ 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);
- }
+ constructor() {
+ let td = document.createElement("td");
+ td.classList.add("LTableCell");
+ super(td);
+ }
}
export class Text extends Node {
- private _text: string = "";
- get text() {
- return this._text;
- }
- set text(x) {
- this._text = x;
- this.wraps.textContent = x;
- }
+ private _text: string = "";
+ get text() {
+ return this._text;
+ }
+ set text(x) {
+ this._text = x;
+ this.wraps.textContent = x;
+ }
}
export class Label extends Text {
- constructor(text: string = "") {
- let el = document.createElement("span");
- el.classList.add("LLabel");
- super(el);
- this.text = text;
- }
+ constructor(text: string = "") {
+ let el = document.createElement("span");
+ el.classList.add("LLabel");
+ super(el);
+ this.text = text;
+ }
}
export class Button extends Text {
- constructor(label: string, action: EventListener) {
- let btn = document.createElement("button");
- btn.textContent = label;
- btn.addEventListener("click", action);
- btn.classList.add("LButton");
- super(btn);
- this.text = label;
- }
+ constructor(label: string, action: EventListener) {
+ let btn = document.createElement("button");
+ btn.textContent = label;
+ btn.addEventListener("click", action);
+ btn.classList.add("LButton");
+ super(btn);
+ this.text = label;
+ }
+}
+interface Updatable {
+ watch: (watcher: (newValue: string)=>void) => void;
}
-export class Input extends Text {
- constructor(type: HTMLInputElement["type"] = "text") {
- let el = document.createElement("input");
- el.type = type;
- el.classList.add("LInput");
- super(el);
- }
- set placeholder(x: string) {
- (this.wraps as HTMLInputElement).placeholder = x;
- }
- get value(): string {
- return (this.wraps as HTMLInputElement).value;
- }
- set value(x: string) {
- (this.wraps as HTMLInputElement).value = x;
- }
+export class Input extends Text implements Updatable {
+ constructor(type: HTMLInputElement["type"] = "text") {
+ let el = document.createElement("input");
+ el.type = type;
+ el.classList.add("LInput");
+ super(el);
+ }
+ set placeholder(x: string) {
+ (this.wraps as HTMLInputElement).placeholder = x;
+ }
+ get value(): string {
+ return (this.wraps as HTMLInputElement).value;
+ }
+ set value(x: string) {
+ (this.wraps as HTMLInputElement).value = x;
+ this.watching.forEach(y=>y(x));
+ }
+
+
+
+ // Implementation for Updatable
+ watching: ((newValue: string) => void)[] = [];
+ watch(watcher: (newValue: string)=>void) {
+ this.watching.push(watcher);
+ this.wraps.oninput = (e)=>{watcher(this.value)};
+ watcher(this.value);
+ }
}
export class Image extends Node {
- constructor(src: string) {
- let img = document.createElement("img");
- img.src = src;
- img.classList.add("LImage");
- super(img);
- }
+ constructor(src: string) {
+ let img = document.createElement("img");
+ img.src = src;
+ img.classList.add("LImage");
+ super(img);
+ }
}
export class Modal extends Node {
- constructor() {
- let dlg: HTMLDialogElement = document.createElement("dialog");
- dlg.classList.add("LModal");
- super(dlg);
- }
- show() {
- Body.add(this);
- (this.wraps as HTMLDialogElement).showModal();
- }
- hide() {
- (this.wraps as HTMLDialogElement).close();
- Body.wraps.removeChild(this.wraps);
- }
+ constructor() {
+ let dlg: HTMLDialogElement = document.createElement("dialog");
+ dlg.classList.add("LModal");
+ super(dlg);
+ }
+ show() {
+ Body.add(this);
+ (this.wraps as HTMLDialogElement).showModal();
+ }
+ hide() {
+ (this.wraps as HTMLDialogElement).close();
+ Body.wraps.removeChild(this.wraps);
+ }
}
// user helper shit
diff --git a/src/issuesearch.ts b/src/issuesearch.ts
index 5a4db98..0514ed8 100644
--- a/src/issuesearch.ts
+++ b/src/issuesearch.ts
@@ -1,7 +1,7 @@
-import { Body, Button, Column, Input, Label, Row, Table, TableCell, TableRow } from "./domlink.js";
+import { Body, Button, Column, Input, Label, Link, Row, Table, TableCell, TableRow } from "./domlink.js";
type issueRecord = {
- body: string;
+ body: string;
createdAt: string;
issueId: number;
owner: string;
@@ -57,34 +57,34 @@ async function xcall(host: string, method: string, params: Record;
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);
+ statusText.text = `finding issues...`;
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;
+ statusText.text = `finding issues... (${allIssueRefs.length}/${irsp.total})`;
+ let rsp = await (await fetch(`https://constellation.microcosm.blue/links?target=${encodedRepoUri}&collection=sh.tangled.repo.issue&path=.repo&cursor=${nextCursor}`, {headers:{Accept:"application/json"}})).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) => {
+ allIssues = (await Promise.all(allIssueRefs.map(async (issueRef) => {
let rsp = await xcall(SLINGSHOT, "com.atproto.repo.getRecord", {
repo: issueRef.did,
collection: issueRef.collection,
@@ -97,25 +97,36 @@ async function listIssues(repo: string) {
} catch (error) {
return null;
}
+ statusText.text = `retrieved issue #${issue.issueId} (not in order!)`;
let handle = doc.handle;
return {
handle: handle,
issue: issue
};
- }))).sort((a,b)=>{
+ }))).filter(x=>x as issueItem).sort((a,b)=>{
let x = 0;
if (a && b) {
x = b.issue.issueId-a.issue.issueId
}
return x;
- });
- sortedIssues.forEach(that =>{
- if (that) {
+ }) as issueItem[];
+ statusText.text = `got ${allIssues.length} issues!`;
+}
+function renderIssues(issues:issueItem[]) {
+ let view_issues_list = new Table().add(
+ new TableRow().with(
+ new TableCell().add("Handle"),
+ new TableCell().add("Issue#"),
+ new TableCell().add("Title")
+ )
+ );
+ issues.forEach(issue =>{
+ if (issue) {
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)
+ new TableCell().add(new Link(issue.handle).to(`https://tangled.sh/@${issue.handle}`)),
+ new TableCell().add(new Link(issue.issue.issueId.toString()).to(`${currentRepoLink}/issues/${issue.issue.issueId}`)),
+ new TableCell().add(issue.issue.title)
);
view_issues_list.add(view_issue_row);
}
@@ -130,19 +141,59 @@ let statusText = new Label("waiting");
let runButton = new Button("GO!",async ()=>{
(runButton.wraps as HTMLButtonElement).disabled = true;
if (last) {
- Body.remove(last);
+ try {
+ Body.remove(last);
+ } catch {}
+ last = null;
}
+ allIssues = [];
+ searchRow.style(x=>x.display="none");
try {
- last = await listIssues(repoUrl.value);
+ await getIssues(repoUrl.value);
+ console.log(allIssues);
+ last = renderIssues(allIssues);
+ searchRow.style(x=>x.removeProperty("display"));
} catch (e) {
last = new Label("Failed");
- Body.add(last);
+ console.log(e);
}
+ Body.add(last);
+ searchInput.value = "";
console.log(last);
(runButton.wraps as HTMLButtonElement).disabled = false;
});
+
+
+
+let searchInput = new Input();
+searchInput.watch((search)=>{
+ if (allIssues.length > 0) {
+ if (last) {
+ Body.remove(last);
+ }
+ search = search.toLowerCase();
+ last = renderIssues(allIssues.filter((item)=>{
+ return item.issue.issueId == Number(item) ||
+ search.split(" ").every((word)=> (
+ item.handle.toLowerCase().includes(word) ||
+ item.issue.title.toLowerCase().includes(word) ||
+ item.issue.body.toLowerCase().includes(word)))
+ }));
+ Body.add(last);
+ }
+});
+let searchRow = new Row().with(
+ "search",
+ searchInput
+);
+searchRow.style(x=>x.display="none");
+
+
+
Body.add(new Row().with(
"List issues for repo: ",
repoUrl,
- runButton
-));
\ No newline at end of file
+ runButton,
+ statusText
+));
+Body.add(searchRow);
diff --git a/static/issuesearch.html b/static/issuesearch.html
index 00a1f8d..12e2ee9 100644
--- a/static/issuesearch.html
+++ b/static/issuesearch.html
@@ -1,15 +1,15 @@
-
-
- issuesearch
-
-
-
+
+
+ issuesearch
+
+
+
-
-
+
+
--
2.51.2
|