@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 categorySelect = container.querySelector("#sch-category");
107
- const categoryId = categorySelect?.value || null;
108
- if (!title || !body) {
109
- errorEl.innerHTML = `
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 both the subject and description.</s-text>
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>, statusFilter?: TicketStatus): Promise<void>;
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
- export async function renderTicketList(container, client, emitter, statusFilter) {
26
- container.innerHTML = `
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
- <div id="sch-ticket-list-body">
43
- <div style="display:flex; justify-content:center; padding:32px 0;">
44
- <s-spinner size="small"></s-spinner>
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
- try {
56
- const result = await client.listTickets(statusFilter ? { status: statusFilter } : {});
57
- const tickets = result.items;
58
- if (tickets.length === 0) {
59
- body.innerHTML = `
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
- <s-text variant="headingSm">No support tickets yet</s-text>
68
- <div style="margin-top:8px; margin-bottom:20px;">
69
- <s-text tone="subdued">Have a question or need help? Create a ticket and our team will get back to you.</s-text>
70
- </div>
71
- <s-button variant="primary" id="sch-empty-create-btn">Create Your First Ticket</s-button>
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, tickets
139
+ setInnerHtml(body, filtered
80
140
  .map((ticket) => `
81
- <div class="sch-ticket-row" data-ticket-id="${ticket.id}"
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;"
83
- onmouseover="this.style.background='var(--p-color-bg-surface-hover, #f6f6f7)'"
84
- onmouseout="this.style.background='transparent'">
85
- <div style="display:flex; align-items:center; gap:12px; min-width:0; flex:1;">
86
- <div style="width:8px; height:8px; border-radius:50%; flex-shrink:0; background:${getStatusDotColor(ticket.status)};"></div>
87
- <div style="min-width:0; flex:1;">
88
- <div style="display:flex; align-items:center; gap:8px;">
89
- <s-text variant="bodyMd" fontWeight="semibold">${escapeHtml(ticket.title)}</s-text>
90
- </div>
91
- <div style="margin-top:2px;">
92
- <s-text variant="bodySm" tone="subdued">#${ticket.id.slice(0, 8)} &middot; Updated ${timeAgo(ticket.updatedAt)}</s-text>
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)} &middot; 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
- <div style="display:flex; align-items:center; gap:12px; flex-shrink:0; margin-left:16px;">
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
- catch (error) {
115
- setInnerHtml(body, `
116
- <s-banner tone="critical">
117
- <s-text>Failed to load tickets: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
118
- </s-banner>
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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catandbox/schrodinger-web-adapter",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",