@catandbox/schrodinger-web-adapter 0.1.24 → 0.1.26
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.
|
@@ -103,18 +103,30 @@ export function renderNewTicketForm(container, client, categories, emitter) {
|
|
|
103
103
|
errorEl.innerHTML = "";
|
|
104
104
|
const title = container.querySelector("#sch-title").value.trim();
|
|
105
105
|
const body = container.querySelector("#sch-body").value.trim();
|
|
106
|
-
const
|
|
107
|
-
const categoryId =
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const categoryEl = container.querySelector("#sch-category");
|
|
107
|
+
const categoryId = categoryEl?.value || null;
|
|
108
|
+
const categoryRequired = categories.length > 0;
|
|
109
|
+
const missing = [];
|
|
110
|
+
if (!title)
|
|
111
|
+
missing.push("Subject");
|
|
112
|
+
if (categoryRequired && !categoryId)
|
|
113
|
+
missing.push("Category");
|
|
114
|
+
if (!body)
|
|
115
|
+
missing.push("Description");
|
|
116
|
+
if (missing.length > 0) {
|
|
117
|
+
setInnerHtml(errorEl, `
|
|
110
118
|
<s-banner tone="warning">
|
|
111
|
-
<s-text>Please fill in
|
|
119
|
+
<s-text>Please fill in the following required fields: ${escapeHtml(missing.join(", "))}.</s-text>
|
|
112
120
|
</s-banner>
|
|
113
|
-
|
|
121
|
+
`);
|
|
114
122
|
return;
|
|
115
123
|
}
|
|
116
124
|
const submitBtn = container.querySelector("#sch-submit-btn");
|
|
125
|
+
const cancelBtn1 = container.querySelector("#sch-cancel-btn");
|
|
126
|
+
const cancelBtn2 = container.querySelector("#sch-cancel-btn-2");
|
|
117
127
|
submitBtn.setAttribute("disabled", "true");
|
|
128
|
+
cancelBtn1?.setAttribute("disabled", "true");
|
|
129
|
+
cancelBtn2?.setAttribute("disabled", "true");
|
|
118
130
|
try {
|
|
119
131
|
const clientMessageId = crypto.randomUUID();
|
|
120
132
|
const ticket = await client.createTicket({
|
|
@@ -143,6 +155,8 @@ export function renderNewTicketForm(container, client, categories, emitter) {
|
|
|
143
155
|
</s-banner>
|
|
144
156
|
`);
|
|
145
157
|
submitBtn.removeAttribute("disabled");
|
|
158
|
+
cancelBtn1?.removeAttribute("disabled");
|
|
159
|
+
cancelBtn2?.removeAttribute("disabled");
|
|
146
160
|
}
|
|
147
161
|
});
|
|
148
162
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { TicketStatus } from "@catandbox/schrodinger-contracts";
|
|
2
1
|
import type { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
3
2
|
import type { EventEmitter } from "./events";
|
|
4
3
|
export interface TicketListEvents {
|
|
@@ -6,4 +5,4 @@ export interface TicketListEvents {
|
|
|
6
5
|
"ticket:select": string;
|
|
7
6
|
"ticket:create": void;
|
|
8
7
|
}
|
|
9
|
-
export declare function renderTicketList(container: HTMLElement, client: SupportApiClient, emitter: EventEmitter<TicketListEvents
|
|
8
|
+
export declare function renderTicketList(container: HTMLElement, client: SupportApiClient, emitter: EventEmitter<TicketListEvents>): Promise<void>;
|
|
@@ -22,8 +22,37 @@ function timeAgo(timestamp) {
|
|
|
22
22
|
return `${days}d ago`;
|
|
23
23
|
return formatDate(timestamp);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const STATUS_OPTIONS = [
|
|
26
|
+
{ value: "", label: "All statuses" },
|
|
27
|
+
{ value: "Active", label: "Active" },
|
|
28
|
+
{ value: "InProgress", label: "In Progress" },
|
|
29
|
+
{ value: "AwaitingResponse", label: "Awaiting Response" },
|
|
30
|
+
{ value: "Closed", label: "Closed" },
|
|
31
|
+
{ value: "Archived", label: "Archived" }
|
|
32
|
+
];
|
|
33
|
+
const ORDER_OPTIONS = [
|
|
34
|
+
{ value: "newest", label: "Latest first" },
|
|
35
|
+
{ value: "oldest", label: "Oldest first" }
|
|
36
|
+
];
|
|
37
|
+
export async function renderTicketList(container, client, emitter) {
|
|
38
|
+
// Load tickets and categories in parallel
|
|
39
|
+
const [result, portalConfig] = await Promise.all([
|
|
40
|
+
client.listTickets({}).catch(() => ({ items: [] })),
|
|
41
|
+
client.getPortalConfig().catch(() => ({ categories: [], aliases: [] }))
|
|
42
|
+
]);
|
|
43
|
+
const allTickets = result.items;
|
|
44
|
+
const categories = portalConfig.categories;
|
|
45
|
+
// Filter/sort state
|
|
46
|
+
let categoryFilter = "";
|
|
47
|
+
let statusFilter = "";
|
|
48
|
+
let order = "newest";
|
|
49
|
+
const categoryOptions = [
|
|
50
|
+
`<s-option value="">All categories</s-option>`,
|
|
51
|
+
...categories.map((c) => `<s-option value="${c.id}">${escapeHtml(c.name)}</s-option>`)
|
|
52
|
+
].join("");
|
|
53
|
+
const statusOptions = STATUS_OPTIONS.map((o) => `<s-option value="${o.value}">${o.label}</s-option>`).join("");
|
|
54
|
+
const orderOptions = ORDER_OPTIONS.map((o) => `<s-option value="${o.value}">${o.label}</s-option>`).join("");
|
|
55
|
+
setInnerHtml(container, `
|
|
27
56
|
<s-card>
|
|
28
57
|
<s-box padding="large">
|
|
29
58
|
<s-stack gap="large">
|
|
@@ -39,24 +68,50 @@ export async function renderTicketList(container, client, emitter, statusFilter)
|
|
|
39
68
|
</span>
|
|
40
69
|
</s-button>
|
|
41
70
|
</div>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
|
|
72
|
+
<div style="display:flex; gap:12px; flex-wrap:wrap;">
|
|
73
|
+
${categories.length > 0 ? `
|
|
74
|
+
<div style="flex:1; min-width:140px;">
|
|
75
|
+
<s-select id="sch-filter-category" label="Category">
|
|
76
|
+
${categoryOptions}
|
|
77
|
+
</s-select>
|
|
78
|
+
</div>` : ""}
|
|
79
|
+
<div style="flex:1; min-width:140px;">
|
|
80
|
+
<s-select id="sch-filter-status" label="Status">
|
|
81
|
+
${statusOptions}
|
|
82
|
+
</s-select>
|
|
83
|
+
</div>
|
|
84
|
+
<div style="flex:1; min-width:140px;">
|
|
85
|
+
<s-select id="sch-filter-order" label="Order">
|
|
86
|
+
${orderOptions}
|
|
87
|
+
</s-select>
|
|
45
88
|
</div>
|
|
46
89
|
</div>
|
|
90
|
+
|
|
91
|
+
<div id="sch-ticket-list-body"></div>
|
|
47
92
|
</s-stack>
|
|
48
93
|
</s-box>
|
|
49
94
|
</s-card>
|
|
50
|
-
|
|
51
|
-
const body = container.querySelector("#sch-ticket-list-body");
|
|
95
|
+
`);
|
|
52
96
|
container
|
|
53
97
|
.querySelector("#sch-new-ticket-btn")
|
|
54
98
|
?.addEventListener("click", () => emitter.emit("ticket:create", undefined));
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
function getSelectValue(id) {
|
|
100
|
+
const el = container.querySelector(`#${id}`);
|
|
101
|
+
return el?.value ?? "";
|
|
102
|
+
}
|
|
103
|
+
function renderBody() {
|
|
104
|
+
const body = container.querySelector("#sch-ticket-list-body");
|
|
105
|
+
let filtered = allTickets.filter((t) => {
|
|
106
|
+
if (statusFilter && t.status !== statusFilter)
|
|
107
|
+
return false;
|
|
108
|
+
if (categoryFilter && t.categoryId !== categoryFilter)
|
|
109
|
+
return false;
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
filtered = filtered.slice().sort((a, b) => order === "oldest" ? a.createdAt - b.createdAt : b.createdAt - a.createdAt);
|
|
113
|
+
if (filtered.length === 0) {
|
|
114
|
+
setInnerHtml(body, `
|
|
60
115
|
<div style="text-align:center; padding:48px 24px;">
|
|
61
116
|
<div style="margin-bottom:16px;">
|
|
62
117
|
<svg width="48" height="48" viewBox="0 0 20 20" fill="none" style="color:var(--p-color-icon-subdued, #8c9196);">
|
|
@@ -64,60 +119,67 @@ export async function renderTicketList(container, client, emitter, statusFilter)
|
|
|
64
119
|
<path d="M10 5.5a.5.5 0 0 1 .5.5v3.793l2.354 2.353a.5.5 0 0 1-.708.708l-2.5-2.5A.5.5 0 0 1 9.5 10V6a.5.5 0 0 1 .5-.5z" fill="currentColor"/>
|
|
65
120
|
</svg>
|
|
66
121
|
</div>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
122
|
+
${allTickets.length === 0
|
|
123
|
+
? `<s-text variant="headingSm">No support tickets yet</s-text>
|
|
124
|
+
<div style="margin-top:8px; margin-bottom:20px;">
|
|
125
|
+
<s-text tone="subdued">Have a question or need help? Create a ticket and our team will get back to you.</s-text>
|
|
126
|
+
</div>
|
|
127
|
+
<s-button variant="primary" id="sch-empty-create-btn">Create Your First Ticket</s-button>`
|
|
128
|
+
: `<s-text variant="headingSm">No tickets match your filters</s-text>
|
|
129
|
+
<div style="margin-top:8px;">
|
|
130
|
+
<s-text tone="subdued">Try adjusting the filters above.</s-text>
|
|
131
|
+
</div>`}
|
|
72
132
|
</div>
|
|
73
|
-
|
|
133
|
+
`);
|
|
74
134
|
container
|
|
75
135
|
.querySelector("#sch-empty-create-btn")
|
|
76
136
|
?.addEventListener("click", () => emitter.emit("ticket:create", undefined));
|
|
77
137
|
return;
|
|
78
138
|
}
|
|
79
|
-
setInnerHtml(body,
|
|
139
|
+
setInnerHtml(body, filtered
|
|
80
140
|
.map((ticket) => `
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
141
|
+
<div class="sch-ticket-row" data-ticket-id="${ticket.id}"
|
|
142
|
+
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;"
|
|
143
|
+
onmouseover="this.style.background='var(--p-color-bg-surface-hover, #f6f6f7)'"
|
|
144
|
+
onmouseout="this.style.background='transparent'">
|
|
145
|
+
<div style="display:flex; align-items:center; gap:12px; min-width:0; flex:1;">
|
|
146
|
+
<div style="width:8px; height:8px; border-radius:50%; flex-shrink:0; background:${getStatusDotColor(ticket.status)};"></div>
|
|
147
|
+
<div style="min-width:0; flex:1;">
|
|
148
|
+
<div style="display:flex; align-items:center; gap:8px;">
|
|
149
|
+
<s-text variant="bodyMd" fontWeight="semibold">${escapeHtml(ticket.title)}</s-text>
|
|
150
|
+
</div>
|
|
151
|
+
<div style="margin-top:2px;">
|
|
152
|
+
<s-text variant="bodySm" tone="subdued">#${ticket.id.slice(0, 8)} · Updated ${timeAgo(ticket.updatedAt)}</s-text>
|
|
153
|
+
</div>
|
|
93
154
|
</div>
|
|
94
155
|
</div>
|
|
156
|
+
<div style="display:flex; align-items:center; gap:12px; flex-shrink:0; margin-left:16px;">
|
|
157
|
+
${renderStatusBadge(ticket.status)}
|
|
158
|
+
<svg width="16" height="16" viewBox="0 0 20 20" fill="var(--p-color-icon-subdued, #8c9196)">
|
|
159
|
+
<path d="M7.293 4.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414-1.414L11.586 10 7.293 5.707a1 1 0 0 1 0-1.414z"/>
|
|
160
|
+
</svg>
|
|
161
|
+
</div>
|
|
95
162
|
</div>
|
|
96
|
-
|
|
97
|
-
${renderStatusBadge(ticket.status)}
|
|
98
|
-
<svg width="16" height="16" viewBox="0 0 20 20" fill="var(--p-color-icon-subdued, #8c9196)">
|
|
99
|
-
<path d="M7.293 4.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414-1.414L11.586 10 7.293 5.707a1 1 0 0 1 0-1.414z"/>
|
|
100
|
-
</svg>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
`)
|
|
163
|
+
`)
|
|
104
164
|
.join(`<div style="border-bottom:1px solid var(--p-color-border-subdued, #e1e3e5); margin:0;"></div>`));
|
|
105
165
|
container.querySelectorAll("[data-ticket-id]").forEach((el) => {
|
|
106
166
|
el.addEventListener("click", () => {
|
|
107
167
|
const ticketId = el.getAttribute("data-ticket-id");
|
|
108
|
-
if (ticketId)
|
|
168
|
+
if (ticketId)
|
|
109
169
|
emitter.emit("ticket:select", ticketId);
|
|
110
|
-
}
|
|
111
170
|
});
|
|
112
171
|
});
|
|
113
172
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`);
|
|
173
|
+
function onFilterChange() {
|
|
174
|
+
categoryFilter = getSelectValue("sch-filter-category");
|
|
175
|
+
statusFilter = getSelectValue("sch-filter-status");
|
|
176
|
+
order = getSelectValue("sch-filter-order") || "newest";
|
|
177
|
+
renderBody();
|
|
120
178
|
}
|
|
179
|
+
container.querySelector("#sch-filter-category")?.addEventListener("change", onFilterChange);
|
|
180
|
+
container.querySelector("#sch-filter-status")?.addEventListener("change", onFilterChange);
|
|
181
|
+
container.querySelector("#sch-filter-order")?.addEventListener("change", onFilterChange);
|
|
182
|
+
renderBody();
|
|
121
183
|
}
|
|
122
184
|
function getStatusDotColor(status) {
|
|
123
185
|
const colors = {
|