forked from retoor/devplacepy
fix: correct "bugs" to "issues" in routing table and README references across multiple documentation files
This commit is contained in:
@@ -458,6 +458,30 @@ img {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@keyframes btn-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.btn-spinner {
|
||||
display: none;
|
||||
width: 0.85em;
|
||||
height: 0.85em;
|
||||
margin-right: 0.4em;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
vertical-align: -0.15em;
|
||||
animation: btn-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.btn.is-loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn.is-loading .btn-spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -544,8 +568,9 @@ img {
|
||||
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; background: none; border: none; cursor: pointer; font-family: inherit; line-height: 1; }
|
||||
.topnav-icon:hover { color: var(--text-primary); }
|
||||
.topnav-icon.active { color: var(--accent); }
|
||||
.nav-bell { position: relative; display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
|
||||
.nav-badge {
|
||||
position: absolute; top: 0; right: 0; z-index: 1; min-width: 16px; height: 16px;
|
||||
position: absolute; top: -6px; right: -8px; z-index: 1; min-width: 16px; height: 16px;
|
||||
padding: 0 0.25rem; border-radius: 8px; background: var(--accent); color: var(--white);
|
||||
font-size: 0.6875rem; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@@ -867,8 +892,19 @@ button.comment-form-submit:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
body:has(.page-messages) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page:has(.page-messages) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
/* retoor <retoor@molodetz.nl> */
|
||||
|
||||
.bugs-layout {
|
||||
.issues-layout {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.bugs-header {
|
||||
.issues-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bugs-header h1 {
|
||||
.issues-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bugs-filters {
|
||||
.issues-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bugs-filter {
|
||||
.issues-filter {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
@@ -28,20 +28,20 @@
|
||||
border: 1px solid var(--border);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bugs-filter:hover {
|
||||
.issues-filter:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.bugs-filter.active {
|
||||
.issues-filter.active {
|
||||
background: var(--accent);
|
||||
color: var(--white);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.bugs-modal-hint {
|
||||
.issues-modal-hint {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.bug-card {
|
||||
.issue-card {
|
||||
display: block;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
@@ -51,29 +51,29 @@
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
.bug-card:hover {
|
||||
.issue-card:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.bug-card-header {
|
||||
.issue-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.bug-number {
|
||||
.issue-number {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.bug-title {
|
||||
.issue-title {
|
||||
flex: 1;
|
||||
}
|
||||
.bug-title {
|
||||
.issue-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.bug-status {
|
||||
.issue-status {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
@@ -81,75 +81,75 @@
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.bug-status.open {
|
||||
.issue-status.open {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
.bug-status.closed {
|
||||
.issue-status.closed {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
.bug-desc {
|
||||
.issue-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.bug-meta {
|
||||
.issue-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.bug-dot {
|
||||
.issue-dot {
|
||||
color: var(--border);
|
||||
}
|
||||
|
||||
.bug-detail-nav {
|
||||
.issue-detail-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bug-back,
|
||||
.bug-gitea-link {
|
||||
.issue-back,
|
||||
.issue-gitea-link {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bug-detail-card {
|
||||
.issue-detail-card {
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.bug-detail-header {
|
||||
.issue-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.bug-detail-title {
|
||||
.issue-detail-title {
|
||||
flex: 1;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bug-admin-actions {
|
||||
.issue-admin-actions {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.bug-detail-body {
|
||||
.issue-detail-body {
|
||||
margin-top: 1rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.bug-comment {
|
||||
.issue-comment {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.bug-comment-head {
|
||||
.issue-comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
@@ -157,7 +157,7 @@
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.bug-comment-badge {
|
||||
.issue-comment-badge {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
@@ -167,15 +167,15 @@
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
.bug-comment-body {
|
||||
.issue-comment-body {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.bug-comment-form {
|
||||
.issue-comment-form {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.bug-comment-form textarea {
|
||||
.issue-comment-form textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
@@ -185,28 +185,28 @@
|
||||
color: var(--text-primary);
|
||||
resize: vertical;
|
||||
}
|
||||
.bug-comment-form-footer {
|
||||
.issue-comment-form-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.bugs-header {
|
||||
.issues-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.bugs-header h1 {
|
||||
.issues-header h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.bug-card {
|
||||
.issue-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.bug-card-header {
|
||||
.issue-card-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
/* retoor <retoor@molodetz.nl> */
|
||||
|
||||
.page-messages {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.messages-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 320px) 1fr;
|
||||
gap: 0;
|
||||
height: calc(100dvh - var(--nav-height) - 3.5rem);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
@@ -16,6 +24,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.messages-list-header {
|
||||
@@ -58,8 +67,16 @@
|
||||
}
|
||||
|
||||
.messages-conversations {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.messages-conversations::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
@@ -176,6 +193,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.messages-input-area input[type="text"] {
|
||||
@@ -248,8 +266,7 @@
|
||||
@media (max-width: 768px) {
|
||||
.messages-layout {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
min-height: calc(100dvh - var(--nav-height) - 3.5rem);
|
||||
min-height: 0;
|
||||
}
|
||||
.messages-list {
|
||||
display: flex;
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.profile-role {
|
||||
@@ -135,7 +137,8 @@ a.profile-stat-value:hover {
|
||||
.profile-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
@@ -146,16 +149,27 @@ a.profile-stat-value:hover {
|
||||
|
||||
.profile-info-label {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-info-row > .flex-center-gap {
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.profile-info-value {
|
||||
color: var(--text-secondary);
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.profile-info-edit {
|
||||
color: var(--text-muted);
|
||||
padding: 0.25rem;
|
||||
font-size: 0.8125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-info-edit:hover {
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
|
||||
import { ApiTester } from "./ApiTester.js";
|
||||
import { CodeCopy } from "./CodeCopy.js";
|
||||
import { DevRantTester, DevRantLogin } from "./DevRantTester.js";
|
||||
|
||||
export class ApiDocs {
|
||||
constructor() {
|
||||
this.copy = new CodeCopy();
|
||||
const mounts = document.querySelectorAll("[data-api-tester]");
|
||||
if (!mounts.length) return;
|
||||
const ctx = window.DEVPLACE_DOCS || { loggedIn: false, username: "", apiKey: "", isAdmin: false };
|
||||
ctx.base = location.origin;
|
||||
const mounts = document.querySelectorAll("[data-api-tester]");
|
||||
this.testers = [...mounts].map((mount) => new ApiTester(mount, ctx));
|
||||
this.devrantLogins = [...document.querySelectorAll("[data-devrant-login]")].map(
|
||||
(mount) => new DevRantLogin(mount),
|
||||
);
|
||||
this.devrantTesters = [...document.querySelectorAll("[data-devrant-tester]")].map(
|
||||
(mount) => new DevRantTester(mount),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { Tabs } from "./Tabs.js";
|
||||
import { DeviiTerminal } from "./DeviiTerminal.js";
|
||||
import { ZipDownloader } from "./ZipDownloader.js";
|
||||
import { ProjectForker } from "./ProjectForker.js";
|
||||
import { BugReporter } from "./BugReporter.js";
|
||||
import { IssueReporter } from "./IssueReporter.js";
|
||||
import { MediaGallery } from "./MediaGallery.js";
|
||||
import WindowManager from "./components/WindowManager.js";
|
||||
import { ContainerTerminalManager } from "./ContainerTerminalManager.js";
|
||||
@@ -65,7 +65,7 @@ class Application {
|
||||
this.devii = new DeviiTerminal();
|
||||
this.zipDownloader = new ZipDownloader();
|
||||
this.projectForker = new ProjectForker();
|
||||
this.bugReporter = new BugReporter();
|
||||
this.issueReporter = new IssueReporter();
|
||||
this.mediaGallery = new MediaGallery();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
const STORAGE_KEY = "devrant-docs-auth";
|
||||
|
||||
class DevRantSession {
|
||||
constructor() {
|
||||
const docs = window.DEVPLACE_DOCS || {};
|
||||
this.base = docs.base || location.origin;
|
||||
this.auth = null;
|
||||
this.username = "";
|
||||
this.listeners = new Set();
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
this.auth = parsed.auth || null;
|
||||
this.username = parsed.username || "";
|
||||
}
|
||||
} catch {
|
||||
this.auth = null;
|
||||
}
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
return Boolean(this.auth && this.auth.token_key);
|
||||
}
|
||||
|
||||
triple() {
|
||||
return this.isLoggedIn() ? { ...this.auth } : {};
|
||||
}
|
||||
|
||||
onChange(callback) {
|
||||
this.listeners.add(callback);
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
_emit() {
|
||||
for (const callback of this.listeners) callback();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ auth: this.auth, username: this.username }),
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setFromToken(token, username) {
|
||||
this.auth = {
|
||||
user_id: token.user_id,
|
||||
token_id: token.id,
|
||||
token_key: token.key,
|
||||
};
|
||||
if (username) this.username = username;
|
||||
this._persist();
|
||||
this._emit();
|
||||
}
|
||||
|
||||
async login(username, password) {
|
||||
const response = await fetch(`${this.base}/api/users/auth-token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({ username, password }).toString(),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!data.success) throw new Error(data.error || "login failed");
|
||||
this.setFromToken(data.auth_token, username);
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.auth = null;
|
||||
this.username = "";
|
||||
this._persist();
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
|
||||
export const devrantSession = new DevRantSession();
|
||||
@@ -0,0 +1,315 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { CodeBlock } from "./CodeBlock.js";
|
||||
import { devrantSession } from "./DevRantSession.js";
|
||||
|
||||
function el(tag, props = {}, children = []) {
|
||||
const node = document.createElement(tag);
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (key === "class") node.className = value;
|
||||
else if (key === "text") node.textContent = value;
|
||||
else if (key === "html") node.innerHTML = value;
|
||||
else if (value === true) node.setAttribute(key, "");
|
||||
else if (value !== false && value !== null) node.setAttribute(key, value);
|
||||
}
|
||||
for (const child of children) {
|
||||
if (child) node.appendChild(child);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export class DevRantLogin {
|
||||
constructor(mount) {
|
||||
this.mount = mount;
|
||||
this.render();
|
||||
devrantSession.onChange(() => this.render());
|
||||
}
|
||||
|
||||
render() {
|
||||
this.mount.innerHTML = "";
|
||||
const bar = el("div", { class: "api-tester devrant-login" });
|
||||
if (devrantSession.isLoggedIn()) {
|
||||
bar.appendChild(
|
||||
el("span", {
|
||||
class: "try-note",
|
||||
text: `Authenticated as ${devrantSession.username || "user " + devrantSession.auth.user_id} (token #${devrantSession.auth.token_id}).`,
|
||||
}),
|
||||
);
|
||||
const out = el("button", { type: "button", class: "btn try-send", text: "Log out" });
|
||||
out.addEventListener("click", () => devrantSession.logout());
|
||||
bar.appendChild(out);
|
||||
this.mount.appendChild(bar);
|
||||
return;
|
||||
}
|
||||
const user = el("input", { class: "param-input", type: "text", placeholder: "username or email" });
|
||||
const pass = el("input", { class: "param-input", type: "password", placeholder: "password" });
|
||||
const docs = window.DEVPLACE_DOCS || {};
|
||||
if (docs.username) user.value = docs.username;
|
||||
const send = el("button", { type: "button", class: "btn btn-primary try-send", text: "Log in" });
|
||||
const note = el("span", { class: "try-note", text: "Log in once to enable the authenticated widgets on every devRant page." });
|
||||
send.addEventListener("click", async () => {
|
||||
send.disabled = true;
|
||||
note.textContent = "Logging in...";
|
||||
try {
|
||||
await devrantSession.login(user.value.trim(), pass.value);
|
||||
} catch (error) {
|
||||
note.textContent = error.message || "Login failed";
|
||||
send.disabled = false;
|
||||
}
|
||||
});
|
||||
bar.appendChild(el("div", { class: "auth-field" }, [el("label", { text: "Username" }), user]));
|
||||
bar.appendChild(el("div", { class: "auth-field" }, [el("label", { text: "Password" }), pass]));
|
||||
bar.appendChild(send);
|
||||
bar.appendChild(note);
|
||||
this.mount.appendChild(bar);
|
||||
}
|
||||
}
|
||||
|
||||
export class DevRantTester {
|
||||
constructor(mount) {
|
||||
this.mount = mount;
|
||||
try {
|
||||
this.config = JSON.parse(mount.dataset.config || "{}");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
this.base = (window.DEVPLACE_DOCS || {}).base || location.origin;
|
||||
this.inputs = [];
|
||||
this.build();
|
||||
this.renderExpected();
|
||||
if (this.requiresAuth()) {
|
||||
devrantSession.onChange(() => this.syncAuth());
|
||||
this.syncAuth();
|
||||
}
|
||||
}
|
||||
|
||||
requiresAuth() {
|
||||
return this.config.auth === "user";
|
||||
}
|
||||
|
||||
build() {
|
||||
const root = el("div", { class: "api-tester" });
|
||||
if (this.config.params.length) root.appendChild(this.buildParams());
|
||||
if (this.requiresAuth()) {
|
||||
this.authNote = el("div", { class: "param-desc devrant-auth-note" });
|
||||
root.appendChild(this.authNote);
|
||||
}
|
||||
root.appendChild(this.buildActions());
|
||||
root.appendChild(this.buildResponse());
|
||||
this.mount.appendChild(root);
|
||||
}
|
||||
|
||||
buildParams() {
|
||||
const table = el("div", { class: "param-table" });
|
||||
for (const param of this.config.params) {
|
||||
const control = this.buildControl(param);
|
||||
this.inputs.push({ param, control });
|
||||
const label = el("div", { class: "param-label" }, [
|
||||
el("span", { class: "param-name", text: param.name }),
|
||||
param.required ? el("span", { class: "param-required", text: "*" }) : null,
|
||||
el("span", { class: "param-loc param-loc-" + param.location, text: param.location }),
|
||||
]);
|
||||
const meta = el("div", { class: "param-meta" }, [
|
||||
control,
|
||||
param.description ? el("div", { class: "param-desc", text: param.description }) : null,
|
||||
]);
|
||||
table.appendChild(el("div", { class: "param-row" }, [label, meta]));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
buildControl(param) {
|
||||
if (param.type === "enum") {
|
||||
const select = el("select", { class: "param-input" });
|
||||
for (const option of param.options || []) {
|
||||
const opt = el("option", { value: option, text: option });
|
||||
if (option === param.example) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
if (param.type === "textarea") {
|
||||
const area = el("textarea", { class: "param-input", rows: "3" });
|
||||
area.value = param.example || "";
|
||||
return area;
|
||||
}
|
||||
const input = el("input", {
|
||||
class: "param-input",
|
||||
type: param.type === "int" ? "number" : param.type === "password" ? "password" : "text",
|
||||
placeholder: param.example || "",
|
||||
});
|
||||
if (param.type !== "password") input.value = param.example || "";
|
||||
return input;
|
||||
}
|
||||
|
||||
buildActions() {
|
||||
const bar = el("div", { class: "try-panel" });
|
||||
this.sendBtn = el("button", { type: "button", class: "btn btn-primary try-send", text: "Send request" });
|
||||
this.sendBtn.addEventListener("click", () => this.send());
|
||||
bar.appendChild(this.sendBtn);
|
||||
return bar;
|
||||
}
|
||||
|
||||
syncAuth() {
|
||||
if (!this.authNote) return;
|
||||
if (devrantSession.isLoggedIn()) {
|
||||
this.authNote.textContent = `Authenticated as ${devrantSession.username || "user " + devrantSession.auth.user_id}.`;
|
||||
this.sendBtn.disabled = false;
|
||||
} else {
|
||||
this.authNote.textContent = "Requires login - use the Log in widget at the top of the page.";
|
||||
this.sendBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
buildResponse() {
|
||||
const wrap = el("div", { class: "response-viewer" });
|
||||
const tabs = el("div", { class: "response-tabs" });
|
||||
this.responseTabs = {};
|
||||
for (const tab of [{ id: "expected", label: "Expected" }, { id: "live", label: "Live response" }]) {
|
||||
const btn = el("button", { type: "button", class: "code-tab", text: tab.label });
|
||||
btn.addEventListener("click", () => this.selectTab(tab.id));
|
||||
this.responseTabs[tab.id] = btn;
|
||||
tabs.appendChild(btn);
|
||||
}
|
||||
this.expectedEl = el("div", { class: "response-pane" });
|
||||
this.liveEl = el("div", { class: "response-pane" });
|
||||
this.liveEl.appendChild(el("p", { class: "response-placeholder", text: "Run the request to see the live response." }));
|
||||
wrap.appendChild(tabs);
|
||||
wrap.appendChild(this.expectedEl);
|
||||
wrap.appendChild(this.liveEl);
|
||||
this.selectTab("expected");
|
||||
return wrap;
|
||||
}
|
||||
|
||||
selectTab(id) {
|
||||
for (const [name, btn] of Object.entries(this.responseTabs)) {
|
||||
btn.classList.toggle("active", name === id);
|
||||
}
|
||||
this.expectedEl.classList.toggle("active", id === "expected");
|
||||
this.liveEl.classList.toggle("active", id === "live");
|
||||
}
|
||||
|
||||
renderExpected() {
|
||||
const sample = this.config.sample_response;
|
||||
if (sample === null || sample === undefined) {
|
||||
this.expectedEl.appendChild(el("p", { class: "response-note", text: "No documented response body." }));
|
||||
return;
|
||||
}
|
||||
this.expectedEl.appendChild(el("p", { class: "response-note", text: "Modeled JSON response:" }));
|
||||
const pre = el("pre", { class: "response-body" });
|
||||
pre.appendChild(el("code", { class: "language-json", text: JSON.stringify(sample, null, 2) }));
|
||||
this.expectedEl.appendChild(pre);
|
||||
CodeBlock.enhance(pre, { lineNumbers: false });
|
||||
}
|
||||
|
||||
values() {
|
||||
const out = { path: {}, query: {}, body: {} };
|
||||
for (const { param, control } of this.inputs) {
|
||||
const value = control.value;
|
||||
if (value === "" && !param.required) continue;
|
||||
out[param.location][param.name] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
buildUrl(values) {
|
||||
let path = this.config.path;
|
||||
for (const [name, value] of Object.entries(values.path)) {
|
||||
path = path.replace("{" + name + "}", encodeURIComponent(value));
|
||||
}
|
||||
const query = { ...values.query };
|
||||
const usesQuery = this.config.method === "GET" || this.config.method === "DELETE";
|
||||
if (usesQuery && this.requiresAuth()) Object.assign(query, devrantSession.triple());
|
||||
const entries = Object.entries(query);
|
||||
const qs = entries.length
|
||||
? "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
|
||||
: "";
|
||||
return this.base + path + qs;
|
||||
}
|
||||
|
||||
async confirmDestructive() {
|
||||
if (!(this.config.destructive || this.config.method === "DELETE")) return true;
|
||||
if (window.app && window.app.dialog) {
|
||||
return window.app.dialog.confirm({
|
||||
title: "Run request",
|
||||
message: "This performs a real action on your account. Continue?",
|
||||
confirmLabel: "Run",
|
||||
danger: true,
|
||||
});
|
||||
}
|
||||
return window.confirm("This performs a real action on your account. Continue?");
|
||||
}
|
||||
|
||||
async send() {
|
||||
if (this.requiresAuth() && !devrantSession.isLoggedIn()) {
|
||||
this.showError("Log in first using the widget at the top of the page.");
|
||||
return;
|
||||
}
|
||||
if (!(await this.confirmDestructive())) return;
|
||||
const values = this.values();
|
||||
const url = this.buildUrl(values);
|
||||
const headers = { Accept: "application/json" };
|
||||
const options = { method: this.config.method, headers };
|
||||
if (this.config.encoding === "form") {
|
||||
const body = { ...values.body };
|
||||
if (this.requiresAuth()) Object.assign(body, devrantSession.triple());
|
||||
options.body = new URLSearchParams(body).toString();
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
}
|
||||
this.sendBtn.disabled = true;
|
||||
const started = performance.now();
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const ms = Math.round(performance.now() - started);
|
||||
await this.renderResponse(response, ms, values);
|
||||
} catch (error) {
|
||||
this.showError(error.message || "Network error");
|
||||
} finally {
|
||||
this.sendBtn.disabled = false;
|
||||
if (this.requiresAuth()) this.syncAuth();
|
||||
}
|
||||
}
|
||||
|
||||
async renderResponse(response, ms, values) {
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
this.liveEl.innerHTML = "";
|
||||
this.selectTab("live");
|
||||
const cls = "response-" + Math.floor(response.status / 100) + "xx";
|
||||
const meta = el("div", { class: "response-meta" }, [
|
||||
el("span", { class: "response-status " + cls, text: `${response.status} ${response.statusText}` }),
|
||||
el("span", { text: `${ms} ms` }),
|
||||
el("span", { text: contentType.split(";")[0] || "unknown" }),
|
||||
]);
|
||||
this.liveEl.appendChild(meta);
|
||||
if (contentType.includes("application/json")) {
|
||||
const data = await response.json();
|
||||
this.maybeCaptureLogin(data, values);
|
||||
const pre = el("pre", { class: "response-body" });
|
||||
pre.appendChild(el("code", { class: "language-json", text: JSON.stringify(data, null, 2) }));
|
||||
this.liveEl.appendChild(pre);
|
||||
CodeBlock.enhance(pre, { lineNumbers: false });
|
||||
return;
|
||||
}
|
||||
const text = await response.text();
|
||||
const pre = el("pre", { class: "response-body" });
|
||||
pre.appendChild(el("code", { text: text.slice(0, 2000) }));
|
||||
this.liveEl.appendChild(pre);
|
||||
CodeBlock.enhance(pre, { highlight: false, lineNumbers: false });
|
||||
}
|
||||
|
||||
maybeCaptureLogin(data, values) {
|
||||
const loginIds = ["devrant-login", "devrant-register"];
|
||||
if (loginIds.includes(this.config.id) && data && data.success && data.auth_token) {
|
||||
devrantSession.setFromToken(data.auth_token, values.body.username || "");
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.liveEl.innerHTML = "";
|
||||
this.selectTab("live");
|
||||
this.liveEl.appendChild(el("div", { class: "response-meta" }, [
|
||||
el("span", { class: "response-status response-5xx", text: "Error" }),
|
||||
]));
|
||||
this.liveEl.appendChild(el("p", { class: "response-redirect", text: message }));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
import { TextInput } from "./TextInput.js";
|
||||
import { DomUtils } from "./DomUtils.js";
|
||||
import { assetUrl } from "./assetVersion.js";
|
||||
|
||||
const EMOJI_DATA_SOURCE = assetUrl("/static/vendor/emoji-picker-element/data.json");
|
||||
|
||||
export class EmojiPicker {
|
||||
constructor(textarea) {
|
||||
@@ -21,6 +24,7 @@ export class EmojiPicker {
|
||||
this.wrapper.style.zIndex = "2000";
|
||||
|
||||
const picker = document.createElement("emoji-picker");
|
||||
picker.setAttribute("data-source", EMOJI_DATA_SOURCE);
|
||||
picker.style.width = "320px";
|
||||
picker.style.height = "350px";
|
||||
this.wrapper.appendChild(picker);
|
||||
|
||||
@@ -57,6 +57,7 @@ export class FormManager {
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add("is-loading");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { Http } from "./Http.js";
|
||||
import { JobPoller } from "./JobPoller.js";
|
||||
|
||||
export class BugReporter {
|
||||
export class IssueReporter {
|
||||
constructor() {
|
||||
this.active = false;
|
||||
document.addEventListener("submit", (event) => {
|
||||
const form = event.target.closest("form[data-bug-create]");
|
||||
const form = event.target.closest("form[data-issue-create]");
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
this.submit(form);
|
||||
@@ -24,7 +24,10 @@ export class BugReporter {
|
||||
}
|
||||
this.active = true;
|
||||
const button = form.querySelector("button[type='submit']");
|
||||
if (button) button.disabled = true;
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.classList.add("is-loading");
|
||||
}
|
||||
window.app.toast.show("Filing your report and improving it with AI...", {
|
||||
type: "info",
|
||||
ms: 5000,
|
||||
@@ -36,15 +39,18 @@ export class BugReporter {
|
||||
window.app.toast.show("Could not submit the report", { type: "error" });
|
||||
} finally {
|
||||
this.active = false;
|
||||
if (button) button.disabled = false;
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.classList.remove("is-loading");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
poll(statusUrl) {
|
||||
return JobPoller.run(statusUrl, {
|
||||
onDone: (status) => {
|
||||
window.app.toast.show("Bug report filed", { type: "success" });
|
||||
window.location.href = status.bug_url || "/bugs";
|
||||
window.app.toast.show("Issue report filed", { type: "success" });
|
||||
window.location.href = status.issue_url || "/issues";
|
||||
},
|
||||
onFailed: (status) => {
|
||||
window.app.toast.show(
|
||||
@@ -8,13 +8,6 @@ export class MessagesLayout {
|
||||
}
|
||||
this.thread = document.querySelector(".messages-thread");
|
||||
this.input = document.querySelector('.messages-input-area input[name="content"]');
|
||||
this.stackBreakpoint = 768;
|
||||
this.minHeight = 320;
|
||||
this.resize = this.resize.bind(this);
|
||||
|
||||
this.resize();
|
||||
window.addEventListener("resize", this.resize);
|
||||
window.addEventListener("orientationchange", this.resize);
|
||||
|
||||
this.scrollThreadToEnd();
|
||||
if (this.input) {
|
||||
@@ -22,18 +15,6 @@ export class MessagesLayout {
|
||||
}
|
||||
}
|
||||
|
||||
resize() {
|
||||
if (window.innerWidth <= this.stackBreakpoint) {
|
||||
this.layout.style.height = "";
|
||||
return;
|
||||
}
|
||||
const top = this.layout.getBoundingClientRect().top;
|
||||
const page = this.layout.closest(".page");
|
||||
const bottomPadding = page ? parseFloat(getComputedStyle(page).paddingBottom) || 0 : 0;
|
||||
const available = window.innerHeight - top - bottomPadding;
|
||||
this.layout.style.height = `${Math.max(available, this.minHeight)}px`;
|
||||
}
|
||||
|
||||
scrollThreadToEnd() {
|
||||
if (this.thread) {
|
||||
this.thread.scrollTop = this.thread.scrollHeight;
|
||||
|
||||
@@ -15,4 +15,18 @@ export class OptimisticAction {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async submitOptimistic(url, params, errorTarget, apply, revert, reconcile) {
|
||||
apply();
|
||||
try {
|
||||
const result = await Http.sendForm(url, params);
|
||||
if (reconcile) reconcile(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("optimistic action failed", error);
|
||||
revert();
|
||||
if (errorTarget) Toast.flash(errorTarget, "Error", 1500);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,33 @@ export class VoteManager extends OptimisticAction {
|
||||
|
||||
cast(form, button) {
|
||||
const action = form.getAttribute("action");
|
||||
const value = form.querySelector('input[name="value"]').value;
|
||||
return this.submit(action, { value }, button, (result) => this.render(action, result));
|
||||
const clickedValue = parseInt(form.querySelector('input[name="value"]').value, 10);
|
||||
const targetUid = action.split("/").pop();
|
||||
const prevValue = this.currentValue(action);
|
||||
const prevNet = this.currentNet(targetUid);
|
||||
const newValue = prevValue === clickedValue ? 0 : clickedValue;
|
||||
const predictedNet = prevNet + (newValue - prevValue);
|
||||
return this.submitOptimistic(
|
||||
action,
|
||||
{ value: clickedValue },
|
||||
button,
|
||||
() => this.render(action, { net: predictedNet, value: newValue }),
|
||||
() => this.render(action, { net: prevNet, value: prevValue }),
|
||||
(result) => this.render(action, result),
|
||||
);
|
||||
}
|
||||
|
||||
currentValue(action) {
|
||||
const voted = document.querySelector(`form[action="${action}"] button.voted`);
|
||||
if (!voted) return 0;
|
||||
return parseInt(voted.closest("form").querySelector('input[name="value"]').value, 10);
|
||||
}
|
||||
|
||||
currentNet(targetUid) {
|
||||
const counter = document.querySelector(`[data-vote-count="${targetUid}"]`);
|
||||
if (!counter) return 0;
|
||||
const parsed = parseInt(counter.textContent, 10);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
render(action, result) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,7 +30,7 @@ const MODE_READWRITE = 'readwrite';
|
||||
const INDEX_SKIN_UNICODE = 'skinUnicodes';
|
||||
const FIELD_SKIN_UNICODE = 'skinUnicodes';
|
||||
|
||||
const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';
|
||||
const DEFAULT_DATA_SOURCE = '/static/vendor/emoji-picker-element/data.json';
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
// like lodash's uniqBy but much smaller
|
||||
|
||||
+1
-1
@@ -1646,7 +1646,7 @@ function createRoot (shadowRoot, props) {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';
|
||||
const DEFAULT_DATA_SOURCE = '/static/vendor/emoji-picker-element/data.json';
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
var enI18n = {
|
||||
|
||||
Reference in New Issue
Block a user