@catandbox/schrodinger-web-adapter 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM helpers that avoid direct innerHTML/outerHTML assignment.
|
|
3
|
+
* All dynamic content must still be escaped before passing to these functions.
|
|
4
|
+
*/
|
|
5
|
+
export declare function setInnerHtml(el: HTMLElement, html: string): void;
|
|
6
|
+
export declare function replaceOuterHtml(el: HTMLElement, html: string): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM helpers that avoid direct innerHTML/outerHTML assignment.
|
|
3
|
+
* All dynamic content must still be escaped before passing to these functions.
|
|
4
|
+
*/
|
|
5
|
+
export function setInnerHtml(el, html) {
|
|
6
|
+
el.replaceChildren();
|
|
7
|
+
if (html) {
|
|
8
|
+
const frag = document.createRange().createContextualFragment(html);
|
|
9
|
+
el.append(frag);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function replaceOuterHtml(el, html) {
|
|
13
|
+
const frag = document.createRange().createContextualFragment(html);
|
|
14
|
+
el.replaceWith(frag);
|
|
15
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setInnerHtml, replaceOuterHtml } from "./dom-utils";
|
|
1
2
|
const MAX_FILES = 5;
|
|
2
3
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
3
4
|
async function computeSha256(file) {
|
|
@@ -85,7 +86,7 @@ export function createFileUpload() {
|
|
|
85
86
|
const errorEl = container.querySelector("#sch-file-error");
|
|
86
87
|
const err = addFiles(input.files);
|
|
87
88
|
if (err) {
|
|
88
|
-
errorEl
|
|
89
|
+
setInnerHtml(errorEl, `<s-text variant="bodySm" tone="critical">${escapeHtml(err)}</s-text>`);
|
|
89
90
|
}
|
|
90
91
|
else {
|
|
91
92
|
errorEl.innerHTML = "";
|
|
@@ -108,7 +109,7 @@ export function createFileUpload() {
|
|
|
108
109
|
const wrapper = container.querySelector("#sch-file-list")?.parentElement;
|
|
109
110
|
if (!wrapper)
|
|
110
111
|
return;
|
|
111
|
-
wrapper
|
|
112
|
+
replaceOuterHtml(wrapper, render());
|
|
112
113
|
attachListeners(container);
|
|
113
114
|
}
|
|
114
115
|
function hasFiles() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileUpload } from "./file-upload";
|
|
2
|
+
import { setInnerHtml } from "./dom-utils";
|
|
2
3
|
const FIELD_STYLE = [
|
|
3
4
|
"width:100%",
|
|
4
5
|
"padding:10px 12px",
|
|
@@ -18,7 +19,7 @@ export function renderNewTicketForm(container, client, categories, emitter) {
|
|
|
18
19
|
.map((cat) => `<option value="${cat.id}">${escapeHtml(cat.name)}</option>`)
|
|
19
20
|
.join("");
|
|
20
21
|
const fileUpload = createFileUpload();
|
|
21
|
-
container
|
|
22
|
+
setInnerHtml(container, `
|
|
22
23
|
<s-card>
|
|
23
24
|
<s-box padding="large">
|
|
24
25
|
<s-stack gap="large">
|
|
@@ -82,7 +83,7 @@ export function renderNewTicketForm(container, client, categories, emitter) {
|
|
|
82
83
|
</s-stack>
|
|
83
84
|
</s-box>
|
|
84
85
|
</s-card>
|
|
85
|
-
|
|
86
|
+
`);
|
|
86
87
|
fileUpload.attachListeners(container);
|
|
87
88
|
const form = container.querySelector("#sch-new-ticket-form");
|
|
88
89
|
container.querySelector("#sch-cancel-btn")?.addEventListener("click", () => {
|
|
@@ -131,11 +132,11 @@ export function renderNewTicketForm(container, client, categories, emitter) {
|
|
|
131
132
|
emitter.emit("ticket:created", ticket.id);
|
|
132
133
|
}
|
|
133
134
|
catch (error) {
|
|
134
|
-
errorEl
|
|
135
|
+
setInnerHtml(errorEl, `
|
|
135
136
|
<s-banner tone="critical">
|
|
136
137
|
<s-text>Failed to create ticket: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
|
|
137
138
|
</s-banner>
|
|
138
|
-
|
|
139
|
+
`);
|
|
139
140
|
submitBtn.removeAttribute("disabled");
|
|
140
141
|
}
|
|
141
142
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderStatusBadge } from "./status-badge";
|
|
2
2
|
import { createFileUpload } from "./file-upload";
|
|
3
|
+
import { setInnerHtml } from "./dom-utils";
|
|
3
4
|
function formatTimestamp(timestamp) {
|
|
4
5
|
const date = new Date(timestamp * 1000);
|
|
5
6
|
const now = new Date();
|
|
@@ -37,7 +38,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
|
|
|
37
38
|
const data = await client.getTicket(ticketId);
|
|
38
39
|
const { ticket, messages } = data;
|
|
39
40
|
const isOpen = ticket.status !== "Closed" && ticket.status !== "Archived";
|
|
40
|
-
container
|
|
41
|
+
setInnerHtml(container, `
|
|
41
42
|
<s-card>
|
|
42
43
|
<s-box padding="large">
|
|
43
44
|
<s-stack gap="large">
|
|
@@ -103,7 +104,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
|
|
|
103
104
|
</s-box>
|
|
104
105
|
</s-card>`
|
|
105
106
|
: ""}
|
|
106
|
-
|
|
107
|
+
`);
|
|
107
108
|
// Back button
|
|
108
109
|
container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
|
|
109
110
|
emitter.emit("ticket:back", undefined);
|
|
@@ -113,7 +114,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
|
|
|
113
114
|
const replyFilesContainer = container.querySelector("#sch-reply-files");
|
|
114
115
|
if (replyFilesContainer) {
|
|
115
116
|
replyFileUpload = createFileUpload();
|
|
116
|
-
replyFilesContainer
|
|
117
|
+
setInnerHtml(replyFilesContainer, replyFileUpload.render());
|
|
117
118
|
replyFileUpload.attachListeners(replyFilesContainer);
|
|
118
119
|
}
|
|
119
120
|
// Send reply
|
|
@@ -168,7 +169,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
|
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
171
|
catch (error) {
|
|
171
|
-
container
|
|
172
|
+
setInnerHtml(container, `
|
|
172
173
|
<s-card>
|
|
173
174
|
<s-box padding="large">
|
|
174
175
|
<s-stack gap="base">
|
|
@@ -184,7 +185,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
|
|
|
184
185
|
</s-stack>
|
|
185
186
|
</s-box>
|
|
186
187
|
</s-card>
|
|
187
|
-
|
|
188
|
+
`);
|
|
188
189
|
container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
|
|
189
190
|
emitter.emit("ticket:back", undefined);
|
|
190
191
|
});
|
|
@@ -237,11 +238,11 @@ function showError(container, message) {
|
|
|
237
238
|
existing.remove();
|
|
238
239
|
const banner = document.createElement("div");
|
|
239
240
|
banner.id = "sch-error-banner";
|
|
240
|
-
banner
|
|
241
|
+
setInnerHtml(banner, `
|
|
241
242
|
<s-banner tone="critical" style="margin-bottom:12px;">
|
|
242
243
|
<s-text>${escapeHtml(message)}</s-text>
|
|
243
244
|
</s-banner>
|
|
244
|
-
|
|
245
|
+
`);
|
|
245
246
|
const replySection = container.querySelector("#sch-reply-section");
|
|
246
247
|
if (replySection) {
|
|
247
248
|
replySection.insertBefore(banner, replySection.firstChild);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderStatusBadge } from "./status-badge";
|
|
2
|
+
import { setInnerHtml } from "./dom-utils";
|
|
2
3
|
function formatDate(timestamp) {
|
|
3
4
|
return new Date(timestamp * 1000).toLocaleDateString(undefined, {
|
|
4
5
|
month: "short",
|
|
@@ -75,7 +76,7 @@ export async function renderTicketList(container, client, emitter, statusFilter)
|
|
|
75
76
|
?.addEventListener("click", () => emitter.emit("ticket:create", undefined));
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
|
-
body
|
|
79
|
+
setInnerHtml(body, tickets
|
|
79
80
|
.map((ticket) => `
|
|
80
81
|
<div class="sch-ticket-row" data-ticket-id="${ticket.id}"
|
|
81
82
|
style="display:flex; justify-content:space-between; align-items:center; padding:14px 16px; margin:0 -16px; border-radius:8px; cursor:pointer; transition:background 0.15s ease;"
|
|
@@ -100,7 +101,7 @@ export async function renderTicketList(container, client, emitter, statusFilter)
|
|
|
100
101
|
</div>
|
|
101
102
|
</div>
|
|
102
103
|
`)
|
|
103
|
-
.join(`<div style="border-bottom:1px solid var(--p-color-border-subdued, #e1e3e5); margin:0;"></div>`);
|
|
104
|
+
.join(`<div style="border-bottom:1px solid var(--p-color-border-subdued, #e1e3e5); margin:0;"></div>`));
|
|
104
105
|
container.querySelectorAll("[data-ticket-id]").forEach((el) => {
|
|
105
106
|
el.addEventListener("click", () => {
|
|
106
107
|
const ticketId = el.getAttribute("data-ticket-id");
|
|
@@ -111,11 +112,11 @@ export async function renderTicketList(container, client, emitter, statusFilter)
|
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
114
|
catch (error) {
|
|
114
|
-
body
|
|
115
|
+
setInnerHtml(body, `
|
|
115
116
|
<s-banner tone="critical">
|
|
116
117
|
<s-text>Failed to load tickets: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
|
|
117
118
|
</s-banner>
|
|
118
|
-
|
|
119
|
+
`);
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
function getStatusDotColor(status) {
|