249 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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);
 |