@mintplayer/ng-spark 22.23.0 → 22.24.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.
Files changed (68) hide show
  1. package/README.md +181 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +308 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-grid.mjs +2259 -1119
  9. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-history.mjs +611 -0
  11. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  12. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  13. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-models.mjs +366 -762
  15. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  17. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  18. package/fesm2022/mintplayer-ng-spark-panels.mjs +52 -0
  19. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1122 -765
  21. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  22. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  23. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +786 -410
  25. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +292 -284
  27. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2187 -1225
  29. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-query-list.mjs +516 -303
  31. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-renderers.mjs +17 -55
  33. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  35. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  37. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  39. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-shell.mjs +1013 -397
  41. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  43. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  44. package/fesm2022/mintplayer-ng-spark.mjs +13 -50
  45. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  46. package/package.json +17 -1
  47. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  48. package/types/mintplayer-ng-spark-client-operations.d.ts +133 -160
  49. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  50. package/types/mintplayer-ng-spark-grid.d.ts +885 -448
  51. package/types/mintplayer-ng-spark-history.d.ts +223 -0
  52. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  53. package/types/mintplayer-ng-spark-models.d.ts +624 -573
  54. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  55. package/types/mintplayer-ng-spark-panels.d.ts +108 -0
  56. package/types/mintplayer-ng-spark-pipes.d.ts +120 -149
  57. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  58. package/types/mintplayer-ng-spark-po-detail.d.ts +177 -117
  59. package/types/mintplayer-ng-spark-po-edit.d.ts +64 -65
  60. package/types/mintplayer-ng-spark-po-form.d.ts +507 -371
  61. package/types/mintplayer-ng-spark-query-list.d.ts +118 -82
  62. package/types/mintplayer-ng-spark-renderers.d.ts +79 -85
  63. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  64. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  65. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  66. package/types/mintplayer-ng-spark-shell.d.ts +209 -128
  67. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  68. package/types/mintplayer-ng-spark.d.ts +7 -12
@@ -0,0 +1,1212 @@
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, Injectable, InjectionToken, computed, effect, inject, input, signal } from "@angular/core";
3
+ import { toSignal } from "@angular/core/rxjs-interop";
4
+ import { ActivatedRoute, NavigationEnd, Router, RouterLink } from "@angular/router";
5
+ import { EMPTY, filter } from "rxjs";
6
+ import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
7
+ import { TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
8
+ import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
9
+ import { DatePipe } from "@angular/common";
10
+ import { provideSparkDetailActions, provideSparkDetailPanels } from "@mintplayer/ng-spark/panels";
11
+ var SparkModerationService = class SparkModerationService {
12
+ spark = inject(SparkService);
13
+ pending = /* @__PURE__ */ new Map();
14
+ flushScheduled = false;
15
+ router = inject(Router, { optional: true });
16
+ #votesCast = signal(0, ...ngDevMode ? [{ debugName: "#votesCast" }] : /* istanbul ignore next */ []);
17
+ votesCast = this.#votesCast.asReadonly();
18
+ navigated = toSignal(this.router?.events.pipe(filter((e) => e instanceof NavigationEnd)) ?? EMPTY, { initialValue: null });
19
+ ownReputationChanged = computed(() => ({
20
+ navigation: this.navigated(),
21
+ votes: this.#votesCast()
22
+ }), ...ngDevMode ? [{ debugName: "ownReputationChanged" }] : /* istanbul ignore next */ []);
23
+ #ownInFlight = null;
24
+ ownReputation() {
25
+ return this.#ownInFlight ??= this.reputation().finally(() => this.#ownInFlight = null);
26
+ }
27
+ async vote(type, id, direction) {
28
+ const state = await this.spark.postEnvelope("/moderation/vote", {
29
+ objectTypeId: type,
30
+ id,
31
+ direction
32
+ });
33
+ this.#votesCast.update((n) => n + 1);
34
+ return state;
35
+ }
36
+ voteState(type, id) {
37
+ return new Promise((resolve) => {
38
+ let byId = this.pending.get(type);
39
+ if (!byId) this.pending.set(type, byId = /* @__PURE__ */ new Map());
40
+ const waiting = byId.get(id) ?? [];
41
+ waiting.push(resolve);
42
+ byId.set(id, waiting);
43
+ if (!this.flushScheduled) {
44
+ this.flushScheduled = true;
45
+ setTimeout(() => this.flush(), 0);
46
+ }
47
+ });
48
+ }
49
+ votes(type, ids) {
50
+ return this.spark.postEnvelope("/moderation/votes", {
51
+ objectTypeId: type,
52
+ ids
53
+ });
54
+ }
55
+ flag(type, id, reason) {
56
+ return this.spark.postEnvelope("/moderation/flag", {
57
+ objectTypeId: type,
58
+ id,
59
+ reason
60
+ });
61
+ }
62
+ lock(type, id, reason) {
63
+ return this.spark.postEnvelope("/moderation/lock", {
64
+ objectTypeId: type,
65
+ id,
66
+ reason
67
+ });
68
+ }
69
+ unlock(type, id) {
70
+ return this.spark.postEnvelope("/moderation/unlock", {
71
+ objectTypeId: type,
72
+ id
73
+ });
74
+ }
75
+ status(type, id) {
76
+ return this.spark.postEnvelope("/moderation/status", {
77
+ objectTypeId: type,
78
+ id
79
+ });
80
+ }
81
+ reputation(userId) {
82
+ return this.spark.postEnvelope("/moderation/reputation", userId ? { userId } : {});
83
+ }
84
+ history(take = 100) {
85
+ return this.spark.postEnvelope("/moderation/reputation/history", { take });
86
+ }
87
+ cases(status = "open", skip = 0, take = 50) {
88
+ return this.spark.postEnvelope("/moderation/cases", {
89
+ status,
90
+ skip,
91
+ take
92
+ });
93
+ }
94
+ case(caseId) {
95
+ return this.spark.postEnvelope("/moderation/case", { caseId });
96
+ }
97
+ decide(caseId, decision, accountId, reason) {
98
+ return this.spark.postEnvelope("/moderation/case/decide", {
99
+ caseId,
100
+ decision,
101
+ accountId,
102
+ reason
103
+ });
104
+ }
105
+ suspend(userId, days, reason) {
106
+ return this.spark.postEnvelope("/moderation/suspend", {
107
+ userId,
108
+ days: days ?? null,
109
+ reason
110
+ });
111
+ }
112
+ unsuspend(userId) {
113
+ return this.spark.postEnvelope("/moderation/unsuspend", { userId });
114
+ }
115
+ audit(skip = 0, take = 50) {
116
+ return this.spark.postEnvelope("/moderation/audit", {
117
+ skip,
118
+ take
119
+ });
120
+ }
121
+ flush() {
122
+ this.flushScheduled = false;
123
+ const batches = [...this.pending.entries()];
124
+ this.pending.clear();
125
+ for (const [type, byId] of batches) {
126
+ const ids = [...byId.keys()];
127
+ for (let i = 0; i < ids.length; i += 100) {
128
+ const chunk = ids.slice(i, i + 100);
129
+ this.votes(type, chunk).then((states) => {
130
+ const found = new Map(states.map((s) => [s.id, s]));
131
+ for (const id of chunk) byId.get(id)?.forEach((resolve) => resolve(found.get(id) ?? null));
132
+ }, () => {
133
+ for (const id of chunk) byId.get(id)?.forEach((resolve) => resolve(null));
134
+ });
135
+ }
136
+ }
137
+ }
138
+ static ɵfac = i0.ɵɵngDeclareFactory({
139
+ minVersion: "12.0.0",
140
+ version: "22.2.0",
141
+ ngImport: i0,
142
+ type: SparkModerationService,
143
+ deps: [],
144
+ target: i0.ɵɵFactoryTarget.Injectable
145
+ });
146
+ static ɵprov = i0.ɵɵngDeclareInjectable({
147
+ minVersion: "12.0.0",
148
+ version: "22.2.0",
149
+ ngImport: i0,
150
+ type: SparkModerationService,
151
+ providedIn: "root"
152
+ });
153
+ };
154
+ i0.ɵɵngDeclareClassMetadata({
155
+ minVersion: "12.0.0",
156
+ version: "22.2.0",
157
+ ngImport: i0,
158
+ type: SparkModerationService,
159
+ decorators: [{
160
+ type: Injectable,
161
+ args: [{ providedIn: "root" }]
162
+ }]
163
+ });
164
+ const SPARK_MODERATION_REVIEW_PATH = new InjectionToken("SparkModerationReviewPath", { factory: () => "/moderation/review" });
165
+ var SparkVoteComponent = class SparkVoteComponent {
166
+ moderation = inject(SparkModerationService);
167
+ value = input(...ngDevMode ? [void 0, { debugName: "value" }] : /* istanbul ignore next */ []);
168
+ attribute = input(...ngDevMode ? [void 0, { debugName: "attribute" }] : /* istanbul ignore next */ []);
169
+ column = input(...ngDevMode ? [void 0, { debugName: "column" }] : /* istanbul ignore next */ []);
170
+ options = input(...ngDevMode ? [void 0, { debugName: "options" }] : /* istanbul ignore next */ []);
171
+ item = input(...ngDevMode ? [void 0, { debugName: "item" }] : /* istanbul ignore next */ []);
172
+ state = signal(null, ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
173
+ busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
174
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
175
+ target = computed(() => {
176
+ const item = this.item();
177
+ const id = item?.["id"];
178
+ const type = item?.["objectTypeId"] ?? this.options()?.["type"];
179
+ return id && type ? {
180
+ id,
181
+ type
182
+ } : null;
183
+ }, ...ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []);
184
+ constructor() {
185
+ effect(() => {
186
+ const target = this.target();
187
+ if (!target) return;
188
+ this.moderation.voteState(target.type, target.id).then((s) => this.state.set(s));
189
+ });
190
+ }
191
+ async cast(direction) {
192
+ const target = this.target();
193
+ const current = this.state();
194
+ if (!target || !current || this.busy()) return;
195
+ const next = current.myVote === direction ? 0 : direction;
196
+ this.busy.set(true);
197
+ this.error.set(null);
198
+ try {
199
+ this.state.set(await this.moderation.vote(target.type, target.id, next));
200
+ } catch (e) {
201
+ this.error.set(describeModerationError(e));
202
+ } finally {
203
+ this.busy.set(false);
204
+ }
205
+ }
206
+ static ɵfac = i0.ɵɵngDeclareFactory({
207
+ minVersion: "12.0.0",
208
+ version: "22.2.0",
209
+ ngImport: i0,
210
+ type: SparkVoteComponent,
211
+ deps: [],
212
+ target: i0.ɵɵFactoryTarget.Component
213
+ });
214
+ static ɵcmp = i0.ɵɵngDeclareComponent({
215
+ minVersion: "17.0.0",
216
+ version: "22.2.0",
217
+ type: SparkVoteComponent,
218
+ isStandalone: true,
219
+ selector: "spark-vote",
220
+ inputs: {
221
+ value: {
222
+ classPropertyName: "value",
223
+ publicName: "value",
224
+ isSignal: true,
225
+ isRequired: false,
226
+ transformFunction: null
227
+ },
228
+ attribute: {
229
+ classPropertyName: "attribute",
230
+ publicName: "attribute",
231
+ isSignal: true,
232
+ isRequired: false,
233
+ transformFunction: null
234
+ },
235
+ column: {
236
+ classPropertyName: "column",
237
+ publicName: "column",
238
+ isSignal: true,
239
+ isRequired: false,
240
+ transformFunction: null
241
+ },
242
+ options: {
243
+ classPropertyName: "options",
244
+ publicName: "options",
245
+ isSignal: true,
246
+ isRequired: false,
247
+ transformFunction: null
248
+ },
249
+ item: {
250
+ classPropertyName: "item",
251
+ publicName: "item",
252
+ isSignal: true,
253
+ isRequired: false,
254
+ transformFunction: null
255
+ }
256
+ },
257
+ ngImport: i0,
258
+ template: `
259
+ @if (state(); as s) {
260
+ <span class="spark-vote d-inline-flex align-items-center gap-1" [class.spark-vote-locked]="s.locked">
261
+ <button type="button" class="btn btn-sm spark-vote-up" [class.btn-primary]="s.myVote === 1" [class.btn-outline-secondary]="s.myVote !== 1"
262
+ [disabled]="busy() || s.locked || (s.canUpvote === false && s.myVote !== 1)" [attr.aria-pressed]="s.myVote === 1" [attr.aria-label]="'moderation.upvote' | t" (click)="cast(1)">▲</button>
263
+ <span class="spark-vote-score fw-bold" [attr.aria-label]="'moderation.score' | t">{{ s.score }}</span>
264
+ <button type="button" class="btn btn-sm spark-vote-down" [class.btn-danger]="s.myVote === -1" [class.btn-outline-secondary]="s.myVote !== -1"
265
+ [disabled]="busy() || s.locked || (s.canDownvote === false && s.myVote !== -1)" [attr.aria-pressed]="s.myVote === -1" [attr.aria-label]="'moderation.downvote' | t" (click)="cast(-1)">▼</button>
266
+ @if (error(); as message) {
267
+ <span class="text-danger small spark-vote-error" role="alert">{{ message }}</span>
268
+ }
269
+ </span>
270
+ }
271
+ `,
272
+ isInline: true,
273
+ dependencies: [{
274
+ kind: "pipe",
275
+ type: TranslateKeyPipe,
276
+ name: "t"
277
+ }],
278
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
279
+ });
280
+ };
281
+ i0.ɵɵngDeclareClassMetadata({
282
+ minVersion: "12.0.0",
283
+ version: "22.2.0",
284
+ ngImport: i0,
285
+ type: SparkVoteComponent,
286
+ decorators: [{
287
+ type: Component,
288
+ args: [{
289
+ selector: "spark-vote",
290
+ imports: [TranslateKeyPipe],
291
+ changeDetection: ChangeDetectionStrategy.OnPush,
292
+ template: `
293
+ @if (state(); as s) {
294
+ <span class="spark-vote d-inline-flex align-items-center gap-1" [class.spark-vote-locked]="s.locked">
295
+ <button type="button" class="btn btn-sm spark-vote-up" [class.btn-primary]="s.myVote === 1" [class.btn-outline-secondary]="s.myVote !== 1"
296
+ [disabled]="busy() || s.locked || (s.canUpvote === false && s.myVote !== 1)" [attr.aria-pressed]="s.myVote === 1" [attr.aria-label]="'moderation.upvote' | t" (click)="cast(1)">▲</button>
297
+ <span class="spark-vote-score fw-bold" [attr.aria-label]="'moderation.score' | t">{{ s.score }}</span>
298
+ <button type="button" class="btn btn-sm spark-vote-down" [class.btn-danger]="s.myVote === -1" [class.btn-outline-secondary]="s.myVote !== -1"
299
+ [disabled]="busy() || s.locked || (s.canDownvote === false && s.myVote !== -1)" [attr.aria-pressed]="s.myVote === -1" [attr.aria-label]="'moderation.downvote' | t" (click)="cast(-1)">▼</button>
300
+ @if (error(); as message) {
301
+ <span class="text-danger small spark-vote-error" role="alert">{{ message }}</span>
302
+ }
303
+ </span>
304
+ }
305
+ `
306
+ }]
307
+ }],
308
+ ctorParameters: () => [],
309
+ propDecorators: {
310
+ value: [{
311
+ type: i0.Input,
312
+ args: [{
313
+ isSignal: true,
314
+ alias: "value",
315
+ required: false
316
+ }]
317
+ }],
318
+ attribute: [{
319
+ type: i0.Input,
320
+ args: [{
321
+ isSignal: true,
322
+ alias: "attribute",
323
+ required: false
324
+ }]
325
+ }],
326
+ column: [{
327
+ type: i0.Input,
328
+ args: [{
329
+ isSignal: true,
330
+ alias: "column",
331
+ required: false
332
+ }]
333
+ }],
334
+ options: [{
335
+ type: i0.Input,
336
+ args: [{
337
+ isSignal: true,
338
+ alias: "options",
339
+ required: false
340
+ }]
341
+ }],
342
+ item: [{
343
+ type: i0.Input,
344
+ args: [{
345
+ isSignal: true,
346
+ alias: "item",
347
+ required: false
348
+ }]
349
+ }]
350
+ }
351
+ });
352
+ function describeModerationError(e) {
353
+ const err = e;
354
+ const body = err?.error;
355
+ const first = body?.result?.errors?.[0]?.errorMessage;
356
+ if (typeof first === "string") return first;
357
+ if (first && typeof first === "object") {
358
+ const values = Object.values(first);
359
+ if (values.length) return values[0];
360
+ }
361
+ return body?.result?.error ?? err?.message ?? "Failed";
362
+ }
363
+ var SparkFlagButtonComponent = class SparkFlagButtonComponent {
364
+ moderation = inject(SparkModerationService);
365
+ lang = inject(SparkLanguageService);
366
+ context = input.required(...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
367
+ busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
368
+ done = signal(false, ...ngDevMode ? [{ debugName: "done" }] : /* istanbul ignore next */ []);
369
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
370
+ async flag() {
371
+ const reason = prompt(this.lang.t("moderation.flagReason"));
372
+ if (!reason?.trim()) return;
373
+ const ctx = this.context();
374
+ this.busy.set(true);
375
+ this.error.set(null);
376
+ try {
377
+ await this.moderation.flag(ctx.type, ctx.id, reason.trim());
378
+ this.done.set(true);
379
+ } catch (e) {
380
+ this.error.set(describeModerationError(e));
381
+ } finally {
382
+ this.busy.set(false);
383
+ }
384
+ }
385
+ static ɵfac = i0.ɵɵngDeclareFactory({
386
+ minVersion: "12.0.0",
387
+ version: "22.2.0",
388
+ ngImport: i0,
389
+ type: SparkFlagButtonComponent,
390
+ deps: [],
391
+ target: i0.ɵɵFactoryTarget.Component
392
+ });
393
+ static ɵcmp = i0.ɵɵngDeclareComponent({
394
+ minVersion: "17.0.0",
395
+ version: "22.2.0",
396
+ type: SparkFlagButtonComponent,
397
+ isStandalone: true,
398
+ selector: "spark-flag-button",
399
+ inputs: { context: {
400
+ classPropertyName: "context",
401
+ publicName: "context",
402
+ isSignal: true,
403
+ isRequired: true,
404
+ transformFunction: null
405
+ } },
406
+ ngImport: i0,
407
+ template: `
408
+ @if (context().deleted !== 'only') {
409
+ <span class="d-inline-flex gap-2 align-items-center">
410
+ <button type="button" class="btn btn-outline-warning spark-flag" [disabled]="busy() || done()" (click)="flag()">
411
+ {{ 'moderation.flag' | t }}
412
+ </button>
413
+ @if (done()) {
414
+ <span class="small text-success spark-flag-done">{{ 'moderation.flagged' | t }}</span>
415
+ }
416
+ @if (error(); as message) {
417
+ <span class="small text-danger spark-flag-error" role="alert">{{ message }}</span>
418
+ }
419
+ </span>
420
+ }
421
+ `,
422
+ isInline: true,
423
+ dependencies: [{
424
+ kind: "pipe",
425
+ type: TranslateKeyPipe,
426
+ name: "t"
427
+ }],
428
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
429
+ });
430
+ };
431
+ i0.ɵɵngDeclareClassMetadata({
432
+ minVersion: "12.0.0",
433
+ version: "22.2.0",
434
+ ngImport: i0,
435
+ type: SparkFlagButtonComponent,
436
+ decorators: [{
437
+ type: Component,
438
+ args: [{
439
+ selector: "spark-flag-button",
440
+ imports: [TranslateKeyPipe],
441
+ changeDetection: ChangeDetectionStrategy.OnPush,
442
+ template: `
443
+ @if (context().deleted !== 'only') {
444
+ <span class="d-inline-flex gap-2 align-items-center">
445
+ <button type="button" class="btn btn-outline-warning spark-flag" [disabled]="busy() || done()" (click)="flag()">
446
+ {{ 'moderation.flag' | t }}
447
+ </button>
448
+ @if (done()) {
449
+ <span class="small text-success spark-flag-done">{{ 'moderation.flagged' | t }}</span>
450
+ }
451
+ @if (error(); as message) {
452
+ <span class="small text-danger spark-flag-error" role="alert">{{ message }}</span>
453
+ }
454
+ </span>
455
+ }
456
+ `
457
+ }]
458
+ }],
459
+ propDecorators: { context: [{
460
+ type: i0.Input,
461
+ args: [{
462
+ isSignal: true,
463
+ alias: "context",
464
+ required: true
465
+ }]
466
+ }] }
467
+ });
468
+ var SparkModeratorPanelComponent = class SparkModeratorPanelComponent {
469
+ moderation = inject(SparkModerationService);
470
+ lang = inject(SparkLanguageService);
471
+ reviewPath = inject(SPARK_MODERATION_REVIEW_PATH);
472
+ context = input.required(...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
473
+ status = signal(null, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
474
+ busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
475
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
476
+ constructor() {
477
+ effect(() => {
478
+ const ctx = this.context();
479
+ if (ctx.deleted === "only") {
480
+ this.status.set(null);
481
+ return;
482
+ }
483
+ this.moderation.status(ctx.type, ctx.id).then((s) => this.status.set(s), () => this.status.set(null));
484
+ });
485
+ }
486
+ async lock() {
487
+ const reason = prompt(this.lang.t("moderation.lockReason")) ?? void 0;
488
+ await this.run(() => this.moderation.lock(this.context().type, this.context().id, reason));
489
+ }
490
+ async unlock() {
491
+ await this.run(() => this.moderation.unlock(this.context().type, this.context().id));
492
+ }
493
+ async suspendAuthor(userId) {
494
+ const answer = prompt(this.lang.t("moderation.suspendDays"));
495
+ if (answer === null) return;
496
+ const days = answer.trim() === "" ? null : Number(answer);
497
+ if (days !== null && !(days > 0)) return;
498
+ await this.run(() => this.moderation.suspend(userId, days));
499
+ }
500
+ async run(work) {
501
+ this.busy.set(true);
502
+ this.error.set(null);
503
+ try {
504
+ await work();
505
+ const ctx = this.context();
506
+ this.status.set(await this.moderation.status(ctx.type, ctx.id));
507
+ await ctx.reload();
508
+ } catch (e) {
509
+ this.error.set(describeModerationError(e));
510
+ } finally {
511
+ this.busy.set(false);
512
+ }
513
+ }
514
+ static ɵfac = i0.ɵɵngDeclareFactory({
515
+ minVersion: "12.0.0",
516
+ version: "22.2.0",
517
+ ngImport: i0,
518
+ type: SparkModeratorPanelComponent,
519
+ deps: [],
520
+ target: i0.ɵɵFactoryTarget.Component
521
+ });
522
+ static ɵcmp = i0.ɵɵngDeclareComponent({
523
+ minVersion: "17.0.0",
524
+ version: "22.2.0",
525
+ type: SparkModeratorPanelComponent,
526
+ isStandalone: true,
527
+ selector: "spark-moderator-panel",
528
+ inputs: { context: {
529
+ classPropertyName: "context",
530
+ publicName: "context",
531
+ isSignal: true,
532
+ isRequired: true,
533
+ transformFunction: null
534
+ } },
535
+ ngImport: i0,
536
+ template: `
537
+ @if (status(); as s) {
538
+ @if (s.canLock || s.canReview || s.canSuspend) {
539
+ <bs-card class="d-block spark-moderator-panel">
540
+ <bs-card-header>{{ 'moderation.panel' | t }}</bs-card-header>
541
+ <div class="p-3 d-flex flex-wrap gap-2 align-items-center">
542
+ @if (s.locked) {
543
+ <span class="badge bg-secondary spark-moderation-locked">{{ 'moderation.locked' | t }}</span>
544
+ @if (s.lockReason) { <span class="small text-muted">{{ s.lockReason }}</span> }
545
+ }
546
+ @if (s.canLock) {
547
+ @if (s.locked) {
548
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-unlock" [disabled]="busy()" (click)="unlock()">{{ 'moderation.unlock' | t }}</button>
549
+ } @else {
550
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-lock" [disabled]="busy()" (click)="lock()">{{ 'moderation.lock' | t }}</button>
551
+ }
552
+ }
553
+ @if (s.canReview && s.openFlags > 0) {
554
+ <a class="btn btn-sm btn-outline-warning spark-open-flags" [routerLink]="reviewPath" [queryParams]="{ case: s.openCaseId }">
555
+ {{ 'moderation.openFlags' | t }}: {{ s.openFlags }}
556
+ </a>
557
+ }
558
+ @if (s.canSuspend && s.authorId) {
559
+ <button type="button" class="btn btn-sm btn-outline-danger spark-suspend-author" [disabled]="busy()" (click)="suspendAuthor(s.authorId)">{{ 'moderation.suspendAuthor' | t }}</button>
560
+ }
561
+ @if (error(); as message) {
562
+ <span class="small text-danger" role="alert">{{ message }}</span>
563
+ }
564
+ </div>
565
+ </bs-card>
566
+ }
567
+ }
568
+ `,
569
+ isInline: true,
570
+ dependencies: [
571
+ {
572
+ kind: "component",
573
+ type: BsCardComponent,
574
+ selector: "bs-card",
575
+ inputs: ["color", "outline"]
576
+ },
577
+ {
578
+ kind: "component",
579
+ type: BsCardHeaderComponent,
580
+ selector: "bs-card-header",
581
+ inputs: ["color", "navStyle"]
582
+ },
583
+ {
584
+ kind: "directive",
585
+ type: RouterLink,
586
+ selector: "[routerLink]",
587
+ inputs: [
588
+ "target",
589
+ "queryParams",
590
+ "fragment",
591
+ "queryParamsHandling",
592
+ "state",
593
+ "info",
594
+ "relativeTo",
595
+ "preserveFragment",
596
+ "skipLocationChange",
597
+ "replaceUrl",
598
+ "browserUrl",
599
+ "routerLink"
600
+ ]
601
+ },
602
+ {
603
+ kind: "pipe",
604
+ type: TranslateKeyPipe,
605
+ name: "t"
606
+ }
607
+ ],
608
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
609
+ });
610
+ };
611
+ i0.ɵɵngDeclareClassMetadata({
612
+ minVersion: "12.0.0",
613
+ version: "22.2.0",
614
+ ngImport: i0,
615
+ type: SparkModeratorPanelComponent,
616
+ decorators: [{
617
+ type: Component,
618
+ args: [{
619
+ selector: "spark-moderator-panel",
620
+ imports: [
621
+ BsCardComponent,
622
+ BsCardHeaderComponent,
623
+ TranslateKeyPipe,
624
+ RouterLink
625
+ ],
626
+ changeDetection: ChangeDetectionStrategy.OnPush,
627
+ template: `
628
+ @if (status(); as s) {
629
+ @if (s.canLock || s.canReview || s.canSuspend) {
630
+ <bs-card class="d-block spark-moderator-panel">
631
+ <bs-card-header>{{ 'moderation.panel' | t }}</bs-card-header>
632
+ <div class="p-3 d-flex flex-wrap gap-2 align-items-center">
633
+ @if (s.locked) {
634
+ <span class="badge bg-secondary spark-moderation-locked">{{ 'moderation.locked' | t }}</span>
635
+ @if (s.lockReason) { <span class="small text-muted">{{ s.lockReason }}</span> }
636
+ }
637
+ @if (s.canLock) {
638
+ @if (s.locked) {
639
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-unlock" [disabled]="busy()" (click)="unlock()">{{ 'moderation.unlock' | t }}</button>
640
+ } @else {
641
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-lock" [disabled]="busy()" (click)="lock()">{{ 'moderation.lock' | t }}</button>
642
+ }
643
+ }
644
+ @if (s.canReview && s.openFlags > 0) {
645
+ <a class="btn btn-sm btn-outline-warning spark-open-flags" [routerLink]="reviewPath" [queryParams]="{ case: s.openCaseId }">
646
+ {{ 'moderation.openFlags' | t }}: {{ s.openFlags }}
647
+ </a>
648
+ }
649
+ @if (s.canSuspend && s.authorId) {
650
+ <button type="button" class="btn btn-sm btn-outline-danger spark-suspend-author" [disabled]="busy()" (click)="suspendAuthor(s.authorId)">{{ 'moderation.suspendAuthor' | t }}</button>
651
+ }
652
+ @if (error(); as message) {
653
+ <span class="small text-danger" role="alert">{{ message }}</span>
654
+ }
655
+ </div>
656
+ </bs-card>
657
+ }
658
+ }
659
+ `
660
+ }]
661
+ }],
662
+ ctorParameters: () => [],
663
+ propDecorators: { context: [{
664
+ type: i0.Input,
665
+ args: [{
666
+ isSignal: true,
667
+ alias: "context",
668
+ required: true
669
+ }]
670
+ }] }
671
+ });
672
+ var SparkReputationBadgeComponent = class SparkReputationBadgeComponent {
673
+ moderation = inject(SparkModerationService);
674
+ userId = input(...ngDevMode ? [void 0, { debugName: "userId" }] : /* istanbul ignore next */ []);
675
+ reputation = signal(null, ...ngDevMode ? [{ debugName: "reputation" }] : /* istanbul ignore next */ []);
676
+ constructor() {
677
+ effect(() => {
678
+ const userId = this.userId() ?? void 0;
679
+ (userId ? this.moderation.reputation(userId) : (this.moderation.ownReputationChanged(), this.moderation.ownReputation())).then((r) => this.reputation.set(r), () => this.reputation.set(null));
680
+ });
681
+ }
682
+ static ɵfac = i0.ɵɵngDeclareFactory({
683
+ minVersion: "12.0.0",
684
+ version: "22.2.0",
685
+ ngImport: i0,
686
+ type: SparkReputationBadgeComponent,
687
+ deps: [],
688
+ target: i0.ɵɵFactoryTarget.Component
689
+ });
690
+ static ɵcmp = i0.ɵɵngDeclareComponent({
691
+ minVersion: "17.0.0",
692
+ version: "22.2.0",
693
+ type: SparkReputationBadgeComponent,
694
+ isStandalone: true,
695
+ selector: "spark-reputation-badge",
696
+ inputs: { userId: {
697
+ classPropertyName: "userId",
698
+ publicName: "userId",
699
+ isSignal: true,
700
+ isRequired: false,
701
+ transformFunction: null
702
+ } },
703
+ ngImport: i0,
704
+ template: `
705
+ @if (reputation(); as r) {
706
+ <span class="badge bg-light text-dark border spark-reputation-badge" [attr.title]="'moderation.reputation' | t">
707
+ {{ r.total }}
708
+ @if (!userId() && r.pending) {
709
+ <!-- A received downvote makes the pending part negative: "(-2 pending)", never "(+-2 pending)". -->
710
+ <span class="text-muted spark-reputation-pending">({{ r.pending > 0 ? '+' : '' }}{{ r.pending }} {{ 'moderation.pending' | t }})</span>
711
+ }
712
+ </span>
713
+ }
714
+ `,
715
+ isInline: true,
716
+ dependencies: [{
717
+ kind: "pipe",
718
+ type: TranslateKeyPipe,
719
+ name: "t"
720
+ }],
721
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
722
+ });
723
+ };
724
+ i0.ɵɵngDeclareClassMetadata({
725
+ minVersion: "12.0.0",
726
+ version: "22.2.0",
727
+ ngImport: i0,
728
+ type: SparkReputationBadgeComponent,
729
+ decorators: [{
730
+ type: Component,
731
+ args: [{
732
+ selector: "spark-reputation-badge",
733
+ imports: [TranslateKeyPipe],
734
+ changeDetection: ChangeDetectionStrategy.OnPush,
735
+ template: `
736
+ @if (reputation(); as r) {
737
+ <span class="badge bg-light text-dark border spark-reputation-badge" [attr.title]="'moderation.reputation' | t">
738
+ {{ r.total }}
739
+ @if (!userId() && r.pending) {
740
+ <!-- A received downvote makes the pending part negative: "(-2 pending)", never "(+-2 pending)". -->
741
+ <span class="text-muted spark-reputation-pending">({{ r.pending > 0 ? '+' : '' }}{{ r.pending }} {{ 'moderation.pending' | t }})</span>
742
+ }
743
+ </span>
744
+ }
745
+ `
746
+ }]
747
+ }],
748
+ ctorParameters: () => [],
749
+ propDecorators: { userId: [{
750
+ type: i0.Input,
751
+ args: [{
752
+ isSignal: true,
753
+ alias: "userId",
754
+ required: false
755
+ }]
756
+ }] }
757
+ });
758
+ var SparkReviewQueueComponent = class SparkReviewQueueComponent {
759
+ moderation = inject(SparkModerationService);
760
+ route = inject(ActivatedRoute);
761
+ router = inject(Router);
762
+ spark = inject(SparkService);
763
+ cases = signal([], ...ngDevMode ? [{ debugName: "cases" }] : /* istanbul ignore next */ []);
764
+ targets = signal({}, ...ngDevMode ? [{ debugName: "targets" }] : /* istanbul ignore next */ []);
765
+ selected = signal(null, ...ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []);
766
+ busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
767
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
768
+ async ngOnInit() {
769
+ await this.load();
770
+ const requested = this.route.snapshot.queryParamMap.get("case");
771
+ if (requested) await this.open(requested);
772
+ }
773
+ async open(caseId) {
774
+ await this.run(async () => {
775
+ this.selected.set(await this.moderation.case(caseId));
776
+ await this.router.navigate([], {
777
+ relativeTo: this.route,
778
+ queryParams: { case: caseId },
779
+ replaceUrl: true
780
+ });
781
+ });
782
+ }
783
+ async decide(decision, accountId) {
784
+ const current = this.selected();
785
+ if (!current) return;
786
+ await this.run(async () => {
787
+ await this.moderation.decide(current.case.id, decision, accountId);
788
+ this.selected.set(await this.moderation.case(current.case.id));
789
+ await this.load();
790
+ });
791
+ }
792
+ async load() {
793
+ await this.run(async () => this.cases.set(await this.moderation.cases("open")));
794
+ this.resolveTargets(this.cases());
795
+ }
796
+ async resolveTargets(cases) {
797
+ const pending = cases.filter((c) => c.targetId && c.targetType && !(c.id in this.targets()));
798
+ if (!pending.length) return;
799
+ let types = [];
800
+ try {
801
+ types = await this.spark.getEntityTypes();
802
+ } catch {
803
+ return;
804
+ }
805
+ await Promise.all(pending.map(async (c) => {
806
+ const type = types.find((t) => t.name === c.targetType || t.id === c.targetType || t.alias === c.targetType);
807
+ const route = type ? type.alias || type.id : null;
808
+ let title = null;
809
+ if (route) try {
810
+ title = (await this.spark.get(route, c.targetId)).breadcrumb || null;
811
+ } catch {}
812
+ const link = route && title ? [
813
+ "/po",
814
+ route,
815
+ c.targetId
816
+ ] : null;
817
+ this.targets.update((t) => ({
818
+ ...t,
819
+ [c.id]: {
820
+ title,
821
+ link
822
+ }
823
+ }));
824
+ }));
825
+ }
826
+ async run(work) {
827
+ this.busy.set(true);
828
+ this.error.set(null);
829
+ try {
830
+ await work();
831
+ } catch (e) {
832
+ this.error.set(describeModerationError(e));
833
+ } finally {
834
+ this.busy.set(false);
835
+ }
836
+ }
837
+ static ɵfac = i0.ɵɵngDeclareFactory({
838
+ minVersion: "12.0.0",
839
+ version: "22.2.0",
840
+ ngImport: i0,
841
+ type: SparkReviewQueueComponent,
842
+ deps: [],
843
+ target: i0.ɵɵFactoryTarget.Component
844
+ });
845
+ static ɵcmp = i0.ɵɵngDeclareComponent({
846
+ minVersion: "17.0.0",
847
+ version: "22.2.0",
848
+ type: SparkReviewQueueComponent,
849
+ isStandalone: true,
850
+ selector: "spark-review-queue",
851
+ ngImport: i0,
852
+ template: `
853
+ <div class="container-fluid spark-review-queue">
854
+ <h1 class="h3">{{ 'moderation.reviewQueue' | t }}</h1>
855
+ @if (error(); as message) { <div class="alert alert-danger" role="alert">{{ message }}</div> }
856
+ <div class="row g-3">
857
+ <div class="col-md-4">
858
+ @for (c of cases(); track c.id) {
859
+ <button type="button" class="list-group-item list-group-item-action w-100 text-start p-2 border mb-1 spark-case"
860
+ [class.active]="selected()?.case?.id === c.id" (click)="open(c.id)">
861
+ <strong>{{ c.kind }}</strong>
862
+ @if (c.flagCount) { <span class="badge bg-warning text-dark ms-1">{{ c.flagCount }}</span> }
863
+ @if (targets()[c.id]?.title; as title) { <div class="small fw-semibold text-truncate spark-case-target">{{ title }}</div> }
864
+ <div class="small">{{ c.summary }}</div>
865
+ <div class="small text-muted">{{ c.openedAtUtc | date: 'short' }}</div>
866
+ </button>
867
+ } @empty {
868
+ <p class="text-muted spark-no-cases">{{ 'moderation.noCases' | t }}</p>
869
+ }
870
+ </div>
871
+ <div class="col-md-8">
872
+ @if (selected(); as d) {
873
+ <bs-card class="d-block spark-case-detail">
874
+ <bs-card-header>{{ 'moderation.rule' | t }}: {{ d.case.kind }} — {{ d.case.summary }}</bs-card-header>
875
+ <div class="p-3">
876
+ @if (targets()[d.case.id]; as target) {
877
+ <p class="spark-case-target-link">
878
+ @if (target.link) { <a [routerLink]="target.link">{{ target.title ?? d.case.targetId }}</a> }
879
+ @else { {{ target.title ?? d.case.targetId }} }
880
+ </p>
881
+ }
882
+ @if (d.accounts.length) {
883
+ <h2 class="h6">{{ 'moderation.accounts' | t }}</h2>
884
+ <table class="table table-sm spark-case-accounts">
885
+ <thead><tr><th></th><th>{{ 'moderation.ageDays' | t }}</th><th></th><th>{{ 'moderation.sharesNetwork' | t }}</th><th>{{ 'moderation.repBefore' | t }}</th><th>{{ 'moderation.repAfter' | t }}</th></tr></thead>
886
+ <tbody>
887
+ @for (a of d.accounts; track a.id) {
888
+ <tr><td>{{ a.id }}</td><td>{{ a.ageDays }}</td><td>{{ a.registrationMethod }}</td><td>{{ a.sharesNetworkWith }}</td><td>{{ a.reputationBefore }}</td><td>{{ a.reputationAfter }}</td></tr>
889
+ }
890
+ </tbody>
891
+ </table>
892
+ }
893
+ @if (d.matrix.length) {
894
+ <h2 class="h6">{{ 'moderation.matrix' | t }}</h2>
895
+ <ul class="small spark-case-matrix">
896
+ @for (m of d.matrix; track m.voterId + m.authorId) { <li>{{ m.voterId }} → {{ m.authorId }}: {{ m.votes }}</li> }
897
+ </ul>
898
+ }
899
+ @if (d.timeline.length) {
900
+ <h2 class="h6">{{ 'moderation.timeline' | t }}</h2>
901
+ <table class="table table-sm small spark-case-timeline">
902
+ <tbody>
903
+ @for (t of d.timeline; track t.voteId) {
904
+ <tr><td>{{ t.castAtUtc | date: 'short' }}</td><td>{{ t.voterId }} → {{ t.authorId }}</td><td>{{ t.direction > 0 ? '▲' : t.direction < 0 ? '▼' : '·' }}</td><td>{{ t.secondsAfterPost ?? '' }} s</td></tr>
905
+ }
906
+ </tbody>
907
+ </table>
908
+ }
909
+ @if (d.flags.length) {
910
+ <ul class="small spark-case-flags">
911
+ @for (f of d.flags; track f.flaggerId) { <li>{{ f.reason }} <span class="text-muted">({{ f.raisedAtUtc | date: 'short' }})</span></li> }
912
+ </ul>
913
+ }
914
+ @if (d.case.status === 'open') {
915
+ <div class="d-flex flex-wrap gap-2">
916
+ @if (d.case.kind === 'flag') {
917
+ <button type="button" class="btn btn-sm btn-success spark-decide-uphold" [disabled]="busy()" (click)="decide('uphold')">{{ 'moderation.uphold' | t }}</button>
918
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-decide-decline" [disabled]="busy()" (click)="decide('decline')">{{ 'moderation.decline' | t }}</button>
919
+ } @else {
920
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-decide-dismiss" [disabled]="busy()" (click)="decide('dismiss')">{{ 'moderation.dismiss' | t }}</button>
921
+ <button type="button" class="btn btn-sm btn-warning spark-decide-reverse" [disabled]="busy()" (click)="decide('reverse')">{{ 'moderation.reverse' | t }}</button>
922
+ @for (a of d.case.accountIds; track a) {
923
+ <button type="button" class="btn btn-sm btn-outline-danger spark-decide-merge" [disabled]="busy()" (click)="decide('merge', a)">{{ 'moderation.merge' | t }}: {{ a }}</button>
924
+ <button type="button" class="btn btn-sm btn-danger spark-decide-suspend" [disabled]="busy()" (click)="decide('suspend', a)">{{ 'moderation.suspend' | t }}: {{ a }}</button>
925
+ }
926
+ }
927
+ </div>
928
+ }
929
+ </div>
930
+ </bs-card>
931
+ }
932
+ </div>
933
+ </div>
934
+ </div>
935
+ `,
936
+ isInline: true,
937
+ dependencies: [
938
+ {
939
+ kind: "component",
940
+ type: BsCardComponent,
941
+ selector: "bs-card",
942
+ inputs: ["color", "outline"]
943
+ },
944
+ {
945
+ kind: "component",
946
+ type: BsCardHeaderComponent,
947
+ selector: "bs-card-header",
948
+ inputs: ["color", "navStyle"]
949
+ },
950
+ {
951
+ kind: "directive",
952
+ type: RouterLink,
953
+ selector: "[routerLink]",
954
+ inputs: [
955
+ "target",
956
+ "queryParams",
957
+ "fragment",
958
+ "queryParamsHandling",
959
+ "state",
960
+ "info",
961
+ "relativeTo",
962
+ "preserveFragment",
963
+ "skipLocationChange",
964
+ "replaceUrl",
965
+ "browserUrl",
966
+ "routerLink"
967
+ ]
968
+ },
969
+ {
970
+ kind: "pipe",
971
+ type: TranslateKeyPipe,
972
+ name: "t"
973
+ },
974
+ {
975
+ kind: "pipe",
976
+ type: DatePipe,
977
+ name: "date"
978
+ }
979
+ ],
980
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
981
+ });
982
+ };
983
+ i0.ɵɵngDeclareClassMetadata({
984
+ minVersion: "12.0.0",
985
+ version: "22.2.0",
986
+ ngImport: i0,
987
+ type: SparkReviewQueueComponent,
988
+ decorators: [{
989
+ type: Component,
990
+ args: [{
991
+ selector: "spark-review-queue",
992
+ imports: [
993
+ BsCardComponent,
994
+ BsCardHeaderComponent,
995
+ RouterLink,
996
+ TranslateKeyPipe,
997
+ DatePipe
998
+ ],
999
+ changeDetection: ChangeDetectionStrategy.OnPush,
1000
+ template: `
1001
+ <div class="container-fluid spark-review-queue">
1002
+ <h1 class="h3">{{ 'moderation.reviewQueue' | t }}</h1>
1003
+ @if (error(); as message) { <div class="alert alert-danger" role="alert">{{ message }}</div> }
1004
+ <div class="row g-3">
1005
+ <div class="col-md-4">
1006
+ @for (c of cases(); track c.id) {
1007
+ <button type="button" class="list-group-item list-group-item-action w-100 text-start p-2 border mb-1 spark-case"
1008
+ [class.active]="selected()?.case?.id === c.id" (click)="open(c.id)">
1009
+ <strong>{{ c.kind }}</strong>
1010
+ @if (c.flagCount) { <span class="badge bg-warning text-dark ms-1">{{ c.flagCount }}</span> }
1011
+ @if (targets()[c.id]?.title; as title) { <div class="small fw-semibold text-truncate spark-case-target">{{ title }}</div> }
1012
+ <div class="small">{{ c.summary }}</div>
1013
+ <div class="small text-muted">{{ c.openedAtUtc | date: 'short' }}</div>
1014
+ </button>
1015
+ } @empty {
1016
+ <p class="text-muted spark-no-cases">{{ 'moderation.noCases' | t }}</p>
1017
+ }
1018
+ </div>
1019
+ <div class="col-md-8">
1020
+ @if (selected(); as d) {
1021
+ <bs-card class="d-block spark-case-detail">
1022
+ <bs-card-header>{{ 'moderation.rule' | t }}: {{ d.case.kind }} — {{ d.case.summary }}</bs-card-header>
1023
+ <div class="p-3">
1024
+ @if (targets()[d.case.id]; as target) {
1025
+ <p class="spark-case-target-link">
1026
+ @if (target.link) { <a [routerLink]="target.link">{{ target.title ?? d.case.targetId }}</a> }
1027
+ @else { {{ target.title ?? d.case.targetId }} }
1028
+ </p>
1029
+ }
1030
+ @if (d.accounts.length) {
1031
+ <h2 class="h6">{{ 'moderation.accounts' | t }}</h2>
1032
+ <table class="table table-sm spark-case-accounts">
1033
+ <thead><tr><th></th><th>{{ 'moderation.ageDays' | t }}</th><th></th><th>{{ 'moderation.sharesNetwork' | t }}</th><th>{{ 'moderation.repBefore' | t }}</th><th>{{ 'moderation.repAfter' | t }}</th></tr></thead>
1034
+ <tbody>
1035
+ @for (a of d.accounts; track a.id) {
1036
+ <tr><td>{{ a.id }}</td><td>{{ a.ageDays }}</td><td>{{ a.registrationMethod }}</td><td>{{ a.sharesNetworkWith }}</td><td>{{ a.reputationBefore }}</td><td>{{ a.reputationAfter }}</td></tr>
1037
+ }
1038
+ </tbody>
1039
+ </table>
1040
+ }
1041
+ @if (d.matrix.length) {
1042
+ <h2 class="h6">{{ 'moderation.matrix' | t }}</h2>
1043
+ <ul class="small spark-case-matrix">
1044
+ @for (m of d.matrix; track m.voterId + m.authorId) { <li>{{ m.voterId }} → {{ m.authorId }}: {{ m.votes }}</li> }
1045
+ </ul>
1046
+ }
1047
+ @if (d.timeline.length) {
1048
+ <h2 class="h6">{{ 'moderation.timeline' | t }}</h2>
1049
+ <table class="table table-sm small spark-case-timeline">
1050
+ <tbody>
1051
+ @for (t of d.timeline; track t.voteId) {
1052
+ <tr><td>{{ t.castAtUtc | date: 'short' }}</td><td>{{ t.voterId }} → {{ t.authorId }}</td><td>{{ t.direction > 0 ? '▲' : t.direction < 0 ? '▼' : '·' }}</td><td>{{ t.secondsAfterPost ?? '' }} s</td></tr>
1053
+ }
1054
+ </tbody>
1055
+ </table>
1056
+ }
1057
+ @if (d.flags.length) {
1058
+ <ul class="small spark-case-flags">
1059
+ @for (f of d.flags; track f.flaggerId) { <li>{{ f.reason }} <span class="text-muted">({{ f.raisedAtUtc | date: 'short' }})</span></li> }
1060
+ </ul>
1061
+ }
1062
+ @if (d.case.status === 'open') {
1063
+ <div class="d-flex flex-wrap gap-2">
1064
+ @if (d.case.kind === 'flag') {
1065
+ <button type="button" class="btn btn-sm btn-success spark-decide-uphold" [disabled]="busy()" (click)="decide('uphold')">{{ 'moderation.uphold' | t }}</button>
1066
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-decide-decline" [disabled]="busy()" (click)="decide('decline')">{{ 'moderation.decline' | t }}</button>
1067
+ } @else {
1068
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-decide-dismiss" [disabled]="busy()" (click)="decide('dismiss')">{{ 'moderation.dismiss' | t }}</button>
1069
+ <button type="button" class="btn btn-sm btn-warning spark-decide-reverse" [disabled]="busy()" (click)="decide('reverse')">{{ 'moderation.reverse' | t }}</button>
1070
+ @for (a of d.case.accountIds; track a) {
1071
+ <button type="button" class="btn btn-sm btn-outline-danger spark-decide-merge" [disabled]="busy()" (click)="decide('merge', a)">{{ 'moderation.merge' | t }}: {{ a }}</button>
1072
+ <button type="button" class="btn btn-sm btn-danger spark-decide-suspend" [disabled]="busy()" (click)="decide('suspend', a)">{{ 'moderation.suspend' | t }}: {{ a }}</button>
1073
+ }
1074
+ }
1075
+ </div>
1076
+ }
1077
+ </div>
1078
+ </bs-card>
1079
+ }
1080
+ </div>
1081
+ </div>
1082
+ </div>
1083
+ `
1084
+ }]
1085
+ }]
1086
+ });
1087
+ var SparkReviewQueueLinkComponent = class SparkReviewQueueLinkComponent {
1088
+ moderation = inject(SparkModerationService);
1089
+ linkClass = input("", ...ngDevMode ? [{ debugName: "linkClass" }] : /* istanbul ignore next */ []);
1090
+ canReview = signal(false, ...ngDevMode ? [{ debugName: "canReview" }] : /* istanbul ignore next */ []);
1091
+ constructor() {
1092
+ effect(() => {
1093
+ this.moderation.ownReputationChanged();
1094
+ this.moderation.ownReputation().then((r) => this.canReview.set(r.canReview === true), () => this.canReview.set(false));
1095
+ });
1096
+ }
1097
+ static ɵfac = i0.ɵɵngDeclareFactory({
1098
+ minVersion: "12.0.0",
1099
+ version: "22.2.0",
1100
+ ngImport: i0,
1101
+ type: SparkReviewQueueLinkComponent,
1102
+ deps: [],
1103
+ target: i0.ɵɵFactoryTarget.Component
1104
+ });
1105
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1106
+ minVersion: "17.0.0",
1107
+ version: "22.2.0",
1108
+ type: SparkReviewQueueLinkComponent,
1109
+ isStandalone: true,
1110
+ selector: "spark-review-queue-link",
1111
+ inputs: { linkClass: {
1112
+ classPropertyName: "linkClass",
1113
+ publicName: "linkClass",
1114
+ isSignal: true,
1115
+ isRequired: false,
1116
+ transformFunction: null
1117
+ } },
1118
+ host: { properties: { "style.display": "canReview() ? null : \"none\"" } },
1119
+ ngImport: i0,
1120
+ template: `
1121
+ @if (canReview()) {
1122
+ <a class="spark-review-queue-link" [class]="linkClass()" routerLink="/moderation/review">{{ 'moderation.reviewQueue' | t }}</a>
1123
+ }
1124
+ `,
1125
+ isInline: true,
1126
+ dependencies: [{
1127
+ kind: "directive",
1128
+ type: RouterLink,
1129
+ selector: "[routerLink]",
1130
+ inputs: [
1131
+ "target",
1132
+ "queryParams",
1133
+ "fragment",
1134
+ "queryParamsHandling",
1135
+ "state",
1136
+ "info",
1137
+ "relativeTo",
1138
+ "preserveFragment",
1139
+ "skipLocationChange",
1140
+ "replaceUrl",
1141
+ "browserUrl",
1142
+ "routerLink"
1143
+ ]
1144
+ }, {
1145
+ kind: "pipe",
1146
+ type: TranslateKeyPipe,
1147
+ name: "t"
1148
+ }],
1149
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
1150
+ });
1151
+ };
1152
+ i0.ɵɵngDeclareClassMetadata({
1153
+ minVersion: "12.0.0",
1154
+ version: "22.2.0",
1155
+ ngImport: i0,
1156
+ type: SparkReviewQueueLinkComponent,
1157
+ decorators: [{
1158
+ type: Component,
1159
+ args: [{
1160
+ selector: "spark-review-queue-link",
1161
+ imports: [RouterLink, TranslateKeyPipe],
1162
+ changeDetection: ChangeDetectionStrategy.OnPush,
1163
+ host: { "[style.display]": "canReview() ? null : \"none\"" },
1164
+ template: `
1165
+ @if (canReview()) {
1166
+ <a class="spark-review-queue-link" [class]="linkClass()" routerLink="/moderation/review">{{ 'moderation.reviewQueue' | t }}</a>
1167
+ }
1168
+ `
1169
+ }]
1170
+ }],
1171
+ ctorParameters: () => [],
1172
+ propDecorators: { linkClass: [{
1173
+ type: i0.Input,
1174
+ args: [{
1175
+ isSignal: true,
1176
+ alias: "linkClass",
1177
+ required: false
1178
+ }]
1179
+ }] }
1180
+ });
1181
+ function provideSparkModeration(options) {
1182
+ return [
1183
+ ...provideSparkDetailActions({
1184
+ id: "spark-moderation-flag",
1185
+ component: SparkFlagButtonComponent,
1186
+ priority: 90
1187
+ }),
1188
+ ...provideSparkDetailPanels({
1189
+ id: "spark-moderation-panel",
1190
+ component: SparkModeratorPanelComponent,
1191
+ order: options?.panelOrder ?? 90
1192
+ }),
1193
+ {
1194
+ provide: SPARK_MODERATION_REVIEW_PATH,
1195
+ useValue: options?.reviewPath ?? "/moderation/review"
1196
+ }
1197
+ ];
1198
+ }
1199
+ const sparkModerationRenderers = [{
1200
+ name: "spark-vote",
1201
+ detailComponent: SparkVoteComponent,
1202
+ columnComponent: SparkVoteComponent
1203
+ }];
1204
+ function sparkModerationRoutes(path = "moderation/review") {
1205
+ return [{
1206
+ path,
1207
+ component: SparkReviewQueueComponent
1208
+ }];
1209
+ }
1210
+ export { SPARK_MODERATION_REVIEW_PATH, SparkFlagButtonComponent, SparkModerationService, SparkModeratorPanelComponent, SparkReputationBadgeComponent, SparkReviewQueueComponent, SparkReviewQueueLinkComponent, SparkVoteComponent, describeModerationError, provideSparkModeration, sparkModerationRenderers, sparkModerationRoutes };
1211
+
1212
+ //# sourceMappingURL=mintplayer-ng-spark-moderation.mjs.map