@memberjunction/ng-entity-viewer 5.27.1 → 5.29.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 (30) hide show
  1. package/README.md +101 -0
  2. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +14 -1
  3. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  4. package/dist/lib/entity-data-grid/entity-data-grid.component.js +199 -172
  5. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  6. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +9 -1
  7. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  8. package/dist/lib/entity-viewer/entity-viewer.component.js +58 -38
  9. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  10. package/dist/lib/recycle-bin/events/recycle-bin-events.d.ts +91 -0
  11. package/dist/lib/recycle-bin/events/recycle-bin-events.d.ts.map +1 -0
  12. package/dist/lib/recycle-bin/events/recycle-bin-events.js +10 -0
  13. package/dist/lib/recycle-bin/events/recycle-bin-events.js.map +1 -0
  14. package/dist/lib/recycle-bin/recycle-bin-chip.component.d.ts +75 -0
  15. package/dist/lib/recycle-bin/recycle-bin-chip.component.d.ts.map +1 -0
  16. package/dist/lib/recycle-bin/recycle-bin-chip.component.js +228 -0
  17. package/dist/lib/recycle-bin/recycle-bin-chip.component.js.map +1 -0
  18. package/dist/lib/recycle-bin/recycle-bin.component.d.ts +178 -0
  19. package/dist/lib/recycle-bin/recycle-bin.component.d.ts.map +1 -0
  20. package/dist/lib/recycle-bin/recycle-bin.component.js +681 -0
  21. package/dist/lib/recycle-bin/recycle-bin.component.js.map +1 -0
  22. package/dist/module.d.ts +13 -9
  23. package/dist/module.d.ts.map +1 -1
  24. package/dist/module.js +23 -7
  25. package/dist/module.js.map +1 -1
  26. package/dist/public-api.d.ts +3 -0
  27. package/dist/public-api.d.ts.map +1 -1
  28. package/dist/public-api.js +4 -0
  29. package/dist/public-api.js.map +1 -1
  30. package/package.json +13 -11
@@ -0,0 +1,681 @@
1
+ import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output, ViewEncapsulation, } from '@angular/core';
2
+ import { Metadata, RunView, } from '@memberjunction/core';
3
+ import * as i0 from "@angular/core";
4
+ import * as i1 from "@memberjunction/ng-shared-generic";
5
+ import * as i2 from "@memberjunction/ng-record-changes";
6
+ import * as i3 from "@memberjunction/ng-versions";
7
+ const _forTrack0 = ($index, $item) => $item.RecordChange.ID;
8
+ const _forTrack1 = ($index, $item) => $item.Name;
9
+ function RecycleBinComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
10
+ i0.ɵɵelementStart(0, "div", 7);
11
+ i0.ɵɵtext(1);
12
+ i0.ɵɵelementEnd();
13
+ } if (rf & 2) {
14
+ const ctx_r0 = i0.ɵɵnextContext();
15
+ i0.ɵɵadvance();
16
+ i0.ɵɵtextInterpolate(ctx_r0.HeaderSubtitle);
17
+ } }
18
+ function RecycleBinComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
19
+ i0.ɵɵelementStart(0, "div", 8);
20
+ i0.ɵɵelement(1, "i", 14);
21
+ i0.ɵɵelementStart(2, "h3");
22
+ i0.ɵɵtext(3, "Access denied");
23
+ i0.ɵɵelementEnd();
24
+ i0.ɵɵelementStart(4, "p");
25
+ i0.ɵɵtext(5);
26
+ i0.ɵɵelementEnd()();
27
+ } if (rf & 2) {
28
+ const ctx_r0 = i0.ɵɵnextContext();
29
+ i0.ɵɵadvance(5);
30
+ i0.ɵɵtextInterpolate(ctx_r0.LoadError);
31
+ } }
32
+ function RecycleBinComponent_Conditional_10_Template(rf, ctx) { if (rf & 1) {
33
+ i0.ɵɵelement(0, "mj-loading", 9);
34
+ } }
35
+ function RecycleBinComponent_Conditional_11_Template(rf, ctx) { if (rf & 1) {
36
+ i0.ɵɵelementStart(0, "div", 10);
37
+ i0.ɵɵelement(1, "i", 15);
38
+ i0.ɵɵelementStart(2, "h3");
39
+ i0.ɵɵtext(3, "Could not load deleted records");
40
+ i0.ɵɵelementEnd();
41
+ i0.ɵɵelementStart(4, "p");
42
+ i0.ɵɵtext(5);
43
+ i0.ɵɵelementEnd()();
44
+ } if (rf & 2) {
45
+ const ctx_r0 = i0.ɵɵnextContext();
46
+ i0.ɵɵadvance(5);
47
+ i0.ɵɵtextInterpolate(ctx_r0.LoadError);
48
+ } }
49
+ function RecycleBinComponent_Conditional_12_Template(rf, ctx) { if (rf & 1) {
50
+ i0.ɵɵelementStart(0, "div", 11);
51
+ i0.ɵɵelement(1, "i", 16);
52
+ i0.ɵɵelementStart(2, "h3");
53
+ i0.ɵɵtext(3, "Recycle Bin is empty");
54
+ i0.ɵɵelementEnd();
55
+ i0.ɵɵelementStart(4, "p");
56
+ i0.ɵɵtext(5, "No records have been deleted from this entity, or change tracking isn't enabled here.");
57
+ i0.ɵɵelementEnd()();
58
+ } }
59
+ function RecycleBinComponent_Conditional_13_For_2_Conditional_4_For_2_Template(rf, ctx) { if (rf & 1) {
60
+ i0.ɵɵelementStart(0, "span", 26)(1, "strong");
61
+ i0.ɵɵtext(2);
62
+ i0.ɵɵelementEnd();
63
+ i0.ɵɵtext(3);
64
+ i0.ɵɵelementEnd();
65
+ } if (rf & 2) {
66
+ const sf_r3 = ctx.$implicit;
67
+ i0.ɵɵadvance(2);
68
+ i0.ɵɵtextInterpolate1("", sf_r3.DisplayName, ":");
69
+ i0.ɵɵadvance();
70
+ i0.ɵɵtextInterpolate1(" ", sf_r3.Value, " ");
71
+ } }
72
+ function RecycleBinComponent_Conditional_13_For_2_Conditional_4_Template(rf, ctx) { if (rf & 1) {
73
+ i0.ɵɵelementStart(0, "div", 20);
74
+ i0.ɵɵrepeaterCreate(1, RecycleBinComponent_Conditional_13_For_2_Conditional_4_For_2_Template, 4, 2, "span", 26, _forTrack1);
75
+ i0.ɵɵelementEnd();
76
+ } if (rf & 2) {
77
+ const entry_r4 = i0.ɵɵnextContext().$implicit;
78
+ i0.ɵɵadvance();
79
+ i0.ɵɵrepeater(entry_r4.SupportingFields);
80
+ } }
81
+ function RecycleBinComponent_Conditional_13_For_2_Conditional_8_Template(rf, ctx) { if (rf & 1) {
82
+ i0.ɵɵtext(0);
83
+ } if (rf & 2) {
84
+ const entry_r4 = i0.ɵɵnextContext().$implicit;
85
+ const ctx_r0 = i0.ɵɵnextContext(2);
86
+ i0.ɵɵtextInterpolate1(" by ", ctx_r0.getUserDisplay(entry_r4.RecordChange.User), " ");
87
+ } }
88
+ function RecycleBinComponent_Conditional_13_For_2_Template(rf, ctx) { if (rf & 1) {
89
+ const _r2 = i0.ɵɵgetCurrentView();
90
+ i0.ɵɵelementStart(0, "div", 17)(1, "div", 18)(2, "div", 19);
91
+ i0.ɵɵtext(3);
92
+ i0.ɵɵelementEnd();
93
+ i0.ɵɵconditionalCreate(4, RecycleBinComponent_Conditional_13_For_2_Conditional_4_Template, 3, 0, "div", 20);
94
+ i0.ɵɵelementStart(5, "div", 21);
95
+ i0.ɵɵelement(6, "i", 22);
96
+ i0.ɵɵtext(7);
97
+ i0.ɵɵconditionalCreate(8, RecycleBinComponent_Conditional_13_For_2_Conditional_8_Template, 1, 1);
98
+ i0.ɵɵelementEnd()();
99
+ i0.ɵɵelementStart(9, "div", 23)(10, "button", 24);
100
+ i0.ɵɵlistener("click", function RecycleBinComponent_Conditional_13_For_2_Template_button_click_10_listener() { const entry_r4 = i0.ɵɵrestoreView(_r2).$implicit; const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OnRestoreClicked(entry_r4)); });
101
+ i0.ɵɵelement(11, "i", 25);
102
+ i0.ɵɵtext(12, " Restore ");
103
+ i0.ɵɵelementEnd()()();
104
+ } if (rf & 2) {
105
+ const entry_r4 = ctx.$implicit;
106
+ const ctx_r0 = i0.ɵɵnextContext(2);
107
+ i0.ɵɵadvance(3);
108
+ i0.ɵɵtextInterpolate(entry_r4.DisplaySummary);
109
+ i0.ɵɵadvance();
110
+ i0.ɵɵconditional(entry_r4.SupportingFields.length > 0 ? 4 : -1);
111
+ i0.ɵɵadvance(3);
112
+ i0.ɵɵtextInterpolate1(" Deleted ", ctx_r0.formatTimestamp(entry_r4.RecordChange.ChangedAt), " ");
113
+ i0.ɵɵadvance();
114
+ i0.ɵɵconditional(entry_r4.RecordChange.User ? 8 : -1);
115
+ i0.ɵɵadvance(2);
116
+ i0.ɵɵproperty("disabled", !ctx_r0.CanCreate)("title", ctx_r0.CanCreate ? "Re-create this record from its snapshot" : "You do not have Create permission for this entity");
117
+ } }
118
+ function RecycleBinComponent_Conditional_13_Template(rf, ctx) { if (rf & 1) {
119
+ i0.ɵɵelementStart(0, "div", 12);
120
+ i0.ɵɵrepeaterCreate(1, RecycleBinComponent_Conditional_13_For_2_Template, 13, 6, "div", 17, _forTrack0);
121
+ i0.ɵɵelementEnd();
122
+ } if (rf & 2) {
123
+ const ctx_r0 = i0.ɵɵnextContext();
124
+ i0.ɵɵadvance();
125
+ i0.ɵɵrepeater(ctx_r0.Entries);
126
+ } }
127
+ /**
128
+ * Slide-in panel that lists all hard-deleted records for a single entity
129
+ * and lets a user with `Delete` permission re-create any of them from its
130
+ * historical RecordChange snapshot.
131
+ *
132
+ * ### When to use
133
+ *
134
+ * Surface this component from any entity-viewing context where the user
135
+ * might need to restore a hard-deleted record. The {@link EntityViewerComponent},
136
+ * {@link EntityDataGridComponent}, and {@link EntityCardsComponent}
137
+ * already expose a `ShowRecycleBin` input (default `true`) that renders a
138
+ * chip in their toolbar — you only need to use this component directly if
139
+ * you're building a custom viewer.
140
+ *
141
+ * ### Permissions
142
+ *
143
+ * The chip / panel is gated on `entity.UserPermissions.CanDelete` —
144
+ * rationale: there is no native "undelete" permission in MemberJunction,
145
+ * but if a user has the higher-trust permission to *delete* records of an
146
+ * entity, restoring deleted ones is well within scope. The actual
147
+ * re-create action additionally requires `CanCreate`; without it the
148
+ * Restore button on each card disables with a tooltip.
149
+ *
150
+ * ### Soft deletes vs hard deletes
151
+ *
152
+ * This component only surfaces *hard*-deleted records. Soft-deletes (e.g.,
153
+ * `IsDeleted` flags, `Status='Inactive'`, etc.) leave the record visible
154
+ * in normal entity views, so the standard Record Changes panel + restore
155
+ * preview already handles them — no Recycle Bin needed.
156
+ *
157
+ * ### Cancelable Before/After event surface
158
+ *
159
+ * Every meaningful action emits a paired `before*` / `after*` event. The
160
+ * `before*` event carries `cancel: boolean` so consumers can intercept
161
+ * — useful for custom approval workflows, audit logging, or to take over
162
+ * the actual restore execution entirely. See {@link BeforeRecordRestoreEventArgs}
163
+ * and friends.
164
+ *
165
+ * @example Basic usage (rare — usually embedded by entity-viewer)
166
+ * <mj-recycle-bin
167
+ * [Visible]="showBin"
168
+ * [EntityName]="'Customers'"
169
+ * (Closed)="showBin = false"
170
+ * (BeforeRecordRestore)="auditRestore($event)"
171
+ * (AfterRestoreCommit)="onRestored($event)">
172
+ * </mj-recycle-bin>
173
+ *
174
+ * @example Intercepting a restore
175
+ * onBeforeRecordRestore(e: BeforeRecordRestoreEventArgs) {
176
+ * if (!myCustomApproval(e.entry)) {
177
+ * e.cancel = true;
178
+ * e.cancelReason = 'Awaiting compliance approval';
179
+ * }
180
+ * }
181
+ */
182
+ export class RecycleBinComponent {
183
+ cdr;
184
+ // ─── Inputs ─────────────────────────────────────────────────────
185
+ /**
186
+ * Controls panel visibility. Set to true to slide in (and trigger an
187
+ * initial load); false to slide out.
188
+ */
189
+ _visible = false;
190
+ set Visible(value) {
191
+ const wasVisible = this._visible;
192
+ this._visible = value;
193
+ if (value && !wasVisible && this.isInitialized) {
194
+ this.LoadDeletedRecords();
195
+ }
196
+ }
197
+ get Visible() {
198
+ return this._visible;
199
+ }
200
+ /**
201
+ * The name of the entity whose deleted records will be listed. Required.
202
+ */
203
+ EntityName = null;
204
+ /**
205
+ * Optional context user. When omitted, falls back to
206
+ * {@link Metadata.Provider.CurrentUser} per standard MJ conventions.
207
+ */
208
+ ContextUser = null;
209
+ /**
210
+ * Maximum number of deleted-record cards to load. Defaults to 200 — this
211
+ * is a UI affordance, not a hard limit; consumers needing pagination
212
+ * should listen to {@link AfterRecycleBinOpen} and surface their own UI
213
+ * if `deletedRecordCount === MaxRecords`.
214
+ */
215
+ MaxRecords = 200;
216
+ // ─── Outputs ────────────────────────────────────────────────────
217
+ /** Fires when the user closes the panel. */
218
+ Closed = new EventEmitter();
219
+ /**
220
+ * Cancelable. Fires when {@link Visible} flips to true and the bin is
221
+ * about to query for deleted records. Setting `cancel = true` aborts
222
+ * the query (and the panel will show the empty state).
223
+ */
224
+ BeforeRecycleBinOpen = new EventEmitter();
225
+ /** Fires after the deleted-record query completes. */
226
+ AfterRecycleBinOpen = new EventEmitter();
227
+ /**
228
+ * Cancelable. Fires when the user clicks Restore on a deleted-record
229
+ * card, before the preview panel opens. Setting `cancel = true` skips
230
+ * the preview entirely — useful for consumers that want to take over
231
+ * the restore flow themselves.
232
+ */
233
+ BeforeRecordRestore = new EventEmitter();
234
+ /**
235
+ * Fires after the user closes the restore preview, with `success`
236
+ * indicating whether the actual insert worked.
237
+ */
238
+ AfterRecordRestore = new EventEmitter();
239
+ /**
240
+ * Cancelable. Fires after the user clicks Restore in the preview but
241
+ * before the entity is inserted. Setting `cancel = true` aborts the
242
+ * insert.
243
+ */
244
+ BeforeRestoreCommit = new EventEmitter();
245
+ /** Fires after the insert completes (success or failure). */
246
+ AfterRestoreCommit = new EventEmitter();
247
+ // ─── Public template state ──────────────────────────────────────
248
+ IsLoading = false;
249
+ LoadError = null;
250
+ Entries = [];
251
+ /** The currently selected entry whose preview is open. */
252
+ SelectedEntry = null;
253
+ PreviewVisible = false;
254
+ /** Permission flags (computed once when EntityName is set). */
255
+ CanDelete = false;
256
+ CanCreate = false;
257
+ isInitialized = false;
258
+ resolvedEntityInfo = null;
259
+ constructor(cdr) {
260
+ this.cdr = cdr;
261
+ }
262
+ ngOnInit() {
263
+ this.isInitialized = true;
264
+ if (this.Visible)
265
+ this.LoadDeletedRecords();
266
+ }
267
+ // ─── Derived state ──────────────────────────────────────────────
268
+ get HeaderSubtitle() {
269
+ if (!this.EntityName)
270
+ return '';
271
+ const n = this.Entries.length;
272
+ return `${n} record${n === 1 ? '' : 's'} have been deleted from ${this.EntityName}.`;
273
+ }
274
+ /**
275
+ * The user has Delete permission and may browse the bin. The Restore
276
+ * button on each card additionally requires {@link CanCreate}.
277
+ */
278
+ get HasAccess() {
279
+ return this.CanDelete;
280
+ }
281
+ // ─── Public actions ─────────────────────────────────────────────
282
+ /**
283
+ * Manually trigger a re-load — useful for consumers who want to refresh
284
+ * after some external event (e.g., a delete happened and they want the
285
+ * bin to re-populate immediately).
286
+ */
287
+ async LoadDeletedRecords() {
288
+ if (!this.EntityName) {
289
+ this.LoadError = 'EntityName is required';
290
+ return;
291
+ }
292
+ // Resolve metadata + permissions
293
+ const md = new Metadata();
294
+ const entityInfo = md.Entities.find(e => e.Name.trim().toLowerCase() === this.EntityName.trim().toLowerCase());
295
+ if (!entityInfo) {
296
+ this.LoadError = `Entity '${this.EntityName}' not found`;
297
+ this.cdr.markForCheck();
298
+ return;
299
+ }
300
+ this.resolvedEntityInfo = entityInfo;
301
+ const effectiveUser = this.ContextUser ?? md.CurrentUser;
302
+ if (effectiveUser) {
303
+ const perms = entityInfo.GetUserPermisions(effectiveUser);
304
+ this.CanDelete = perms?.CanDelete ?? false;
305
+ this.CanCreate = perms?.CanCreate ?? false;
306
+ }
307
+ else {
308
+ this.CanDelete = false;
309
+ this.CanCreate = false;
310
+ }
311
+ if (!this.CanDelete) {
312
+ this.Entries = [];
313
+ this.LoadError = 'You do not have Delete permission for this entity.';
314
+ this.cdr.markForCheck();
315
+ return;
316
+ }
317
+ // Fire BeforeRecycleBinOpen — consumer may cancel
318
+ const beforeArgs = {
319
+ bin: this,
320
+ timestamp: new Date(),
321
+ entityName: this.EntityName,
322
+ cancel: false,
323
+ _kind: 'beforeRecycleBinOpen',
324
+ };
325
+ this.BeforeRecycleBinOpen.emit(beforeArgs);
326
+ if (beforeArgs.cancel) {
327
+ this.LoadError = beforeArgs.cancelReason ?? 'Cancelled by host';
328
+ this.cdr.markForCheck();
329
+ return;
330
+ }
331
+ this.IsLoading = true;
332
+ this.LoadError = null;
333
+ this.Entries = [];
334
+ this.cdr.markForCheck();
335
+ try {
336
+ const rv = new RunView();
337
+ // Get the most recent Delete change per RecordID. We over-fetch and
338
+ // dedupe in JS — the data is small and SQL grouping with the latest
339
+ // change per ID is awkward across dialects.
340
+ const result = await rv.RunView({
341
+ EntityName: 'MJ: Record Changes',
342
+ ExtraFilter: `EntityID='${entityInfo.ID}' AND Type='Delete'`,
343
+ OrderBy: 'ChangedAt DESC',
344
+ MaxRows: this.MaxRecords * 2, // dedupe headroom
345
+ ResultType: 'entity_object',
346
+ }, this.ContextUser ?? undefined);
347
+ if (!result.Success) {
348
+ this.LoadError = result.ErrorMessage || 'Failed to load deleted records';
349
+ }
350
+ else {
351
+ this.Entries = this.dedupeAndBuildEntries(result.Results, entityInfo);
352
+ }
353
+ }
354
+ catch (e) {
355
+ this.LoadError = e instanceof Error ? e.message : 'Unknown error';
356
+ }
357
+ finally {
358
+ this.IsLoading = false;
359
+ this.cdr.markForCheck();
360
+ }
361
+ // Fire AfterRecycleBinOpen
362
+ this.AfterRecycleBinOpen.emit({
363
+ bin: this,
364
+ timestamp: new Date(),
365
+ entityName: this.EntityName,
366
+ deletedRecordCount: this.Entries.length,
367
+ _kind: 'afterRecycleBinOpen',
368
+ });
369
+ }
370
+ /**
371
+ * User clicked Restore on a card — open the preview panel (unless a
372
+ * BeforeRecordRestore handler cancelled).
373
+ */
374
+ OnRestoreClicked(entry) {
375
+ if (!this.CanCreate)
376
+ return; // gated in template too
377
+ const beforeArgs = {
378
+ bin: this,
379
+ timestamp: new Date(),
380
+ entityName: this.EntityName ?? '',
381
+ entry,
382
+ cancel: false,
383
+ _kind: 'beforeRecordRestore',
384
+ };
385
+ this.BeforeRecordRestore.emit(beforeArgs);
386
+ if (beforeArgs.cancel)
387
+ return;
388
+ this.SelectedEntry = entry;
389
+ this.PreviewVisible = true;
390
+ this.cdr.markForCheck();
391
+ }
392
+ /**
393
+ * Preview panel emitted RestoreConfirmed — perform the insert and emit
394
+ * the matching after* events.
395
+ */
396
+ async OnPreviewConfirmed(commit) {
397
+ if (!this.SelectedEntry || !this.resolvedEntityInfo)
398
+ return;
399
+ const entry = this.SelectedEntry;
400
+ const beforeArgs = {
401
+ bin: this,
402
+ timestamp: new Date(),
403
+ entityName: this.EntityName ?? '',
404
+ entry,
405
+ fieldValues: commit.FieldValues,
406
+ reason: commit.Reason,
407
+ cancel: false,
408
+ _kind: 'beforeRestoreCommit',
409
+ };
410
+ this.BeforeRestoreCommit.emit(beforeArgs);
411
+ if (beforeArgs.cancel) {
412
+ this.PreviewVisible = false;
413
+ this.cdr.markForCheck();
414
+ return;
415
+ }
416
+ let success = false;
417
+ let errorMessage;
418
+ let newRecordID;
419
+ try {
420
+ const md = new Metadata();
421
+ const entity = await md.GetEntityObject(this.resolvedEntityInfo.Name, this.ContextUser ?? undefined);
422
+ // Apply the snapshot fields, including the original primary key
423
+ // (preserves any FK references that still point at this ID).
424
+ for (const fv of commit.FieldValues) {
425
+ entity.Set(fv.FieldName, fv.Value);
426
+ }
427
+ // Also re-apply the original primary key from the snapshot so dangling
428
+ // FK references continue to work. Set() on a fresh entity treats this
429
+ // as the new record's PK.
430
+ try {
431
+ const snapshotData = JSON.parse(entry.RecordChange.FullRecordJSON || '{}');
432
+ for (const pk of this.resolvedEntityInfo.PrimaryKeys) {
433
+ if (snapshotData[pk.Name] != null) {
434
+ entity.Set(pk.Name, snapshotData[pk.Name]);
435
+ }
436
+ }
437
+ }
438
+ catch {
439
+ // PK preservation is best-effort
440
+ }
441
+ entity.SetRestoreContext(commit.SourceChangeID, commit.Reason);
442
+ try {
443
+ success = await entity.Save();
444
+ if (success) {
445
+ newRecordID = entity.PrimaryKey.ToString();
446
+ }
447
+ else {
448
+ errorMessage = entity.LatestResult?.CompleteMessage ?? 'Save returned false';
449
+ }
450
+ }
451
+ finally {
452
+ entity.ClearRestoreContext();
453
+ }
454
+ }
455
+ catch (e) {
456
+ success = false;
457
+ errorMessage = e instanceof Error ? e.message : 'Unknown error';
458
+ }
459
+ this.AfterRestoreCommit.emit({
460
+ bin: this,
461
+ timestamp: new Date(),
462
+ entityName: this.EntityName ?? '',
463
+ entry,
464
+ success,
465
+ newRecordID,
466
+ errorMessage,
467
+ _kind: 'afterRestoreCommit',
468
+ });
469
+ this.AfterRecordRestore.emit({
470
+ bin: this,
471
+ timestamp: new Date(),
472
+ entityName: this.EntityName ?? '',
473
+ entry,
474
+ success,
475
+ errorMessage,
476
+ _kind: 'afterRecordRestore',
477
+ });
478
+ if (success) {
479
+ // Remove the restored entry from the list and close preview
480
+ this.Entries = this.Entries.filter(e => e !== entry);
481
+ }
482
+ this.PreviewVisible = false;
483
+ this.SelectedEntry = null;
484
+ this.cdr.markForCheck();
485
+ }
486
+ OnPreviewCancelled() {
487
+ this.PreviewVisible = false;
488
+ this.SelectedEntry = null;
489
+ this.cdr.markForCheck();
490
+ }
491
+ OnClose() {
492
+ this._visible = false;
493
+ this.cdr.markForCheck();
494
+ this.Closed.emit();
495
+ }
496
+ // ─── Internal: build entries from raw RecordChange rows ─────────
497
+ /**
498
+ * Dedupes the raw delete-change rows: keeps only the most recent Delete
499
+ * per RecordID (since a record could in principle have been recreated
500
+ * and re-deleted). Builds the display fields heuristically by reading
501
+ * the snapshot JSON.
502
+ */
503
+ dedupeAndBuildEntries(rows, entityInfo) {
504
+ const seen = new Set();
505
+ const entries = [];
506
+ for (const row of rows) {
507
+ if (entries.length >= this.MaxRecords)
508
+ break;
509
+ if (seen.has(row.RecordID))
510
+ continue;
511
+ seen.add(row.RecordID);
512
+ const snapshot = this.parseSnapshot(row.FullRecordJSON);
513
+ if (!snapshot)
514
+ continue;
515
+ const entry = {
516
+ RecordChange: row,
517
+ RecordID: row.RecordID,
518
+ DisplaySummary: this.buildDisplaySummary(snapshot, entityInfo),
519
+ SupportingFields: this.buildSupportingFields(snapshot, entityInfo),
520
+ };
521
+ entries.push(entry);
522
+ }
523
+ return entries;
524
+ }
525
+ /**
526
+ * Builds the headline of the card. Prefers the entity's name field, then
527
+ * any non-PK string field, then the RecordID itself as a last resort.
528
+ */
529
+ buildDisplaySummary(snapshot, entityInfo) {
530
+ const nameField = entityInfo.NameField;
531
+ if (nameField && snapshot[nameField.Name] != null && snapshot[nameField.Name] !== '') {
532
+ return String(snapshot[nameField.Name]);
533
+ }
534
+ // Fallback: first non-PK, non-empty string field
535
+ for (const f of entityInfo.Fields) {
536
+ if (f.IsPrimaryKey)
537
+ continue;
538
+ const v = snapshot[f.Name];
539
+ if (typeof v === 'string' && v.trim().length > 0)
540
+ return v;
541
+ }
542
+ return `Record ${snapshot[entityInfo.PrimaryKeys[0]?.Name] ?? ''}`;
543
+ }
544
+ /**
545
+ * Picks up to 3 "interesting" fields from the snapshot to render as
546
+ * supporting metadata under the headline. Heuristic: prefer fields that
547
+ * are non-empty, non-PK, non-system, and have a display name.
548
+ */
549
+ buildSupportingFields(snapshot, entityInfo) {
550
+ const out = [];
551
+ const nameFieldName = entityInfo.NameField?.Name;
552
+ for (const f of entityInfo.Fields) {
553
+ if (out.length >= 3)
554
+ break;
555
+ if (f.IsPrimaryKey)
556
+ continue;
557
+ if (f.Name.startsWith('__mj_'))
558
+ continue;
559
+ if (f.Name === nameFieldName)
560
+ continue;
561
+ const v = snapshot[f.Name];
562
+ if (v == null || v === '')
563
+ continue;
564
+ out.push({
565
+ Name: f.Name,
566
+ DisplayName: f.DisplayNameOrName,
567
+ Value: this.formatFieldValue(v, f),
568
+ });
569
+ }
570
+ return out;
571
+ }
572
+ formatFieldValue(value, _field) {
573
+ if (value == null)
574
+ return '';
575
+ if (typeof value === 'object') {
576
+ try {
577
+ return JSON.stringify(value);
578
+ }
579
+ catch {
580
+ return String(value);
581
+ }
582
+ }
583
+ return String(value);
584
+ }
585
+ parseSnapshot(json) {
586
+ if (!json)
587
+ return null;
588
+ try {
589
+ return JSON.parse(json);
590
+ }
591
+ catch {
592
+ return null;
593
+ }
594
+ }
595
+ // ─── Display helpers (template) ─────────────────────────────────
596
+ formatTimestamp(date) {
597
+ if (!date)
598
+ return '';
599
+ return new Intl.DateTimeFormat('en-US', {
600
+ year: 'numeric',
601
+ month: 'short',
602
+ day: 'numeric',
603
+ hour: 'numeric',
604
+ minute: '2-digit',
605
+ hour12: true,
606
+ }).format(new Date(date));
607
+ }
608
+ getUserDisplay(user) {
609
+ if (!user)
610
+ return 'Unknown';
611
+ if (user.includes('@'))
612
+ return user.split('@')[0];
613
+ return user;
614
+ }
615
+ static ɵfac = function RecycleBinComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RecycleBinComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
616
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RecycleBinComponent, selectors: [["mj-recycle-bin"]], inputs: { Visible: "Visible", EntityName: "EntityName", ContextUser: "ContextUser", MaxRecords: "MaxRecords" }, outputs: { Closed: "Closed", BeforeRecycleBinOpen: "BeforeRecycleBinOpen", AfterRecycleBinOpen: "AfterRecycleBinOpen", BeforeRecordRestore: "BeforeRecordRestore", AfterRecordRestore: "AfterRecordRestore", BeforeRestoreCommit: "BeforeRestoreCommit", AfterRestoreCommit: "AfterRestoreCommit" }, standalone: false, decls: 15, vars: 16, consts: [[3, "Closed", "Mode", "Title", "Visible", "Resizable", "MinWidthPx", "MaxWidthRatio"], [1, "rb-container"], [1, "rb-header"], [1, "rb-header-icon"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash-can-arrow-up"], [1, "rb-header-text"], [1, "rb-header-title"], [1, "rb-header-subtitle"], [1, "rb-state", "rb-state-denied"], ["text", "Loading deleted records...", "size", "medium"], [1, "rb-state", "rb-state-error"], [1, "rb-state", "rb-state-empty"], [1, "rb-list"], [3, "RestoreConfirmed", "RestoreCancelled", "Visible", "Mode", "RecordChange", "EntityName"], ["aria-hidden", "true", 1, "fa-solid", "fa-lock"], ["aria-hidden", "true", 1, "fa-solid", "fa-triangle-exclamation"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash-can"], [1, "rb-card"], [1, "rb-card-info"], [1, "rb-card-name"], [1, "rb-card-fields"], [1, "rb-card-meta"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"], [1, "rb-card-actions"], ["type", "button", 1, "rb-btn", "rb-btn-restore", 3, "click", "disabled", "title"], ["aria-hidden", "true", 1, "fa-solid", "fa-rotate-left"], [1, "rb-card-field"]], template: function RecycleBinComponent_Template(rf, ctx) { if (rf & 1) {
617
+ i0.ɵɵelementStart(0, "mj-slide-panel", 0);
618
+ i0.ɵɵlistener("Closed", function RecycleBinComponent_Template_mj_slide_panel_Closed_0_listener() { return ctx.OnClose(); });
619
+ i0.ɵɵelementStart(1, "div", 1)(2, "div", 2)(3, "div", 3);
620
+ i0.ɵɵelement(4, "i", 4);
621
+ i0.ɵɵelementEnd();
622
+ i0.ɵɵelementStart(5, "div", 5)(6, "div", 6);
623
+ i0.ɵɵtext(7, "Recycle Bin");
624
+ i0.ɵɵelementEnd();
625
+ i0.ɵɵconditionalCreate(8, RecycleBinComponent_Conditional_8_Template, 2, 1, "div", 7);
626
+ i0.ɵɵelementEnd()();
627
+ i0.ɵɵconditionalCreate(9, RecycleBinComponent_Conditional_9_Template, 6, 1, "div", 8);
628
+ i0.ɵɵconditionalCreate(10, RecycleBinComponent_Conditional_10_Template, 1, 0, "mj-loading", 9);
629
+ i0.ɵɵconditionalCreate(11, RecycleBinComponent_Conditional_11_Template, 6, 1, "div", 10);
630
+ i0.ɵɵconditionalCreate(12, RecycleBinComponent_Conditional_12_Template, 6, 0, "div", 11);
631
+ i0.ɵɵconditionalCreate(13, RecycleBinComponent_Conditional_13_Template, 3, 0, "div", 12);
632
+ i0.ɵɵelementEnd();
633
+ i0.ɵɵelementStart(14, "mj-restore-preview-panel", 13);
634
+ i0.ɵɵlistener("RestoreConfirmed", function RecycleBinComponent_Template_mj_restore_preview_panel_RestoreConfirmed_14_listener($event) { return ctx.OnPreviewConfirmed($event); })("RestoreCancelled", function RecycleBinComponent_Template_mj_restore_preview_panel_RestoreCancelled_14_listener() { return ctx.OnPreviewCancelled(); });
635
+ i0.ɵɵelementEnd()();
636
+ } if (rf & 2) {
637
+ i0.ɵɵproperty("Mode", "slide")("Title", "Recycle Bin \u00B7 " + (ctx.EntityName || ""))("Visible", ctx.Visible)("Resizable", true)("MinWidthPx", 480)("MaxWidthRatio", 0.6);
638
+ i0.ɵɵadvance(8);
639
+ i0.ɵɵconditional(ctx.EntityName ? 8 : -1);
640
+ i0.ɵɵadvance();
641
+ i0.ɵɵconditional(!ctx.IsLoading && ctx.LoadError && !ctx.HasAccess ? 9 : -1);
642
+ i0.ɵɵadvance();
643
+ i0.ɵɵconditional(ctx.IsLoading ? 10 : -1);
644
+ i0.ɵɵadvance();
645
+ i0.ɵɵconditional(!ctx.IsLoading && ctx.LoadError && ctx.HasAccess ? 11 : -1);
646
+ i0.ɵɵadvance();
647
+ i0.ɵɵconditional(!ctx.IsLoading && !ctx.LoadError && ctx.HasAccess && ctx.Entries.length === 0 ? 12 : -1);
648
+ i0.ɵɵadvance();
649
+ i0.ɵɵconditional(!ctx.IsLoading && !ctx.LoadError && ctx.Entries.length > 0 ? 13 : -1);
650
+ i0.ɵɵadvance();
651
+ i0.ɵɵproperty("Visible", ctx.PreviewVisible)("Mode", "undelete")("RecordChange", (ctx.SelectedEntry == null ? null : ctx.SelectedEntry.RecordChange) ?? null)("EntityName", ctx.EntityName);
652
+ } }, dependencies: [i1.LoadingComponent, i2.RestorePreviewPanelComponent, i3.MjSlidePanelComponent], styles: ["/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Recycle Bin\n * Slide-in panel that lists hard-deleted records and offers re-create.\n * All colors use --mj-* design tokens.\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.rb-container {\n padding: 18px 20px;\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n\n/* \u2500\u2500\u2500 Header \u2500\u2500\u2500 */\n\n.rb-header {\n display: flex;\n align-items: flex-start;\n gap: 14px;\n padding: 8px 0 14px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.rb-header-icon {\n width: 40px;\n height: 40px;\n border-radius: 9px;\n background: color-mix(in srgb, var(--mj-status-info) 12%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 17px;\n}\n\n.rb-header-text { flex: 1; min-width: 0; }\n\n.rb-header-title {\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin-bottom: 2px;\n}\n\n.rb-header-subtitle {\n color: var(--mj-text-muted);\n font-size: 12px;\n}\n\n/* \u2500\u2500\u2500 State (denied / error / empty) \u2500\u2500\u2500 */\n\n.rb-state {\n text-align: center;\n padding: 50px 20px;\n color: var(--mj-text-muted);\n}\n\n.rb-state i {\n font-size: 36px;\n margin-bottom: 12px;\n display: block;\n}\n\n.rb-state h3 {\n margin: 0 0 6px;\n font-size: 15px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.rb-state p {\n margin: 0;\n font-size: 13px;\n max-width: 320px;\n margin-left: auto;\n margin-right: auto;\n}\n\n.rb-state-denied i { color: var(--mj-text-disabled); }\n.rb-state-error i { color: var(--mj-status-error); }\n.rb-state-empty i { color: var(--mj-text-disabled); }\n\n/* \u2500\u2500\u2500 List \u2500\u2500\u2500 */\n\n.rb-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* \u2500\u2500\u2500 Card \u2500\u2500\u2500 */\n\n.rb-card {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n padding: 14px;\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 14px;\n align-items: center;\n transition: border-color 0.15s;\n}\n\n.rb-card:hover {\n border-color: var(--mj-border-strong);\n}\n\n.rb-card-info { min-width: 0; }\n\n.rb-card-name {\n font-weight: 600;\n font-size: 14px;\n color: var(--mj-text-primary);\n margin-bottom: 5px;\n word-break: break-word;\n}\n\n.rb-card-fields {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 6px;\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n\n.rb-card-field strong {\n color: var(--mj-text-secondary);\n font-weight: 500;\n margin-right: 3px;\n}\n\n.rb-card-meta {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-text-muted);\n font-size: 11px;\n}\n\n.rb-card-meta i {\n color: var(--mj-text-disabled);\n}\n\n/* \u2500\u2500\u2500 Card actions \u2500\u2500\u2500 */\n\n.rb-card-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n flex-shrink: 0;\n}\n\n.rb-btn {\n padding: 7px 14px;\n border-radius: 7px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n border: 1px solid transparent;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n background: var(--mj-bg-surface);\n transition: background-color 0.1s, color 0.1s, border-color 0.1s;\n}\n\n.rb-btn-restore {\n color: var(--mj-status-success);\n border-color: var(--mj-status-success);\n}\n\n.rb-btn-restore:hover:not(:disabled) {\n background: var(--mj-status-success);\n color: var(--mj-text-inverse, #fff);\n}\n\n.rb-btn-restore:disabled {\n color: var(--mj-text-disabled);\n border-color: var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n}\n"], encapsulation: 2, changeDetection: 0 });
653
+ }
654
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RecycleBinComponent, [{
655
+ type: Component,
656
+ args: [{ standalone: false, selector: 'mj-recycle-bin', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<mj-slide-panel\n [Mode]=\"'slide'\"\n [Title]=\"'Recycle Bin \u00B7 ' + (EntityName || '')\"\n [Visible]=\"Visible\"\n [Resizable]=\"true\"\n [MinWidthPx]=\"480\"\n [MaxWidthRatio]=\"0.6\"\n (Closed)=\"OnClose()\">\n\n <div class=\"rb-container\">\n\n <!-- Header -->\n <div class=\"rb-header\">\n <div class=\"rb-header-icon\">\n <i class=\"fa-solid fa-trash-can-arrow-up\" aria-hidden=\"true\"></i>\n </div>\n <div class=\"rb-header-text\">\n <div class=\"rb-header-title\">Recycle Bin</div>\n @if (EntityName) {\n <div class=\"rb-header-subtitle\">{{ HeaderSubtitle }}</div>\n }\n </div>\n </div>\n\n <!-- No-permission state -->\n @if (!IsLoading && LoadError && !HasAccess) {\n <div class=\"rb-state rb-state-denied\">\n <i class=\"fa-solid fa-lock\" aria-hidden=\"true\"></i>\n <h3>Access denied</h3>\n <p>{{ LoadError }}</p>\n </div>\n }\n\n <!-- Loading -->\n @if (IsLoading) {\n <mj-loading text=\"Loading deleted records...\" size=\"medium\"></mj-loading>\n }\n\n <!-- Generic error -->\n @if (!IsLoading && LoadError && HasAccess) {\n <div class=\"rb-state rb-state-error\">\n <i class=\"fa-solid fa-triangle-exclamation\" aria-hidden=\"true\"></i>\n <h3>Could not load deleted records</h3>\n <p>{{ LoadError }}</p>\n </div>\n }\n\n <!-- Empty state -->\n @if (!IsLoading && !LoadError && HasAccess && Entries.length === 0) {\n <div class=\"rb-state rb-state-empty\">\n <i class=\"fa-solid fa-trash-can\" aria-hidden=\"true\"></i>\n <h3>Recycle Bin is empty</h3>\n <p>No records have been deleted from this entity, or change tracking\n isn't enabled here.</p>\n </div>\n }\n\n <!-- Entries -->\n @if (!IsLoading && !LoadError && Entries.length > 0) {\n <div class=\"rb-list\">\n @for (entry of Entries; track entry.RecordChange.ID) {\n <div class=\"rb-card\">\n <div class=\"rb-card-info\">\n <div class=\"rb-card-name\">{{ entry.DisplaySummary }}</div>\n @if (entry.SupportingFields.length > 0) {\n <div class=\"rb-card-fields\">\n @for (sf of entry.SupportingFields; track sf.Name) {\n <span class=\"rb-card-field\">\n <strong>{{ sf.DisplayName }}:</strong> {{ sf.Value }}\n </span>\n }\n </div>\n }\n <div class=\"rb-card-meta\">\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n Deleted {{ formatTimestamp(entry.RecordChange.ChangedAt) }}\n @if (entry.RecordChange.User) {\n by {{ getUserDisplay(entry.RecordChange.User) }}\n }\n </div>\n </div>\n <div class=\"rb-card-actions\">\n <button class=\"rb-btn rb-btn-restore\"\n [disabled]=\"!CanCreate\"\n [title]=\"CanCreate ? 'Re-create this record from its snapshot' : 'You do not have Create permission for this entity'\"\n (click)=\"OnRestoreClicked(entry)\"\n type=\"button\">\n <i class=\"fa-solid fa-rotate-left\" aria-hidden=\"true\"></i>\n Restore\n </button>\n </div>\n </div>\n }\n </div>\n }\n\n </div>\n\n <!-- Restore preview slide-in (reused from ng-record-changes) -->\n <mj-restore-preview-panel\n [Visible]=\"PreviewVisible\"\n [Mode]=\"'undelete'\"\n [RecordChange]=\"SelectedEntry?.RecordChange ?? null\"\n [EntityName]=\"EntityName\"\n (RestoreConfirmed)=\"OnPreviewConfirmed($event)\"\n (RestoreCancelled)=\"OnPreviewCancelled()\">\n </mj-restore-preview-panel>\n\n</mj-slide-panel>\n", styles: ["/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Recycle Bin\n * Slide-in panel that lists hard-deleted records and offers re-create.\n * All colors use --mj-* design tokens.\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.rb-container {\n padding: 18px 20px;\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n\n/* \u2500\u2500\u2500 Header \u2500\u2500\u2500 */\n\n.rb-header {\n display: flex;\n align-items: flex-start;\n gap: 14px;\n padding: 8px 0 14px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.rb-header-icon {\n width: 40px;\n height: 40px;\n border-radius: 9px;\n background: color-mix(in srgb, var(--mj-status-info) 12%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 17px;\n}\n\n.rb-header-text { flex: 1; min-width: 0; }\n\n.rb-header-title {\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin-bottom: 2px;\n}\n\n.rb-header-subtitle {\n color: var(--mj-text-muted);\n font-size: 12px;\n}\n\n/* \u2500\u2500\u2500 State (denied / error / empty) \u2500\u2500\u2500 */\n\n.rb-state {\n text-align: center;\n padding: 50px 20px;\n color: var(--mj-text-muted);\n}\n\n.rb-state i {\n font-size: 36px;\n margin-bottom: 12px;\n display: block;\n}\n\n.rb-state h3 {\n margin: 0 0 6px;\n font-size: 15px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.rb-state p {\n margin: 0;\n font-size: 13px;\n max-width: 320px;\n margin-left: auto;\n margin-right: auto;\n}\n\n.rb-state-denied i { color: var(--mj-text-disabled); }\n.rb-state-error i { color: var(--mj-status-error); }\n.rb-state-empty i { color: var(--mj-text-disabled); }\n\n/* \u2500\u2500\u2500 List \u2500\u2500\u2500 */\n\n.rb-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* \u2500\u2500\u2500 Card \u2500\u2500\u2500 */\n\n.rb-card {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n padding: 14px;\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 14px;\n align-items: center;\n transition: border-color 0.15s;\n}\n\n.rb-card:hover {\n border-color: var(--mj-border-strong);\n}\n\n.rb-card-info { min-width: 0; }\n\n.rb-card-name {\n font-weight: 600;\n font-size: 14px;\n color: var(--mj-text-primary);\n margin-bottom: 5px;\n word-break: break-word;\n}\n\n.rb-card-fields {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 6px;\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n\n.rb-card-field strong {\n color: var(--mj-text-secondary);\n font-weight: 500;\n margin-right: 3px;\n}\n\n.rb-card-meta {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-text-muted);\n font-size: 11px;\n}\n\n.rb-card-meta i {\n color: var(--mj-text-disabled);\n}\n\n/* \u2500\u2500\u2500 Card actions \u2500\u2500\u2500 */\n\n.rb-card-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n flex-shrink: 0;\n}\n\n.rb-btn {\n padding: 7px 14px;\n border-radius: 7px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n border: 1px solid transparent;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n background: var(--mj-bg-surface);\n transition: background-color 0.1s, color 0.1s, border-color 0.1s;\n}\n\n.rb-btn-restore {\n color: var(--mj-status-success);\n border-color: var(--mj-status-success);\n}\n\n.rb-btn-restore:hover:not(:disabled) {\n background: var(--mj-status-success);\n color: var(--mj-text-inverse, #fff);\n}\n\n.rb-btn-restore:disabled {\n color: var(--mj-text-disabled);\n border-color: var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n}\n"] }]
657
+ }], () => [{ type: i0.ChangeDetectorRef }], { Visible: [{
658
+ type: Input
659
+ }], EntityName: [{
660
+ type: Input
661
+ }], ContextUser: [{
662
+ type: Input
663
+ }], MaxRecords: [{
664
+ type: Input
665
+ }], Closed: [{
666
+ type: Output
667
+ }], BeforeRecycleBinOpen: [{
668
+ type: Output
669
+ }], AfterRecycleBinOpen: [{
670
+ type: Output
671
+ }], BeforeRecordRestore: [{
672
+ type: Output
673
+ }], AfterRecordRestore: [{
674
+ type: Output
675
+ }], BeforeRestoreCommit: [{
676
+ type: Output
677
+ }], AfterRestoreCommit: [{
678
+ type: Output
679
+ }] }); })();
680
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RecycleBinComponent, { className: "RecycleBinComponent", filePath: "src/lib/recycle-bin/recycle-bin.component.ts", lineNumber: 93 }); })();
681
+ //# sourceMappingURL=recycle-bin.component.js.map