@lightworkai.official/debug-capture-angular 0.6.0 → 0.7.0

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.
package/dist/index.mjs CHANGED
@@ -14,1512 +14,8 @@ var __legacyMetadataTS = (k, v) => {
14
14
  };
15
15
 
16
16
  // src/index.ts
17
- import {
18
- APP_INITIALIZER,
19
- Component as Component8,
20
- Input as Input8,
21
- makeEnvironmentProviders
22
- } from "@angular/core";
23
- import {
24
- configureDebugCapture,
25
- installDebugCapture,
26
- launch,
27
- setErroredQueriesSource
28
- } from "@lightworkai.official/debug-capture";
29
-
30
- // src/components/my-tickets-panel.component.ts
31
- import { Component as Component7, EventEmitter as EventEmitter5, Input as Input7, Output as Output5 } from "@angular/core";
32
- import { FormsModule } from "@angular/forms";
33
- import { NgFor as NgFor5, NgIf as NgIf6 } from "@angular/common";
34
- import {
35
- NoIdentityError,
36
- fetchRealmConfig,
37
- getAttachmentUrl,
38
- getConfig,
39
- getMyTicket,
40
- getMyTicketHistory,
41
- listMyTickets,
42
- reopenMyTicket,
43
- replyToTicket,
44
- statusMaps,
45
- ticketStrings,
46
- uploadInlineImage
47
- } from "@lightworkai.official/debug-capture";
48
-
49
- // src/components/icon.component.ts
50
- import { Component, Input } from "@angular/core";
51
- import { DomSanitizer } from "@angular/platform-browser";
52
- class IconComponent {
53
- sanitizer;
54
- set svg(value) {
55
- this.safe = this.sanitizer.bypassSecurityTrustHtml(value);
56
- }
57
- safe = "";
58
- constructor(sanitizer) {
59
- this.sanitizer = sanitizer;
60
- }
61
- }
62
- __legacyDecorateClassTS([
63
- Input({ required: true }),
64
- __legacyMetadataTS("design:paramtypes", [
65
- String
66
- ]),
67
- __legacyMetadataTS("design:type", String)
68
- ], IconComponent.prototype, "svg", null);
69
- IconComponent = __legacyDecorateClassTS([
70
- Component({
71
- selector: "lw-icon",
72
- standalone: true,
73
- template: `<span [innerHTML]="safe"></span>`,
74
- styles: [":host { display: inline-flex; align-items: center; }"]
75
- }),
76
- __legacyMetadataTS("design:paramtypes", [
77
- typeof DomSanitizer === "undefined" ? Object : DomSanitizer
78
- ])
79
- ], IconComponent);
80
-
81
- // src/components/ticket-table.component.ts
82
- import { Component as Component2, EventEmitter, Input as Input2, Output } from "@angular/core";
83
- import { NgFor, NgIf } from "@angular/common";
84
- import {
85
- compareBy,
86
- formatDateTime,
87
- hasSolution,
88
- hasTeamReply,
89
- matchesKeyword,
90
- pageCount,
91
- pageOf,
92
- statusTone,
93
- ticketKey
94
- } from "@lightworkai.official/debug-capture";
95
-
96
- // src/components/icons.ts
97
- function glyph(paths, size = 15) {
98
- const body = paths.map((d) => `<path d="${d}"/>`).join("");
99
- return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
100
- }
101
- var ICONS = {
102
- bold: glyph(["M6 4h8a4 4 0 0 1 0 8H6z", "M6 12h9a4 4 0 0 1 0 8H6z"]),
103
- italic: glyph(["M19 4h-9", "M14 20H5", "M15 4L9 20"]),
104
- underline: glyph(["M6 4v6a6 6 0 0 0 12 0V4", "M4 20h16"]),
105
- bulletList: glyph(["M8 6h13", "M8 12h13", "M8 18h13", "M3 6h.01", "M3 12h.01", "M3 18h.01"]),
106
- orderedList: glyph(["M10 6h11", "M10 12h11", "M10 18h11", "M4 6h1v4", "M4 10h2", "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"]),
107
- link: glyph([
108
- "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
109
- "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
110
- ]),
111
- image: glyph([
112
- "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
113
- "M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z",
114
- "M21 15l-5-5L5 21"
115
- ]),
116
- send: glyph(["M22 2L11 13", "M22 2l-7 20-4-9-9-4 20-7z"]),
117
- search: glyph(["M21 21l-4.34-4.34", "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"], 16),
118
- arrowLeft: glyph(["M19 12H5", "M12 19l-7-7 7-7"], 14),
119
- check: glyph(["M20 6L9 17l-5-5"], 12),
120
- message: glyph(["M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"], 12),
121
- rotate: glyph(["M3 12a9 9 0 1 0 3-6.7L3 8", "M3 3v5h5"], 14),
122
- alert: glyph(["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z", "M12 8v4", "M12 16h.01"], 16),
123
- inbox: glyph([
124
- "M22 12h-6l-2 3h-4l-2-3H2",
125
- "M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
126
- ], 22),
127
- clock: glyph(["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z", "M12 6v6l4 2"], 22),
128
- checkCircle: glyph(["M22 11.08V12a10 10 0 1 1-5.93-9.14", "M22 4L12 14.01l-3-3"], 22),
129
- ban: glyph(["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z", "M4.93 4.93l14.14 14.14"], 22),
130
- wrench: glyph([
131
- "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
132
- ], 16),
133
- square: glyph(["M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z"], 14),
134
- arrowUpRight: glyph(["M7 17L17 7", "M7 7h10v10"], 14),
135
- highlighter: glyph([
136
- "M9 11l-6 6v3h9l3-3",
137
- "M22 12l-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"
138
- ], 14),
139
- type: glyph(["M4 7V4h16v3", "M9 20h6", "M12 4v16"], 14),
140
- trash: glyph([
141
- "M3 6h18",
142
- "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
143
- ], 14),
144
- pencil: glyph(["M12 20h9", "M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"], 11),
145
- checkSmall: glyph(["M20 6L9 17l-5-5"], 12),
146
- xSmall: glyph(["M18 6L6 18", "M6 6l12 12"], 12)
147
- };
148
-
149
- // src/components/ticket-table.component.ts
150
- class TicketTableComponent {
151
- constructor() {
152
- this.items = [];
153
- this.timezone = "Asia/Bangkok";
154
- this.keyword = "";
155
- this.statusFilter = "";
156
- this.page = 1;
157
- this.opened = new EventEmitter;
158
- this.sorted = new EventEmitter;
159
- this.paged = new EventEmitter;
160
- this.counted = new EventEmitter;
161
- }
162
- icons = ICONS;
163
- columns = [
164
- { key: "number", label: "colNumber", width: "120px" },
165
- { key: "title", label: "colTitle" },
166
- { key: "module", label: "colModule", width: "160px" },
167
- { key: "status", label: "colStatus", width: "130px" },
168
- { key: "response", label: "colResponse", width: "150px" },
169
- { key: "createdAt", label: "colCreated", width: "170px" },
170
- { key: "updatedAt", label: "colUpdated", width: "170px" }
171
- ];
172
- get visible() {
173
- const rows = this.items.filter((item) => this.statusFilter ? item.status === this.statusFilter : true).filter((item) => matchesKeyword(item, this.keyword, this.slug, this.maps.labelOf)).sort(compareBy(this.sort, this.maps, this.slug));
174
- this.counted.emit(rows.length);
175
- return rows;
176
- }
177
- get pages() {
178
- return pageCount(this.visible.length);
179
- }
180
- get current() {
181
- return Math.min(this.page, this.pages);
182
- }
183
- get rows() {
184
- return pageOf(this.visible, this.current);
185
- }
186
- label(key) {
187
- const column = this.columns.find((c) => c.key === key);
188
- return this.t[column.label];
189
- }
190
- arrow(key) {
191
- if (this.sort.key !== key)
192
- return "⇅";
193
- return this.sort.direction === "asc" ? "▲" : "▼";
194
- }
195
- ariaSort(key) {
196
- if (this.sort.key !== key)
197
- return null;
198
- return this.sort.direction === "asc" ? "ascending" : "descending";
199
- }
200
- key(row) {
201
- return ticketKey(this.slug, row.number);
202
- }
203
- tone(status) {
204
- return statusTone(this.maps.columnOf(status)?.color);
205
- }
206
- solved(row) {
207
- return hasSolution(row);
208
- }
209
- replied(row) {
210
- return hasTeamReply(row);
211
- }
212
- when(iso) {
213
- return formatDateTime(iso, this.timezone, this.locale);
214
- }
215
- }
216
- __legacyDecorateClassTS([
217
- Input2({ required: true }),
218
- __legacyMetadataTS("design:type", Array)
219
- ], TicketTableComponent.prototype, "items", undefined);
220
- __legacyDecorateClassTS([
221
- Input2({ required: true }),
222
- __legacyMetadataTS("design:type", typeof StatusMaps === "undefined" ? Object : StatusMaps)
223
- ], TicketTableComponent.prototype, "maps", undefined);
224
- __legacyDecorateClassTS([
225
- Input2({ required: true }),
226
- __legacyMetadataTS("design:type", typeof TicketStrings === "undefined" ? Object : TicketStrings)
227
- ], TicketTableComponent.prototype, "t", undefined);
228
- __legacyDecorateClassTS([
229
- Input2(),
230
- __legacyMetadataTS("design:type", String)
231
- ], TicketTableComponent.prototype, "slug", undefined);
232
- __legacyDecorateClassTS([
233
- Input2(),
234
- __legacyMetadataTS("design:type", Object)
235
- ], TicketTableComponent.prototype, "timezone", undefined);
236
- __legacyDecorateClassTS([
237
- Input2(),
238
- __legacyMetadataTS("design:type", String)
239
- ], TicketTableComponent.prototype, "locale", undefined);
240
- __legacyDecorateClassTS([
241
- Input2(),
242
- __legacyMetadataTS("design:type", Object)
243
- ], TicketTableComponent.prototype, "keyword", undefined);
244
- __legacyDecorateClassTS([
245
- Input2(),
246
- __legacyMetadataTS("design:type", Object)
247
- ], TicketTableComponent.prototype, "statusFilter", undefined);
248
- __legacyDecorateClassTS([
249
- Input2({ required: true }),
250
- __legacyMetadataTS("design:type", typeof Sort === "undefined" ? Object : Sort)
251
- ], TicketTableComponent.prototype, "sort", undefined);
252
- __legacyDecorateClassTS([
253
- Input2(),
254
- __legacyMetadataTS("design:type", Object)
255
- ], TicketTableComponent.prototype, "page", undefined);
256
- __legacyDecorateClassTS([
257
- Output(),
258
- __legacyMetadataTS("design:type", Object)
259
- ], TicketTableComponent.prototype, "opened", undefined);
260
- __legacyDecorateClassTS([
261
- Output(),
262
- __legacyMetadataTS("design:type", Object)
263
- ], TicketTableComponent.prototype, "sorted", undefined);
264
- __legacyDecorateClassTS([
265
- Output(),
266
- __legacyMetadataTS("design:type", Object)
267
- ], TicketTableComponent.prototype, "paged", undefined);
268
- __legacyDecorateClassTS([
269
- Output(),
270
- __legacyMetadataTS("design:type", Object)
271
- ], TicketTableComponent.prototype, "counted", undefined);
272
- TicketTableComponent = __legacyDecorateClassTS([
273
- Component2({
274
- selector: "lw-ticket-table",
275
- standalone: true,
276
- imports: [NgFor, NgIf, IconComponent],
277
- template: `
278
- <div class="lw-tablewrap">
279
- <table class="lw-table">
280
- <thead>
281
- <tr>
282
- <th
283
- *ngFor="let column of columns"
284
- scope="col"
285
- class="lw-th"
286
- [style.width]="column.width"
287
- [attr.aria-sort]="ariaSort(column.key)"
288
- (click)="sorted.emit(column.key)"
289
- >
290
- {{ label(column.key) }}
291
- <span class="lw-arrow">{{ arrow(column.key) }}</span>
292
- </th>
293
- </tr>
294
- </thead>
295
- <tbody>
296
- <tr *ngIf="!rows.length">
297
- <td [attr.colspan]="columns.length" class="lw-empty">
298
- {{ items.length === 0 ? t.empty : t.emptyFiltered }}
299
- </td>
300
- </tr>
301
- <tr
302
- *ngFor="let row of rows"
303
- class="lw-row"
304
- tabindex="0"
305
- (click)="opened.emit(row.id)"
306
- (keydown.enter)="opened.emit(row.id)"
307
- (keydown.space)="opened.emit(row.id)"
308
- >
309
- <td class="lw-num">{{ key(row) }}</td>
310
- <td class="lw-title">{{ row.title }}</td>
311
- <td>
312
- <span *ngIf="row.category; else noModule" class="lw-chip">{{ row.category }}</span>
313
- <ng-template #noModule><span class="lw-dash">—</span></ng-template>
314
- </td>
315
- <td>
316
- <!-- "Waiting on you" outranks the neutral status: it is the honest
317
- state and the only one the reporter can act on. -->
318
- <span *ngIf="maps.isAwaitingReply(row.status); else plainStatus" class="lw-pill lw-pill--awaiting">
319
- <lw-icon [svg]="icons.message" /> {{ t.awaitingReply }}
320
- </span>
321
- <ng-template #plainStatus>
322
- <span class="lw-status" [style.background]="tone(row.status).bg" [style.color]="tone(row.status).fg">
323
- {{ maps.labelOf(row.status) }}
324
- </span>
325
- </ng-template>
326
- </td>
327
- <td>
328
- <span *ngIf="solved(row)" class="lw-pill lw-pill--solution">
329
- <lw-icon [svg]="icons.check" /> {{ t.hasSolution }}
330
- </span>
331
- <span *ngIf="!solved(row) && replied(row)" class="lw-pill lw-pill--replied">
332
- <lw-icon [svg]="icons.message" /> {{ t.hasReply }}
333
- </span>
334
- <span *ngIf="!solved(row) && !replied(row)" class="lw-muted">{{ t.noReply }}</span>
335
- </td>
336
- <td class="lw-dim">{{ when(row.createdAt) }}</td>
337
- <td class="lw-dim">{{ when(row.updatedAt) }}</td>
338
- </tr>
339
- </tbody>
340
- </table>
341
- </div>
342
-
343
- <div *ngIf="pages > 1" class="lw-pager">
344
- <span>{{ t.of(current, pages) }}</span>
345
- <button type="button" class="lw-btn" [disabled]="current <= 1" (click)="paged.emit(current - 1)">
346
- {{ t.prev }}
347
- </button>
348
- <button type="button" class="lw-btn" [disabled]="current >= pages" (click)="paged.emit(current + 1)">
349
- {{ t.next }}
350
- </button>
351
- </div>
352
- `
353
- })
354
- ], TicketTableComponent);
355
-
356
- // src/components/ticket-detail.component.ts
357
- import {
358
- Component as Component6,
359
- ElementRef as ElementRef4,
360
- EventEmitter as EventEmitter4,
361
- Input as Input6,
362
- Output as Output4,
363
- ViewChild as ViewChild4
364
- } from "@angular/core";
365
- import { NgClass, NgFor as NgFor4, NgIf as NgIf5 } from "@angular/common";
366
- import {
367
- editMyMessage,
368
- formatDateTime as formatDateTime2,
369
- ticketKey as ticketKey2
370
- } from "@lightworkai.official/debug-capture";
371
-
372
- // src/components/ticket-body.component.ts
373
- import { Component as Component3, ElementRef, Input as Input3, ViewChild } from "@angular/core";
374
- import { NgIf as NgIf2 } from "@angular/common";
375
- import { renderBody } from "@lightworkai.official/debug-capture";
376
- class TicketBodyComponent {
377
- constructor() {
378
- this.html = "";
379
- this.host = "";
380
- }
381
- zoomed = null;
382
- onClick(event) {
383
- const node = event.target;
384
- if (!node || node.tagName !== "IMG")
385
- return;
386
- const src = node.getAttribute("src");
387
- if (src)
388
- this.zoomed = src;
389
- }
390
- ngOnChanges() {
391
- if (this.target)
392
- renderBody(this.target.nativeElement, this.html, this.host);
393
- }
394
- }
395
- __legacyDecorateClassTS([
396
- Input3({ required: true }),
397
- __legacyMetadataTS("design:type", Object)
398
- ], TicketBodyComponent.prototype, "html", undefined);
399
- __legacyDecorateClassTS([
400
- Input3({ required: true }),
401
- __legacyMetadataTS("design:type", Object)
402
- ], TicketBodyComponent.prototype, "host", undefined);
403
- __legacyDecorateClassTS([
404
- ViewChild("target", { static: true }),
405
- __legacyMetadataTS("design:type", typeof ElementRef === "undefined" ? Object : ElementRef)
406
- ], TicketBodyComponent.prototype, "target", undefined);
407
- TicketBodyComponent = __legacyDecorateClassTS([
408
- Component3({
409
- selector: "lw-ticket-body",
410
- standalone: true,
411
- imports: [NgIf2],
412
- template: `
413
- <!-- Delegated rather than bound per image: this is sanitised HTML we inject,
414
- not elements we built, so there is nothing to attach to. -->
415
- <div #target class="lw-body" (click)="onClick($event)"></div>
416
-
417
- <div *ngIf="zoomed" class="lw-zoom" role="dialog" aria-modal="true" (click)="zoomed = null">
418
- <button type="button" class="lw-zoom__close" aria-label="close">✕</button>
419
- <img [src]="zoomed" alt="" />
420
- </div>
421
- `
422
- })
423
- ], TicketBodyComponent);
424
-
425
- // src/components/rich-reply-editor.component.ts
426
- import {
427
- ChangeDetectorRef,
428
- Component as Component5,
429
- ElementRef as ElementRef3,
430
- EventEmitter as EventEmitter3,
431
- Input as Input5,
432
- Output as Output3,
433
- ViewChild as ViewChild3
434
- } from "@angular/core";
435
- import { NgFor as NgFor3, NgIf as NgIf4 } from "@angular/common";
436
- import { Editor } from "@tiptap/core";
437
- import StarterKit from "@tiptap/starter-kit";
438
- import Image2 from "@tiptap/extension-image";
439
- import Placeholder from "@tiptap/extension-placeholder";
440
- import { cleanHtml, htmlIsEmpty } from "@lightworkai.official/debug-capture";
441
-
442
- // src/components/image-annotator-dialog.component.ts
443
- import {
444
- Component as Component4,
445
- ElementRef as ElementRef2,
446
- EventEmitter as EventEmitter2,
447
- Input as Input4,
448
- Output as Output2,
449
- ViewChild as ViewChild2
450
- } from "@angular/core";
451
- import { NgFor as NgFor2, NgIf as NgIf3 } from "@angular/common";
452
- import {
453
- createAnnotator,
454
- reporterStrings
455
- } from "@lightworkai.official/debug-capture";
456
- class ImageAnnotatorDialogComponent {
457
- constructor() {
458
- this.src = "";
459
- this.confirmed = new EventEmitter2;
460
- this.cancelled = new EventEmitter2;
461
- }
462
- annotator = null;
463
- active = null;
464
- loading = true;
465
- icons = ICONS;
466
- tools = [
467
- { tool: "rect", key: "toolRect", icon: ICONS.square },
468
- { tool: "arrow", key: "toolArrow", icon: ICONS.arrowUpRight },
469
- { tool: "highlight", key: "toolHighlight", icon: ICONS.highlighter },
470
- { tool: "text", key: "toolText", icon: ICONS.type }
471
- ];
472
- get t() {
473
- return reporterStrings(this.locale);
474
- }
475
- label(key) {
476
- return this.t[key];
477
- }
478
- ngAfterViewInit() {
479
- const image = new Image;
480
- image.addEventListener("load", () => {
481
- const node = this.canvas.nativeElement;
482
- node.width = image.naturalWidth;
483
- node.height = image.naturalHeight;
484
- this.annotator = createAnnotator(node, image, () => {
485
- this.active = null;
486
- });
487
- this.loading = false;
488
- });
489
- image.src = this.src;
490
- }
491
- ngOnDestroy() {
492
- this.annotator?.destroy();
493
- this.annotator = null;
494
- }
495
- pick(tool) {
496
- this.active = this.active === tool ? null : tool;
497
- this.annotator?.setTool(this.active);
498
- }
499
- clear() {
500
- this.annotator?.clear();
501
- this.active = null;
502
- }
503
- backdrop(event) {
504
- if (event.target === event.currentTarget)
505
- this.cancelled.emit();
506
- }
507
- confirm() {
508
- this.confirmed.emit(this.annotator?.toDataUrl() ?? this.src);
509
- }
510
- }
511
- __legacyDecorateClassTS([
512
- Input4({ required: true }),
513
- __legacyMetadataTS("design:type", Object)
514
- ], ImageAnnotatorDialogComponent.prototype, "src", undefined);
515
- __legacyDecorateClassTS([
516
- Input4(),
517
- __legacyMetadataTS("design:type", String)
518
- ], ImageAnnotatorDialogComponent.prototype, "filename", undefined);
519
- __legacyDecorateClassTS([
520
- Input4(),
521
- __legacyMetadataTS("design:type", String)
522
- ], ImageAnnotatorDialogComponent.prototype, "locale", undefined);
523
- __legacyDecorateClassTS([
524
- Output2(),
525
- __legacyMetadataTS("design:type", Object)
526
- ], ImageAnnotatorDialogComponent.prototype, "confirmed", undefined);
527
- __legacyDecorateClassTS([
528
- Output2(),
529
- __legacyMetadataTS("design:type", Object)
530
- ], ImageAnnotatorDialogComponent.prototype, "cancelled", undefined);
531
- __legacyDecorateClassTS([
532
- ViewChild2("canvas", { static: true }),
533
- __legacyMetadataTS("design:type", typeof ElementRef2 === "undefined" ? Object : ElementRef2)
534
- ], ImageAnnotatorDialogComponent.prototype, "canvas", undefined);
535
- ImageAnnotatorDialogComponent = __legacyDecorateClassTS([
536
- Component4({
537
- selector: "lw-image-annotator-dialog",
538
- standalone: true,
539
- imports: [NgFor2, NgIf3, IconComponent],
540
- template: `
541
- <!-- Header, toolbar, canvas, help, footer — the same shape and the same
542
- words as the app's own dialog, so the two are one thing in two places
543
- rather than two things that resemble each other. -->
544
- <div class="lw-annotator" role="dialog" aria-modal="true" (click)="backdrop($event)">
545
- <div class="lw-annotator__panel">
546
- <div class="lw-annotator__head">
547
- <div class="lw-grow">
548
- <h2 class="lw-annotator__title">{{ t.annotateTitle }}</h2>
549
- <p class="lw-annotator__desc">
550
- {{ t.annotateHint }}<span *ngIf="filename"> · {{ filename }}</span>
551
- </p>
552
- </div>
553
- <button type="button" class="lw-annotator__close" [attr.aria-label]="t.cancel" (click)="cancelled.emit()">
554
-
555
- </button>
556
- </div>
557
-
558
- <div class="lw-annotator__tools">
559
- <button
560
- *ngFor="let tool of tools"
561
- type="button"
562
- class="lw-atool"
563
- [attr.aria-pressed]="active === tool.tool"
564
- [disabled]="loading"
565
- (click)="pick(tool.tool)"
566
- >
567
- <lw-icon [svg]="tool.icon" /> {{ label(tool.key) }}
568
- </button>
569
- <span class="lw-grow"></span>
570
- <!-- Right-aligned and on its own, like the app's. Deleting ONE shape
571
- is the ✕ on the shape, which the help line below explains. -->
572
- <button type="button" class="lw-atool lw-atool--plain" [disabled]="loading" (click)="clear()">
573
- <lw-icon [svg]="icons.trash" /> {{ t.clearAll }}
574
- </button>
575
- </div>
576
-
577
- <div class="lw-annotator__canvas">
578
- <div class="lw-annotator__canvasbox"><canvas #canvas></canvas></div>
579
- </div>
580
-
581
- <p class="lw-annotator__hint">{{ t.annotateHelp }}</p>
582
-
583
- <div class="lw-annotator__actions">
584
- <span class="lw-grow"></span>
585
- <button type="button" class="lw-btn" (click)="cancelled.emit()">{{ t.cancel }}</button>
586
- <button type="button" class="lw-send" [disabled]="loading" (click)="confirm()">
587
- {{ t.annotateApply }}
588
- </button>
589
- </div>
590
- </div>
591
- </div>
592
- `
593
- })
594
- ], ImageAnnotatorDialogComponent);
595
-
596
- // src/components/rich-reply-editor.component.ts
597
- class RichReplyEditorComponent {
598
- cdr;
599
- set disabledState(value) {
600
- this.disabled = value;
601
- this.editor?.setEditable(!value);
602
- }
603
- pending = null;
604
- pendingName = "screenshot.png";
605
- disabled = false;
606
- editor = null;
607
- tools = [
608
- { key: "bold", icon: ICONS.bold, mark: "bold", run: () => this.editor?.chain().focus().toggleBold().run() },
609
- { key: "italic", icon: ICONS.italic, mark: "italic", run: () => this.editor?.chain().focus().toggleItalic().run() },
610
- { key: "underline", icon: ICONS.underline, mark: "underline", run: () => this.editor?.chain().focus().toggleUnderline().run() },
611
- { key: "bulletList", icon: ICONS.bulletList, mark: "bulletList", separatorBefore: true, run: () => this.editor?.chain().focus().toggleBulletList().run() },
612
- { key: "orderedList", icon: ICONS.orderedList, mark: "orderedList", run: () => this.editor?.chain().focus().toggleOrderedList().run() },
613
- { key: "link", icon: ICONS.link, mark: "link", run: () => this.toggleLink() },
614
- { key: "attachImage", icon: ICONS.image, mark: "", separatorBefore: true, run: () => this.file.nativeElement.click() }
615
- ];
616
- constructor(cdr) {
617
- this.placeholder = "";
618
- this.host = "";
619
- this.errored = new EventEmitter3;
620
- this.cdr = cdr;
621
- }
622
- ngAfterViewInit() {
623
- this.editor = new Editor({
624
- element: this.mount.nativeElement,
625
- content: this.initial ?? "",
626
- extensions: [
627
- StarterKit,
628
- Image2.configure({ inline: false, allowBase64: false }),
629
- Placeholder.configure({ placeholder: this.placeholder })
630
- ],
631
- editorProps: {
632
- attributes: { class: "lw-editor-body" },
633
- handlePaste: (_view, event) => {
634
- const image = Array.from(event.clipboardData?.files ?? []).find((f) => f.type.startsWith("image/"));
635
- if (!image)
636
- return false;
637
- event.preventDefault();
638
- this.addImage(image);
639
- return true;
640
- },
641
- handleDrop: (_view, event) => {
642
- const dropped = event;
643
- const image = Array.from(dropped.dataTransfer?.files ?? []).find((f) => f.type.startsWith("image/"));
644
- if (!image)
645
- return false;
646
- dropped.preventDefault();
647
- this.addImage(image);
648
- return true;
649
- }
650
- },
651
- onSelectionUpdate: () => this.cdr.markForCheck(),
652
- onUpdate: () => this.cdr.markForCheck()
653
- });
654
- this.editor.setEditable(!this.disabled);
655
- }
656
- ngOnDestroy() {
657
- this.editor?.destroy();
658
- this.editor = null;
659
- }
660
- press(event, tool) {
661
- event.preventDefault();
662
- if (this.disabled)
663
- return;
664
- tool.run();
665
- }
666
- isActive(mark) {
667
- return Boolean(mark) && (this.editor?.isActive(mark) ?? false);
668
- }
669
- onFile(event) {
670
- const input = event.target;
671
- const chosen = input.files?.[0];
672
- input.value = "";
673
- if (chosen)
674
- this.addImage(chosen);
675
- }
676
- async addImage(file) {
677
- if (this.pending)
678
- return;
679
- try {
680
- this.pendingName = file.name || "screenshot.png";
681
- this.pending = await readDataUri(file);
682
- this.cdr.markForCheck();
683
- } catch (e) {
684
- this.errored.emit(e instanceof Error ? e.message : "unreadable file");
685
- }
686
- }
687
- async commitImage(dataUri) {
688
- this.pending = null;
689
- try {
690
- const src = await this.uploadImage(dataUriToFile(dataUri, this.pendingName));
691
- this.editor?.chain().focus().setImage({ src }).run();
692
- } catch (e) {
693
- this.errored.emit(e instanceof Error ? e.message : "upload failed");
694
- } finally {
695
- this.cdr.markForCheck();
696
- }
697
- }
698
- toggleLink() {
699
- if (!this.editor)
700
- return;
701
- if (this.editor.getAttributes("link")["href"]) {
702
- this.editor.chain().focus().unsetLink().run();
703
- return;
704
- }
705
- const href = window.prompt(this.labels.linkPrompt, "https://");
706
- if (!href)
707
- return;
708
- if (!/^https?:\/\//i.test(href)) {
709
- this.errored.emit(`${this.labels.link}: http(s) only`);
710
- return;
711
- }
712
- this.editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
713
- }
714
- value() {
715
- const html = cleanHtml(this.editor?.getHTML() ?? "", this.host);
716
- return htmlIsEmpty(html) ? "" : html;
717
- }
718
- clear() {
719
- this.editor?.commands.clearContent(true);
720
- }
721
- }
722
- __legacyDecorateClassTS([
723
- Input5({ required: true }),
724
- __legacyMetadataTS("design:type", Object)
725
- ], RichReplyEditorComponent.prototype, "placeholder", undefined);
726
- __legacyDecorateClassTS([
727
- Input5({ required: true }),
728
- __legacyMetadataTS("design:type", Object)
729
- ], RichReplyEditorComponent.prototype, "host", undefined);
730
- __legacyDecorateClassTS([
731
- Input5({ required: true }),
732
- __legacyMetadataTS("design:type", typeof EditorLabels === "undefined" ? Object : EditorLabels)
733
- ], RichReplyEditorComponent.prototype, "labels", undefined);
734
- __legacyDecorateClassTS([
735
- Input5({ required: true }),
736
- __legacyMetadataTS("design:type", Function)
737
- ], RichReplyEditorComponent.prototype, "uploadImage", undefined);
738
- __legacyDecorateClassTS([
739
- Input5(),
740
- __legacyMetadataTS("design:paramtypes", [
741
- Boolean
742
- ]),
743
- __legacyMetadataTS("design:type", Boolean)
744
- ], RichReplyEditorComponent.prototype, "disabledState", null);
745
- __legacyDecorateClassTS([
746
- Input5(),
747
- __legacyMetadataTS("design:type", String)
748
- ], RichReplyEditorComponent.prototype, "locale", undefined);
749
- __legacyDecorateClassTS([
750
- Input5(),
751
- __legacyMetadataTS("design:type", String)
752
- ], RichReplyEditorComponent.prototype, "initial", undefined);
753
- __legacyDecorateClassTS([
754
- Output3(),
755
- __legacyMetadataTS("design:type", Object)
756
- ], RichReplyEditorComponent.prototype, "errored", undefined);
757
- __legacyDecorateClassTS([
758
- ViewChild3("mount", { static: true }),
759
- __legacyMetadataTS("design:type", typeof ElementRef3 === "undefined" ? Object : ElementRef3)
760
- ], RichReplyEditorComponent.prototype, "mount", undefined);
761
- __legacyDecorateClassTS([
762
- ViewChild3("file", { static: true }),
763
- __legacyMetadataTS("design:type", typeof ElementRef3 === "undefined" ? Object : ElementRef3)
764
- ], RichReplyEditorComponent.prototype, "file", undefined);
765
- RichReplyEditorComponent = __legacyDecorateClassTS([
766
- Component5({
767
- selector: "lw-rich-reply-editor",
768
- standalone: true,
769
- imports: [NgFor3, NgIf4, IconComponent, ImageAnnotatorDialogComponent],
770
- template: `
771
- <div class="lw-editor" [class.lw-editor--disabled]="disabled">
772
- <div class="lw-editor__tools">
773
- <ng-container *ngFor="let tool of tools">
774
- <span *ngIf="tool.separatorBefore" class="lw-editor__sep"></span>
775
- <button
776
- type="button"
777
- class="lw-tool"
778
- [class.lw-tool--on]="isActive(tool.mark)"
779
- [title]="labels[tool.key]"
780
- [attr.aria-label]="labels[tool.key]"
781
- [attr.aria-pressed]="isActive(tool.mark)"
782
- [disabled]="disabled"
783
- (mousedown)="press($event, tool)"
784
- >
785
- <lw-icon [svg]="tool.icon" />
786
- </button>
787
- </ng-container>
788
- </div>
789
- <div #mount class="lw-editor__content"></div>
790
- <input #file type="file" accept="image/*" hidden (change)="onFile($event)" />
791
-
792
- <lw-image-annotator-dialog
793
- *ngIf="pending"
794
- [src]="pending"
795
- [filename]="pendingName"
796
- [locale]="locale"
797
- (confirmed)="commitImage($event)"
798
- (cancelled)="pending = null"
799
- />
800
- </div>
801
- `
802
- }),
803
- __legacyMetadataTS("design:paramtypes", [
804
- typeof ChangeDetectorRef === "undefined" ? Object : ChangeDetectorRef
805
- ])
806
- ], RichReplyEditorComponent);
807
- function readDataUri(file) {
808
- return new Promise((resolve, reject) => {
809
- const reader = new FileReader;
810
- reader.addEventListener("load", () => typeof reader.result === "string" ? resolve(reader.result) : reject(new Error("unreadable file")));
811
- reader.addEventListener("error", () => reject(new Error("unreadable file")));
812
- reader.readAsDataURL(file);
813
- });
814
- }
815
- function dataUriToFile(dataUri, name) {
816
- const [header, encoded] = dataUri.split(",");
817
- const type = /data:([^;]+)/.exec(header ?? "")?.[1] ?? "image/png";
818
- const binary = atob(encoded ?? "");
819
- const bytes = new Uint8Array(binary.length);
820
- for (let i = 0;i < binary.length; i += 1)
821
- bytes[i] = binary.charCodeAt(i);
822
- return new File([bytes], name, { type });
823
- }
824
-
825
- // src/components/ticket-detail.component.ts
826
- var TONES = {
827
- OPEN: { icon: ICONS.inbox, border: "#bfdbfe", bg: "#eff6ff", fg: "#1e3a8a", accent: "#3b82f6" },
828
- IN_PROGRESS: { icon: ICONS.clock, border: "#fde68a", bg: "#fffbeb", fg: "#78350f", accent: "#f59e0b" },
829
- RESOLVED: { icon: ICONS.checkCircle, border: "#a7f3d0", bg: "#ecfdf5", fg: "#064e3b", accent: "#10b981" },
830
- CLOSED: { icon: ICONS.checkCircle, border: "#e5e7eb", bg: "#f9fafb", fg: "#1f2937", accent: "#9ca3af" },
831
- WONT_FIX: { icon: ICONS.ban, border: "#e5e7eb", bg: "#f9fafb", fg: "#374151", accent: "#9ca3af" }
832
- };
833
- var AWAITING = { icon: ICONS.message, border: "#fcd34d", bg: "#fffbeb", fg: "#78350f", accent: "#f59e0b" };
834
- var MESSAGES = {
835
- OPEN: { th: "รับเรื่องแล้ว — รอทีมงานตรวจสอบ", en: "Received — waiting for the team to look" },
836
- IN_PROGRESS: { th: "ทีมงานกำลังดำเนินการแก้ไข", en: "The team is working on it" },
837
- RESOLVED: { th: "แก้ไขเรียบร้อยแล้ว", en: "Fixed" },
838
- CLOSED: { th: "ปิดเรื่องแล้ว", en: "Closed" },
839
- WONT_FIX: { th: "พิจารณาแล้ว — ยังไม่ได้ดำเนินการในขณะนี้", en: "Reviewed — not being worked on for now" }
840
- };
841
- var HAPPY = ["OPEN", "IN_PROGRESS", "RESOLVED", "CLOSED"];
842
-
843
- class TicketDetailComponent {
844
- constructor() {
845
- this.history = [];
846
- this.host = "";
847
- this.sending = false;
848
- this.reopening = false;
849
- this.back = new EventEmitter4;
850
- this.sent = new EventEmitter4;
851
- this.reopened = new EventEmitter4;
852
- this.edited = new EventEmitter4;
853
- this.errored = new EventEmitter4;
854
- }
855
- set attachmentUrl(read) {
856
- this.readUrl = read;
857
- this.loadShots();
858
- }
859
- editingId = null;
860
- savingEdit = false;
861
- async saveEdit(messageId) {
862
- const body = this.draft?.value() ?? "";
863
- if (!body || this.savingEdit)
864
- return;
865
- this.savingEdit = true;
866
- try {
867
- await editMyMessage(this.ticket.id, messageId, body);
868
- this.editingId = null;
869
- this.edited.emit();
870
- } catch (e) {
871
- this.errored.emit(e instanceof Error ? e.message : this.t.sendFailed);
872
- } finally {
873
- this.savingEdit = false;
874
- }
875
- }
876
- seenMessages = -1;
877
- ngOnChanges() {
878
- const count = this.ticket?.messages.length ?? 0;
879
- const first = this.seenMessages < 0;
880
- if (first || count > this.seenMessages) {
881
- queueMicrotask(() => {
882
- const box = this.thread?.nativeElement;
883
- if (!box)
884
- return;
885
- box.scrollTop = box.scrollHeight;
886
- if (!first)
887
- box.lastElementChild?.scrollIntoView({ block: "nearest", behavior: "smooth" });
888
- });
889
- }
890
- this.seenMessages = count;
891
- }
892
- icons = ICONS;
893
- reopenOpen = false;
894
- shotUrls = {};
895
- readUrl;
896
- get lang() {
897
- return this.locale === "en" ? "en" : "th";
898
- }
899
- get key() {
900
- return ticketKey2(this.config.realm.slug, this.ticket.number);
901
- }
902
- get updated() {
903
- return Boolean(this.ticket.updatedAt && this.ticket.updatedAt !== this.ticket.createdAt);
904
- }
905
- get awaiting() {
906
- return this.maps.isAwaitingReply(this.ticket.status);
907
- }
908
- get tone() {
909
- return this.awaiting ? AWAITING : TONES[this.ticket.status] ?? TONES["OPEN"];
910
- }
911
- get kicker() {
912
- if (this.awaiting)
913
- return this.lang === "en" ? "AWAITING YOUR REPLY" : "รอการตอบกลับจากคุณ";
914
- return this.lang === "en" ? "LATEST STATUS" : "สถานะล่าสุด";
915
- }
916
- get headline() {
917
- if (this.awaiting) {
918
- return this.lang === "en" ? "The team needs more information from you" : "ทีมงานขอข้อมูลเพิ่มเติม — รอการตอบกลับจากคุณ";
919
- }
920
- const described = this.maps.columnOf(this.ticket.status)?.description?.trim();
921
- return described || MESSAGES[this.ticket.status]?.[this.lang] || this.maps.labelOf(this.ticket.status);
922
- }
923
- get showNote() {
924
- return !this.awaiting && !this.ticket.messages.some((m) => m.authorRole === "AGENT") && this.maps.isClosed(this.ticket.status) && Boolean(this.ticket.resolutionNote?.trim());
925
- }
926
- get canReopen() {
927
- return this.maps.isClosed(this.ticket.status) && this.config.featureFlags.selfServiceReopen === true;
928
- }
929
- get reopenPlaceholder() {
930
- return this.lang === "en" ? "Tell us what is still happening (optional)…" : "บอกเราหน่อยว่ายังพบปัญหาอะไร (ไม่ระบุก็ได้)…";
931
- }
932
- get editorLabels() {
933
- return {
934
- bold: this.t.bold,
935
- italic: this.t.italic,
936
- underline: this.t.underline,
937
- bulletList: this.t.bulletList,
938
- orderedList: this.t.orderedList,
939
- link: this.t.link,
940
- linkPrompt: this.t.linkPrompt,
941
- attachImage: this.t.attachImage
942
- };
943
- }
944
- get screenshots() {
945
- return this.ticket.attachments.filter((a) => (a.contentType ?? "").startsWith("image/"));
946
- }
947
- get timeline() {
948
- const events = this.history.length ? this.history.map((h) => ({ status: h.toStatus, at: h.createdAt })) : [{ status: "OPEN", at: this.ticket.createdAt }];
949
- const nodes = events.map((event, i) => {
950
- const previous = events[i - 1];
951
- return {
952
- label: i === 0 ? this.lang === "en" ? "Reported" : "รายงานปัญหา" : this.maps.labelOf(event.status),
953
- time: this.when(event.at),
954
- state: i === events.length - 1 ? "current" : "done",
955
- reopened: Boolean(previous && this.maps.isClosed(previous.status) && !this.maps.isClosed(event.status))
956
- };
957
- });
958
- const index = HAPPY.indexOf(this.ticket.status);
959
- const upcoming = this.maps.isClosed(this.ticket.status) || index < 0 ? [] : HAPPY.slice(index + 1);
960
- for (const status of upcoming) {
961
- nodes.push({
962
- label: this.maps.labelOf(status),
963
- time: this.lang === "en" ? "Pending" : "รอดำเนินการ",
964
- state: "pending",
965
- reopened: false
966
- });
967
- }
968
- return nodes;
969
- }
970
- when(iso) {
971
- return formatDateTime2(iso, this.config.timezone, this.locale);
972
- }
973
- send() {
974
- const body = this.editor?.value() ?? "";
975
- if (!body || this.sending)
976
- return;
977
- this.sent.emit(body);
978
- this.editor?.clear();
979
- }
980
- loadShots() {
981
- if (!this.readUrl || !this.ticket)
982
- return;
983
- for (const shot of this.screenshots) {
984
- this.readUrl(shot.id).then((url) => this.shotUrls = { ...this.shotUrls, [shot.id]: url }, () => {
985
- return;
986
- });
987
- }
988
- }
989
- }
990
- __legacyDecorateClassTS([
991
- Input6({ required: true }),
992
- __legacyMetadataTS("design:type", typeof MyTicketDetail === "undefined" ? Object : MyTicketDetail)
993
- ], TicketDetailComponent.prototype, "ticket", undefined);
994
- __legacyDecorateClassTS([
995
- Input6({ required: true }),
996
- __legacyMetadataTS("design:type", Array)
997
- ], TicketDetailComponent.prototype, "history", undefined);
998
- __legacyDecorateClassTS([
999
- Input6({ required: true }),
1000
- __legacyMetadataTS("design:type", typeof RealmConfig === "undefined" ? Object : RealmConfig)
1001
- ], TicketDetailComponent.prototype, "config", undefined);
1002
- __legacyDecorateClassTS([
1003
- Input6({ required: true }),
1004
- __legacyMetadataTS("design:type", typeof StatusMaps === "undefined" ? Object : StatusMaps)
1005
- ], TicketDetailComponent.prototype, "maps", undefined);
1006
- __legacyDecorateClassTS([
1007
- Input6({ required: true }),
1008
- __legacyMetadataTS("design:type", typeof TicketStrings === "undefined" ? Object : TicketStrings)
1009
- ], TicketDetailComponent.prototype, "t", undefined);
1010
- __legacyDecorateClassTS([
1011
- Input6(),
1012
- __legacyMetadataTS("design:type", Object)
1013
- ], TicketDetailComponent.prototype, "host", undefined);
1014
- __legacyDecorateClassTS([
1015
- Input6(),
1016
- __legacyMetadataTS("design:type", String)
1017
- ], TicketDetailComponent.prototype, "locale", undefined);
1018
- __legacyDecorateClassTS([
1019
- Input6(),
1020
- __legacyMetadataTS("design:type", Object)
1021
- ], TicketDetailComponent.prototype, "sending", undefined);
1022
- __legacyDecorateClassTS([
1023
- Input6(),
1024
- __legacyMetadataTS("design:type", Object)
1025
- ], TicketDetailComponent.prototype, "reopening", undefined);
1026
- __legacyDecorateClassTS([
1027
- Input6({ required: true }),
1028
- __legacyMetadataTS("design:type", Function)
1029
- ], TicketDetailComponent.prototype, "uploadImage", undefined);
1030
- __legacyDecorateClassTS([
1031
- Input6({ required: true }),
1032
- __legacyMetadataTS("design:paramtypes", [
1033
- Function
1034
- ]),
1035
- __legacyMetadataTS("design:type", Function)
1036
- ], TicketDetailComponent.prototype, "attachmentUrl", null);
1037
- __legacyDecorateClassTS([
1038
- Output4(),
1039
- __legacyMetadataTS("design:type", Object)
1040
- ], TicketDetailComponent.prototype, "back", undefined);
1041
- __legacyDecorateClassTS([
1042
- Output4(),
1043
- __legacyMetadataTS("design:type", Object)
1044
- ], TicketDetailComponent.prototype, "sent", undefined);
1045
- __legacyDecorateClassTS([
1046
- Output4(),
1047
- __legacyMetadataTS("design:type", Object)
1048
- ], TicketDetailComponent.prototype, "reopened", undefined);
1049
- __legacyDecorateClassTS([
1050
- Output4(),
1051
- __legacyMetadataTS("design:type", Object)
1052
- ], TicketDetailComponent.prototype, "edited", undefined);
1053
- __legacyDecorateClassTS([
1054
- Output4(),
1055
- __legacyMetadataTS("design:type", Object)
1056
- ], TicketDetailComponent.prototype, "errored", undefined);
1057
- __legacyDecorateClassTS([
1058
- ViewChild4("draft"),
1059
- __legacyMetadataTS("design:type", typeof RichReplyEditorComponent === "undefined" ? Object : RichReplyEditorComponent)
1060
- ], TicketDetailComponent.prototype, "draft", undefined);
1061
- __legacyDecorateClassTS([
1062
- ViewChild4("editor"),
1063
- __legacyMetadataTS("design:type", typeof RichReplyEditorComponent === "undefined" ? Object : RichReplyEditorComponent)
1064
- ], TicketDetailComponent.prototype, "editor", undefined);
1065
- __legacyDecorateClassTS([
1066
- ViewChild4("thread"),
1067
- __legacyMetadataTS("design:type", typeof ElementRef4 === "undefined" ? Object : ElementRef4)
1068
- ], TicketDetailComponent.prototype, "thread", undefined);
1069
- TicketDetailComponent = __legacyDecorateClassTS([
1070
- Component6({
1071
- selector: "lw-ticket-detail",
1072
- standalone: true,
1073
- imports: [NgIf5, NgFor4, NgClass, IconComponent, TicketBodyComponent, RichReplyEditorComponent],
1074
- template: `
1075
- <div class="lw-detail">
1076
- <button type="button" class="lw-back" (click)="back.emit()">
1077
- <lw-icon [svg]="icons.arrowLeft" /> {{ t.back }}
1078
- </button>
1079
-
1080
- <section class="lw-card">
1081
- <h3 class="lw-card__heading">{{ ticket.title }}</h3>
1082
- <div class="lw-facts">
1083
- <span class="lw-key">{{ key }}</span>
1084
- <span *ngIf="ticket.category" class="lw-chip">{{ ticket.category }}</span>
1085
- <span>{{ t.colCreated }} {{ when(ticket.createdAt) }}</span>
1086
- <span *ngIf="updated">{{ t.colUpdated }} {{ when(ticket.updatedAt) }}</span>
1087
- </div>
1088
- <lw-ticket-body *ngIf="ticket.description" class="lw-intro" [html]="ticket.description" [host]="host" />
1089
- <p *ngIf="ticket.routeUrl" class="lw-route">
1090
- {{ t.page }}:
1091
- <a [href]="ticket.routeUrl" target="_blank" rel="noopener noreferrer">{{ ticket.routeUrl }}</a>
1092
- </p>
1093
- </section>
1094
-
1095
- <section class="lw-hero" [style.border-color]="tone.border" [style.background]="tone.bg">
1096
- <lw-icon class="lw-hero__icon" [svg]="tone.icon" [style.color]="tone.accent" />
1097
- <div class="lw-hero__body">
1098
- <p class="lw-hero__kicker">{{ kicker }}</p>
1099
- <p class="lw-hero__headline" [style.color]="tone.fg">{{ headline }}</p>
1100
- <p *ngIf="awaiting" class="lw-hero__nudge">
1101
- {{ lang === "en" ? "Please reply to the team in the conversation below" : "กรุณาตอบกลับให้ทีมงานในช่องแชทด้านล่าง" }}
1102
- </p>
1103
- <div *ngIf="showNote" class="lw-hero__note">
1104
- <lw-icon class="lw-hero__wrench" [svg]="icons.wrench" />
1105
- <lw-ticket-body [html]="ticket.resolutionNote || ''" [host]="host" />
1106
- </div>
1107
- </div>
1108
- </section>
1109
-
1110
- <section *ngIf="canReopen" class="lw-reopen">
1111
- <div *ngIf="!reopenOpen" class="lw-reopen__row">
1112
- <span class="lw-reopen__hint">{{ lang === "en" ? "Problem not fixed?" : "ปัญหายังไม่หาย?" }}</span>
1113
- <button type="button" class="lw-reopen__open" (click)="reopenOpen = true">
1114
- <lw-icon [svg]="icons.rotate" /> {{ t.reopen }}
1115
- </button>
1116
- </div>
1117
- <div *ngIf="reopenOpen">
1118
- <p class="lw-reopen__title">{{ t.reopen }}</p>
1119
- <textarea #note class="lw-reopen__note" rows="2" [placeholder]="reopenPlaceholder"></textarea>
1120
- <div class="lw-reopen__actions">
1121
- <button type="button" class="lw-reopen__cancel" (click)="reopenOpen = false; note.value = ''">
1122
- {{ lang === "en" ? "Cancel" : "ยกเลิก" }}
1123
- </button>
1124
- <button type="button" class="lw-reopen__confirm" [disabled]="reopening" (click)="reopened.emit(note.value.trim())">
1125
- <lw-icon [svg]="icons.rotate" /> {{ reopening ? t.reopening : t.reopen }}
1126
- </button>
1127
- </div>
1128
- </div>
1129
- </section>
1130
-
1131
- <section class="lw-card">
1132
- <h3 class="lw-card__title">{{ lang === "en" ? "Progress" : "ความคืบหน้า" }}</h3>
1133
- <ol class="lw-timeline">
1134
- <li *ngFor="let node of timeline; let i = index; let last = last" class="lw-node">
1135
- <div class="lw-rail">
1136
- <span *ngIf="!last" class="lw-line" [class.lw-line--pending]="node.state === 'pending'"></span>
1137
- <span class="lw-dot" [ngClass]="'lw-dot--' + node.state" [class.lw-dot--reopened]="node.reopened">
1138
- <span *ngIf="node.state === 'current' && !node.reopened" class="lw-pip"></span>
1139
- </span>
1140
- </div>
1141
- <div class="lw-node__text" [class.lw-node__text--last]="last">
1142
- <div class="lw-node__label" [ngClass]="'lw-node__label--' + node.state" [class.lw-node__label--reopened]="node.reopened">
1143
- <lw-icon *ngIf="node.reopened" class="lw-node__icon" [svg]="icons.rotate" />
1144
- {{ node.reopened ? (lang === "en" ? "Reopened" : "เปิดใหม่") + " · " + node.label : node.label }}
1145
- </div>
1146
- <div class="lw-node__time">{{ node.time }}</div>
1147
- </div>
1148
- </li>
1149
- </ol>
1150
- </section>
1151
-
1152
- <section class="lw-card">
1153
- <h3 class="lw-card__title">{{ t.conversation }}</h3>
1154
- <div #thread class="lw-thread">
1155
- <p *ngIf="!ticket.messages.length" class="lw-thread__empty">{{ t.noMessages }}</p>
1156
- <article
1157
- *ngFor="let message of ticket.messages"
1158
- class="lw-msg"
1159
- [class.lw-msg--mine]="message.authorRole === 'REPORTER'"
1160
- >
1161
- <!-- The original's shape exactly: the meta line lives INSIDE a
1162
- bordered bubble, and a TEAM message shows only "ทีมงาน" — never
1163
- the officer's name. That is deliberate there and worth keeping:
1164
- the team answers as one unit, and a reporter should not see
1165
- which person replied. -->
1166
- <div class="lw-msg__bubble">
1167
- <p class="lw-msg__meta">
1168
- <span *ngIf="message.authorRole === 'AGENT'" class="lw-msg__who">{{ t.team }}</span>
1169
- <ng-container *ngIf="message.authorRole !== 'AGENT'">
1170
- <span class="lw-msg__who">{{ message.authorName || t.you }}</span>
1171
- <span class="lw-msg__role">{{ t.reporterRole }}</span>
1172
- </ng-container>
1173
- <span>· {{ when(message.createdAt) }}</span>
1174
- <span *ngIf="message.editedAt" class="lw-msg__edited">· {{ t.edited }}</span>
1175
- <!-- Your own only. The server refuses any message the caller did
1176
- not write, so this button is a convenience, not the rule. -->
1177
- <button
1178
- *ngIf="message.authorRole === 'REPORTER' && editingId !== message.id"
1179
- type="button"
1180
- class="lw-msg__edit"
1181
- (click)="editingId = message.id"
1182
- >
1183
- <lw-icon [svg]="icons.pencil" /> {{ t.edit }}
1184
- </button>
1185
- </p>
1186
- <div *ngIf="editingId === message.id; else plainBody" class="lw-msg__editing">
1187
- <lw-rich-reply-editor
1188
- #draft
1189
- [placeholder]="t.replyPlaceholder"
1190
- [host]="host"
1191
- [locale]="lang"
1192
- [labels]="editorLabels"
1193
- [disabledState]="savingEdit"
1194
- [uploadImage]="uploadImage"
1195
- [initial]="message.body"
1196
- (errored)="errored.emit($event)"
1197
- />
1198
- <div class="lw-msg__editActions">
1199
- <button type="button" class="lw-btn" [disabled]="savingEdit" (click)="editingId = null">
1200
- <lw-icon [svg]="icons.xSmall" /> {{ t.cancel }}
1201
- </button>
1202
- <button type="button" class="lw-send" [disabled]="savingEdit" (click)="saveEdit(message.id)">
1203
- <lw-icon [svg]="icons.checkSmall" /> {{ t.saveEdit }}
1204
- </button>
1205
- </div>
1206
- </div>
1207
- <ng-template #plainBody>
1208
- <lw-ticket-body [html]="message.body" [host]="host" />
1209
- </ng-template>
1210
- </div>
1211
- </article>
1212
- </div>
1213
- <div class="lw-composer">
1214
- <lw-rich-reply-editor
1215
- #editor
1216
- [placeholder]="t.replyPlaceholder"
1217
- [host]="host"
1218
- [locale]="lang"
1219
- [labels]="editorLabels"
1220
- [disabledState]="!!sending"
1221
- [uploadImage]="uploadImage"
1222
- (errored)="errored.emit($event)"
1223
- />
1224
- <div class="lw-composer__actions">
1225
- <button type="button" class="lw-send" [disabled]="sending" (click)="send()">
1226
- <lw-icon [svg]="icons.send" /> {{ sending ? t.sending : t.send }}
1227
- </button>
1228
- </div>
1229
- </div>
1230
- </section>
1231
-
1232
- <section *ngIf="screenshots.length" class="lw-card">
1233
- <h3 class="lw-card__title">{{ lang === "en" ? "Screenshots you sent" : "ภาพหน้าจอที่คุณแจ้ง" }}</h3>
1234
- <ng-container *ngFor="let shot of screenshots">
1235
- <img *ngIf="shotUrls[shot.id]" class="lw-shot" [src]="shotUrls[shot.id]" [alt]="shot.filename || ''" />
1236
- </ng-container>
1237
- </section>
1238
- </div>
1239
- `
1240
- })
1241
- ], TicketDetailComponent);
1242
-
1243
- // src/components/my-tickets-panel.component.ts
1244
- class MyTicketsPanelComponent {
1245
- constructor() {
1246
- this.hideTitle = false;
1247
- this.replied = new EventEmitter5;
1248
- this.reopenedTicket = new EventEmitter5;
1249
- this.errored = new EventEmitter5;
1250
- }
1251
- icons = ICONS;
1252
- items = [];
1253
- config = null;
1254
- loading = true;
1255
- error = null;
1256
- noIdentity = false;
1257
- keyword = "";
1258
- applied = "";
1259
- statusFilter = "";
1260
- sort = { key: "createdAt", direction: "desc" };
1261
- page = 1;
1262
- shown = 0;
1263
- detail = null;
1264
- history = [];
1265
- detailError = null;
1266
- detailLoading = false;
1267
- sending = false;
1268
- reopening = false;
1269
- debounce = null;
1270
- first = true;
1271
- get locale() {
1272
- return this.safe(() => getConfig().locale, undefined);
1273
- }
1274
- get host() {
1275
- return this.safe(() => getConfig().host, "");
1276
- }
1277
- get t() {
1278
- return ticketStrings(this.locale);
1279
- }
1280
- get maps() {
1281
- return statusMaps(this.config?.statusColumns ?? [], this.t.allStatuses);
1282
- }
1283
- ngOnInit() {
1284
- this.load();
1285
- }
1286
- ngOnChanges() {
1287
- if (this.first) {
1288
- this.first = false;
1289
- return;
1290
- }
1291
- this.load();
1292
- }
1293
- async load() {
1294
- this.loading = true;
1295
- this.error = null;
1296
- this.noIdentity = false;
1297
- try {
1298
- const [realm, list] = await Promise.all([fetchRealmConfig(), listMyTickets()]);
1299
- this.config = realm;
1300
- this.items = list;
1301
- } catch (e) {
1302
- if (e instanceof NoIdentityError)
1303
- this.noIdentity = true;
1304
- else
1305
- this.error = e instanceof Error ? e.message : this.t.listFailed;
1306
- } finally {
1307
- this.loading = false;
1308
- }
1309
- }
1310
- onKeyword(next) {
1311
- this.keyword = next;
1312
- if (this.debounce)
1313
- clearTimeout(this.debounce);
1314
- this.debounce = setTimeout(() => {
1315
- this.applied = next;
1316
- this.page = 1;
1317
- }, 300);
1318
- }
1319
- onStatus(next) {
1320
- this.statusFilter = next;
1321
- this.page = 1;
1322
- }
1323
- async open(id) {
1324
- this.detail = null;
1325
- this.history = [];
1326
- this.detailError = null;
1327
- this.detailLoading = true;
1328
- try {
1329
- return await this.reload(id);
1330
- } finally {
1331
- this.detailLoading = false;
1332
- }
1333
- }
1334
- async reload(id) {
1335
- try {
1336
- const [found, events] = await Promise.all([
1337
- getMyTicket(id),
1338
- getMyTicketHistory(id).catch(() => [])
1339
- ]);
1340
- this.detail = found;
1341
- this.history = events;
1342
- return found;
1343
- } catch (e) {
1344
- this.detailError = e instanceof Error ? e.message : this.t.detailFailed;
1345
- return null;
1346
- }
1347
- }
1348
- back() {
1349
- this.detail = null;
1350
- this.load();
1351
- }
1352
- toggleSort(key) {
1353
- this.sort = this.sort.key === key ? { key, direction: this.sort.direction === "asc" ? "desc" : "asc" } : { key, direction: key === "createdAt" || key === "updatedAt" ? "desc" : "asc" };
1354
- }
1355
- async send(body) {
1356
- const ticket = this.detail;
1357
- if (!ticket || this.sending)
1358
- return;
1359
- this.sending = true;
1360
- try {
1361
- await replyToTicket(ticket.id, body);
1362
- const fresh = await this.reload(ticket.id);
1363
- if (fresh)
1364
- this.replied.emit(fresh);
1365
- } catch (e) {
1366
- this.errored.emit(e instanceof Error ? e.message : this.t.sendFailed);
1367
- } finally {
1368
- this.sending = false;
1369
- }
1370
- }
1371
- async reopen(note) {
1372
- const ticket = this.detail;
1373
- if (!ticket || this.reopening)
1374
- return;
1375
- this.reopening = true;
1376
- try {
1377
- await reopenMyTicket(ticket.id, note);
1378
- const fresh = await this.reload(ticket.id);
1379
- if (fresh)
1380
- this.reopenedTicket.emit(fresh);
1381
- } catch (e) {
1382
- this.errored.emit(e instanceof Error ? e.message : this.t.reopenFailed);
1383
- } finally {
1384
- this.reopening = false;
1385
- }
1386
- }
1387
- uploadImage = async (file) => {
1388
- const ticket = this.detail;
1389
- if (!ticket)
1390
- throw new Error("no ticket");
1391
- const dataUri = await readDataUri2(file);
1392
- const stored = await uploadInlineImage(ticket.id, dataUri, file.name);
1393
- return stored.url;
1394
- };
1395
- attachmentUrl = (id) => getAttachmentUrl(id).then((found) => found.url);
1396
- safe(read, fallback) {
1397
- try {
1398
- return read();
1399
- } catch {
1400
- return fallback;
1401
- }
1402
- }
1403
- }
1404
- __legacyDecorateClassTS([
1405
- Input7(),
1406
- __legacyMetadataTS("design:type", Object)
1407
- ], MyTicketsPanelComponent.prototype, "refreshKey", undefined);
1408
- __legacyDecorateClassTS([
1409
- Input7(),
1410
- __legacyMetadataTS("design:type", Object)
1411
- ], MyTicketsPanelComponent.prototype, "hideTitle", undefined);
1412
- __legacyDecorateClassTS([
1413
- Output5(),
1414
- __legacyMetadataTS("design:type", Object)
1415
- ], MyTicketsPanelComponent.prototype, "replied", undefined);
1416
- __legacyDecorateClassTS([
1417
- Output5(),
1418
- __legacyMetadataTS("design:type", Object)
1419
- ], MyTicketsPanelComponent.prototype, "reopenedTicket", undefined);
1420
- __legacyDecorateClassTS([
1421
- Output5(),
1422
- __legacyMetadataTS("design:type", Object)
1423
- ], MyTicketsPanelComponent.prototype, "errored", undefined);
1424
- MyTicketsPanelComponent = __legacyDecorateClassTS([
1425
- Component7({
1426
- selector: "lw-my-tickets-panel",
1427
- standalone: true,
1428
- imports: [NgIf6, NgFor5, FormsModule, IconComponent, TicketTableComponent, TicketDetailComponent],
1429
- template: `
1430
- <div class="lw-panel">
1431
- <ng-container *ngIf="!detail && !detailLoading">
1432
- <h2 *ngIf="!hideTitle" class="lw-panel__title">{{ t.title }}</h2>
1433
- <p class="lw-panel__intro">{{ t.intro }}</p>
1434
-
1435
- <div class="lw-controls">
1436
- <div class="lw-search">
1437
- <lw-icon class="lw-search__icon" [svg]="icons.search" />
1438
- <input
1439
- type="search"
1440
- class="lw-input lw-search__field"
1441
- [placeholder]="t.searchPlaceholder"
1442
- [attr.aria-label]="t.searchLabel"
1443
- [ngModel]="keyword"
1444
- (ngModelChange)="onKeyword($event)"
1445
- />
1446
- </div>
1447
- <select class="lw-input" [ngModel]="statusFilter" (ngModelChange)="onStatus($event)">
1448
- <option *ngFor="let option of maps.options" [value]="option.value">{{ option.label }}</option>
1449
- </select>
1450
- <span class="lw-count">{{ t.count(shown) }}</span>
1451
- </div>
1452
-
1453
- <p *ngIf="noIdentity" class="lw-state">{{ t.signedOut }}</p>
1454
- <div *ngIf="!noIdentity && loading">
1455
- <div class="lw-skeleton"></div>
1456
- <div class="lw-skeleton"></div>
1457
- <div class="lw-skeleton"></div>
1458
- <div class="lw-skeleton"></div>
1459
- </div>
1460
- <p *ngIf="!noIdentity && !loading && error" class="lw-state lw-state--error">
1461
- <lw-icon [svg]="icons.alert" /> {{ error }}
1462
- <button type="button" class="lw-retry" (click)="load()">{{ t.retry }}</button>
1463
- </p>
1464
- <lw-ticket-table
1465
- *ngIf="!noIdentity && !loading && !error"
1466
- [items]="items"
1467
- [maps]="maps"
1468
- [t]="t"
1469
- [slug]="config?.realm?.slug"
1470
- [timezone]="config?.timezone || 'Asia/Bangkok'"
1471
- [locale]="locale"
1472
- [keyword]="applied"
1473
- [statusFilter]="statusFilter"
1474
- [sort]="sort"
1475
- [page]="page"
1476
- (opened)="open($event)"
1477
- (sorted)="toggleSort($event)"
1478
- (paged)="page = $event"
1479
- (counted)="shown = $event"
1480
- />
1481
- </ng-container>
1482
-
1483
- <div *ngIf="detailLoading">
1484
- <div class="lw-skeleton"></div>
1485
- <div class="lw-skeleton"></div>
1486
- </div>
1487
-
1488
- <p *ngIf="!detailLoading && detailError" class="lw-state lw-state--error">{{ detailError }}</p>
1489
-
1490
- <lw-ticket-detail
1491
- *ngIf="!detailLoading && detail && config"
1492
- [ticket]="detail"
1493
- [history]="history"
1494
- [config]="config"
1495
- [maps]="maps"
1496
- [t]="t"
1497
- [host]="host"
1498
- [locale]="locale"
1499
- [sending]="sending"
1500
- [reopening]="reopening"
1501
- [uploadImage]="uploadImage"
1502
- [attachmentUrl]="attachmentUrl"
1503
- (back)="back()"
1504
- (edited)="detail && reload(detail.id)"
1505
- (sent)="send($event)"
1506
- (reopened)="reopen($event)"
1507
- (errored)="errored.emit($event)"
1508
- />
1509
- </div>
1510
- `
1511
- })
1512
- ], MyTicketsPanelComponent);
1513
- function readDataUri2(file) {
1514
- return new Promise((resolve, reject) => {
1515
- const reader = new FileReader;
1516
- reader.addEventListener("load", () => typeof reader.result === "string" ? resolve(reader.result) : reject(new Error("unreadable file")));
1517
- reader.addEventListener("error", () => reject(new Error("unreadable file")));
1518
- reader.readAsDataURL(file);
1519
- });
1520
- }
1521
-
1522
- // src/index.ts
17
+ import { APP_INITIALIZER, Component, Input, makeEnvironmentProviders, Output, EventEmitter } from "@angular/core";
18
+ import { configureDebugCapture, installDebugCapture, launch, setErroredQueriesSource, openSupportPortal, showToast } from "@lightworkai.official/debug-capture";
1523
19
  import { configureDebugCapture as configureDebugCapture2, launch as launch2, onCrash } from "@lightworkai.official/debug-capture";
1524
20
  function provideDebugCapture(options) {
1525
21
  const { errorQueries, ui, ...config } = options;
@@ -1546,11 +42,11 @@ class ReportProblemButtonComponent {
1546
42
  }
1547
43
  }
1548
44
  __legacyDecorateClassTS([
1549
- Input8(),
45
+ Input(),
1550
46
  __legacyMetadataTS("design:type", Object)
1551
47
  ], ReportProblemButtonComponent.prototype, "label", undefined);
1552
48
  ReportProblemButtonComponent = __legacyDecorateClassTS([
1553
- Component8({
49
+ Component({
1554
50
  selector: "lw-report-problem-button",
1555
51
  standalone: true,
1556
52
  template: `
@@ -1577,16 +73,57 @@ ReportProblemButtonComponent = __legacyDecorateClassTS([
1577
73
  styles: [":host { display: inline-flex; }"]
1578
74
  })
1579
75
  ], ReportProblemButtonComponent);
76
+
77
+ class SupportPortalButtonComponent {
78
+ constructor() {
79
+ this.label = "ดูปัญหาที่แจ้ง";
80
+ this.newTab = false;
81
+ this.failed = new EventEmitter;
82
+ }
83
+ pending = false;
84
+ async open() {
85
+ if (this.pending)
86
+ return;
87
+ this.pending = true;
88
+ try {
89
+ await openSupportPortal(this.newTab ? { target: "_blank" } : {});
90
+ } catch (err) {
91
+ this.failed.emit(err);
92
+ showToast("เปิดรายการปัญหาไม่สำเร็จ", "error");
93
+ } finally {
94
+ this.pending = false;
95
+ }
96
+ }
97
+ }
98
+ __legacyDecorateClassTS([
99
+ Input(),
100
+ __legacyMetadataTS("design:type", Object)
101
+ ], SupportPortalButtonComponent.prototype, "label", undefined);
102
+ __legacyDecorateClassTS([
103
+ Input(),
104
+ __legacyMetadataTS("design:type", Object)
105
+ ], SupportPortalButtonComponent.prototype, "newTab", undefined);
106
+ __legacyDecorateClassTS([
107
+ Output(),
108
+ __legacyMetadataTS("design:type", Object)
109
+ ], SupportPortalButtonComponent.prototype, "failed", undefined);
110
+ SupportPortalButtonComponent = __legacyDecorateClassTS([
111
+ Component({
112
+ selector: "lw-support-portal-button",
113
+ standalone: true,
114
+ template: `
115
+ <button type="button" [attr.aria-label]="label" [attr.title]="label" [disabled]="pending" (click)="open()">
116
+ <ng-content>{{ label }}</ng-content>
117
+ </button>
118
+ `,
119
+ styles: [":host { display: inline-flex; }"]
120
+ })
121
+ ], SupportPortalButtonComponent);
1580
122
  export {
1581
123
  provideDebugCapture,
1582
124
  onCrash,
1583
125
  launch2 as launch,
1584
126
  configureDebugCapture2 as configureDebugCapture,
1585
- TicketTableComponent,
1586
- TicketDetailComponent,
1587
- TicketBodyComponent,
1588
- RichReplyEditorComponent,
1589
- ReportProblemButtonComponent,
1590
- MyTicketsPanelComponent,
1591
- ImageAnnotatorDialogComponent
127
+ SupportPortalButtonComponent,
128
+ ReportProblemButtonComponent
1592
129
  };