tildefriends/apps/issues/script.js

249 lines
6.3 KiB
JavaScript

import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
class TfComposeElement extends LitElement {
static get properties() {
return {
value: {type: String},
};
}
input() {
let input = this.renderRoot.getElementById('input');
let preview = this.renderRoot.getElementById('preview');
if (input && preview) {
preview.innerHTML = tfutils.markdown(input.value);
}
}
submit() {
this.dispatchEvent(
new CustomEvent('tf-submit', {
bubbles: true,
composed: true,
detail: {
value: this.renderRoot.getElementById('input').value,
},
})
);
this.renderRoot.getElementById('input').value = '';
this.input();
}
render() {
return html`
<div style="display: flex; flex-direction: row">
<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
<div id="preview" style="flex: 1 1"></div>
</div>
<input type="submit" value="Submit" @click=${this.submit}></input>
`;
}
}
customElements.define('tf-compose', TfComposeElement);
class TfIssuesAppElement extends LitElement {
static get properties() {
return {
issues: {type: Array},
selected: {type: Object},
};
}
constructor() {
super();
this.issues = [];
this.load();
}
async load() {
let issues = {};
let messages = await tfrpc.rpc.query(
`
WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON
messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON
issues.id = messages_refs.ref JOIN messages ON
messages.id = messages_refs.message
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
SELECT * FROM issues
UNION
SELECT * FROM edits ORDER BY timestamp
`,
[k_project]
);
for (let message of messages) {
let content = JSON.parse(message.content);
switch (content.type) {
case 'issue':
issues[message.id] = {
id: message.id,
author: message.author,
text: content.text,
updates: [],
created: message.timestamp,
open: true,
};
break;
case 'issue-edit':
case 'post':
for (let issue of content.issues || []) {
if (issues[issue.link]) {
if (issue.open !== undefined) {
issues[issue.link].open = issue.open;
message.open = issue.open;
}
issues[issue.link].updates.push(message);
issues[issue.link].updated = message.timestamp;
}
}
break;
}
}
this.issues = Object.values(issues).sort(
(x, y) => y.open - x.open || y.created - x.created
);
if (this.selected) {
for (let issue of this.issues) {
if (issue.id == this.selected.id) {
this.selected = issue;
}
}
}
}
render_issue_table_row(issue) {
return html`
<tr>
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
<td
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
${issue.author}
</td>
<td
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
@click=${() => (this.selected = issue)}
>
${issue.text.split('\n')?.[0]}
</td>
<td>
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
</td>
</tr>
`;
}
render_update(update) {
let content = JSON.parse(update.content);
let message;
if (content.text) {
message = unsafeHTML(tfutils.markdown(content.text));
}
return html`
<div style="border-left: 2px solid #fff; padding-left: 8px">
<div>${new Date(update.timestamp).toLocaleString()}</div>
<div>${update.author}</div>
<div>${message}</div>
<div>
${update.open !== undefined
? update.open
? 'issue opened'
: 'issue closed'
: undefined}
</div>
</div>
`;
}
async set_open(id, open) {
if (
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit',
issues: [
{
link: id,
open: open,
},
],
});
await this.load();
}
}
async create_issue(event) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue',
project: k_project,
text: event.detail.value,
});
await this.load();
}
async reply_to_issue(event) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'post',
text: event.detail.value,
root: this.selected.id,
branch: this.selected.updates.length
? this.selected.updates[this.selected.updates.length - 1].id
: this.selected.id,
issues: [
{
link: this.selected.id,
},
],
});
await this.load();
}
render() {
let header = html` <h1>Tilde Friends Issues</h1> `;
if (this.selected) {
return html`
${header}
<div>
<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
${
this.selected.open
? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
}
</div>
<div>${new Date(this.selected.created).toLocaleString()}</div>
<div>${this.selected.author}</div>
<div>${this.selected.id}</div>
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
${this.selected.updates.map((x) => this.render_update(x))}
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
`;
} else {
return html`
${header}
<h2>New Issue</h2>
<tf-compose @tf-submit=${this.create_issue}></tf-compose>
<table>
<tr>
<th>Status</th>
<th>Author</th>
<th>Title</th>
<th>Date</th>
</tr>
${this.issues.map((x) => this.render_issue_table_row(x))}
</table>
`;
}
}
}
customElements.define('tf-issues-app', TfIssuesAppElement);