@lightworkai.official/debug-capture-angular 0.6.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/README.md +67 -0
- package/dist/components/icon.component.d.ts +15 -0
- package/dist/components/icons.d.ts +29 -0
- package/dist/components/image-annotator-dialog.component.d.ts +70 -0
- package/dist/components/my-tickets-panel.component.d.ts +100 -0
- package/dist/components/rich-reply-editor.component.d.ts +89 -0
- package/dist/components/ticket-body.component.d.ts +25 -0
- package/dist/components/ticket-detail.component.d.ts +125 -0
- package/dist/components/ticket-table.component.d.ts +75 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.mjs +1592 -0
- package/package.json +56 -0
- package/src/components/icon.component.ts +24 -0
- package/src/components/icons.ts +64 -0
- package/src/components/image-annotator-dialog.component.ts +159 -0
- package/src/components/my-tickets-panel.component.ts +326 -0
- package/src/components/rich-reply-editor.component.ts +287 -0
- package/src/components/ticket-body.component.ts +53 -0
- package/src/components/ticket-detail.component.ts +441 -0
- package/src/components/ticket-table.component.ts +183 -0
- package/src/index.ts +119 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1592 @@
|
|
|
1
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
4
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else
|
|
6
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
7
|
+
if (d = decorators[i])
|
|
8
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var __legacyMetadataTS = (k, v) => {
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
13
|
+
return Reflect.metadata(k, v);
|
|
14
|
+
};
|
|
15
|
+
|
|
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
|
|
1523
|
+
import { configureDebugCapture as configureDebugCapture2, launch as launch2, onCrash } from "@lightworkai.official/debug-capture";
|
|
1524
|
+
function provideDebugCapture(options) {
|
|
1525
|
+
const { errorQueries, ui, ...config } = options;
|
|
1526
|
+
return makeEnvironmentProviders([
|
|
1527
|
+
{
|
|
1528
|
+
provide: APP_INITIALIZER,
|
|
1529
|
+
multi: true,
|
|
1530
|
+
useValue: () => {
|
|
1531
|
+
configureDebugCapture(config);
|
|
1532
|
+
if (errorQueries)
|
|
1533
|
+
setErroredQueriesSource(errorQueries);
|
|
1534
|
+
installDebugCapture({ ui });
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
]);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
class ReportProblemButtonComponent {
|
|
1541
|
+
constructor() {
|
|
1542
|
+
this.label = "รายงานปัญหา";
|
|
1543
|
+
}
|
|
1544
|
+
report() {
|
|
1545
|
+
launch({ type: "manual" });
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
__legacyDecorateClassTS([
|
|
1549
|
+
Input8(),
|
|
1550
|
+
__legacyMetadataTS("design:type", Object)
|
|
1551
|
+
], ReportProblemButtonComponent.prototype, "label", undefined);
|
|
1552
|
+
ReportProblemButtonComponent = __legacyDecorateClassTS([
|
|
1553
|
+
Component8({
|
|
1554
|
+
selector: "lw-report-problem-button",
|
|
1555
|
+
standalone: true,
|
|
1556
|
+
template: `
|
|
1557
|
+
<button type="button" [attr.aria-label]="label" [attr.title]="label" (click)="report()">
|
|
1558
|
+
<ng-content>
|
|
1559
|
+
<svg
|
|
1560
|
+
width="20"
|
|
1561
|
+
height="20"
|
|
1562
|
+
viewBox="0 0 24 24"
|
|
1563
|
+
fill="none"
|
|
1564
|
+
stroke="currentColor"
|
|
1565
|
+
stroke-width="1.8"
|
|
1566
|
+
stroke-linecap="round"
|
|
1567
|
+
stroke-linejoin="round"
|
|
1568
|
+
aria-hidden="true"
|
|
1569
|
+
>
|
|
1570
|
+
<path d="M8 2l1.9 1.9M16 2l-1.9 1.9" />
|
|
1571
|
+
<rect x="8" y="6" width="8" height="14" rx="4" />
|
|
1572
|
+
<path d="M3 13h5M16 13h5M4 8l3 2M20 8l-3 2M4 18l3-2M20 18l-3-2" />
|
|
1573
|
+
</svg>
|
|
1574
|
+
</ng-content>
|
|
1575
|
+
</button>
|
|
1576
|
+
`,
|
|
1577
|
+
styles: [":host { display: inline-flex; }"]
|
|
1578
|
+
})
|
|
1579
|
+
], ReportProblemButtonComponent);
|
|
1580
|
+
export {
|
|
1581
|
+
provideDebugCapture,
|
|
1582
|
+
onCrash,
|
|
1583
|
+
launch2 as launch,
|
|
1584
|
+
configureDebugCapture2 as configureDebugCapture,
|
|
1585
|
+
TicketTableComponent,
|
|
1586
|
+
TicketDetailComponent,
|
|
1587
|
+
TicketBodyComponent,
|
|
1588
|
+
RichReplyEditorComponent,
|
|
1589
|
+
ReportProblemButtonComponent,
|
|
1590
|
+
MyTicketsPanelComponent,
|
|
1591
|
+
ImageAnnotatorDialogComponent
|
|
1592
|
+
};
|