@memberjunction/ng-record-changes 6.2.0-edge.0 → 6.2.0-edge.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/format-change-value.d.ts +32 -0
- package/dist/lib/format-change-value.d.ts.map +1 -0
- package/dist/lib/format-change-value.js +81 -0
- package/dist/lib/format-change-value.js.map +1 -0
- package/dist/lib/ng-record-changes.component.d.ts +96 -9
- package/dist/lib/ng-record-changes.component.d.ts.map +1 -1
- package/dist/lib/ng-record-changes.component.js +223 -85
- package/dist/lib/ng-record-changes.component.js.map +1 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.d.ts +4 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.d.ts.map +1 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.js +18 -35
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.js.map +1 -1
- package/package.json +10 -10
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ViewEncapsulation, } from '@angular/core';
|
|
2
2
|
import { EntityFieldTSType, RunView, } from '@memberjunction/core';
|
|
3
3
|
import { UUIDsEqual, NormalizeUUID } from '@memberjunction/global';
|
|
4
|
+
import { FormatChangeValue } from './format-change-value';
|
|
4
5
|
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
5
6
|
import { diffChars, diffWords } from 'diff';
|
|
6
7
|
import * as i0 from "@angular/core";
|
|
@@ -621,7 +622,15 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
621
622
|
sanitizer;
|
|
622
623
|
IsLoading = false;
|
|
623
624
|
IsVisible = false;
|
|
624
|
-
|
|
625
|
+
DialogClosed = new EventEmitter();
|
|
626
|
+
/**
|
|
627
|
+
* @deprecated Use {@link DialogClosed}.
|
|
628
|
+
*
|
|
629
|
+
* The same emitter under the old binding name, so a template still binding
|
|
630
|
+
* (dialogClosed) keeps working. Must stay AFTER DialogClosed: class fields
|
|
631
|
+
* initialise in order, and the other way round this captures undefined.
|
|
632
|
+
*/
|
|
633
|
+
dialogClosed = this.DialogClosed;
|
|
625
634
|
record;
|
|
626
635
|
/** Whether to show a "Restore" button on each historical version row. Default false. */
|
|
627
636
|
AllowRestore = false;
|
|
@@ -631,8 +640,24 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
631
640
|
* saving with `record.SetRestoreContext()` set first.
|
|
632
641
|
*/
|
|
633
642
|
RestoreRequested = new EventEmitter();
|
|
634
|
-
|
|
635
|
-
|
|
643
|
+
ViewData = [];
|
|
644
|
+
/** @deprecated Use {@link ViewData}. */
|
|
645
|
+
get viewData() {
|
|
646
|
+
return this.ViewData;
|
|
647
|
+
}
|
|
648
|
+
/** @deprecated Use {@link ViewData}. */
|
|
649
|
+
set viewData(value) {
|
|
650
|
+
this.ViewData = value;
|
|
651
|
+
}
|
|
652
|
+
FilteredData = [];
|
|
653
|
+
/** @deprecated Use {@link FilteredData}. */
|
|
654
|
+
get filteredData() {
|
|
655
|
+
return this.FilteredData;
|
|
656
|
+
}
|
|
657
|
+
/** @deprecated Use {@link FilteredData}. */
|
|
658
|
+
set filteredData(value) {
|
|
659
|
+
this.FilteredData = value;
|
|
660
|
+
}
|
|
636
661
|
/**
|
|
637
662
|
* Change lookup keyed by NormalizeUUID(ID), rebuilt whenever {@link viewData}
|
|
638
663
|
* loads. Lets {@link getRestoredFromSourceChange} resolve a restore row's
|
|
@@ -641,8 +666,24 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
641
666
|
* template `@if` and once inside `getChangeSummary`), giving O(rows^2)/CD.
|
|
642
667
|
*/
|
|
643
668
|
viewDataById = new Map();
|
|
644
|
-
|
|
645
|
-
|
|
669
|
+
DateGroups = [];
|
|
670
|
+
/** @deprecated Use {@link DateGroups}. */
|
|
671
|
+
get dateGroups() {
|
|
672
|
+
return this.DateGroups;
|
|
673
|
+
}
|
|
674
|
+
/** @deprecated Use {@link DateGroups}. */
|
|
675
|
+
set dateGroups(value) {
|
|
676
|
+
this.DateGroups = value;
|
|
677
|
+
}
|
|
678
|
+
ExpandedItems = new Set();
|
|
679
|
+
/** @deprecated Use {@link ExpandedItems}. */
|
|
680
|
+
get expandedItems() {
|
|
681
|
+
return this.ExpandedItems;
|
|
682
|
+
}
|
|
683
|
+
/** @deprecated Use {@link ExpandedItems}. */
|
|
684
|
+
set expandedItems(value) {
|
|
685
|
+
this.ExpandedItems = value;
|
|
686
|
+
}
|
|
646
687
|
/** The change record currently selected for restore preview, or null. */
|
|
647
688
|
RestorePreviewChange = null;
|
|
648
689
|
/** Visibility of the embedded restore preview slide-in. */
|
|
@@ -654,10 +695,34 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
654
695
|
IsLoadingLabels = false;
|
|
655
696
|
ShowCreateWizard = false;
|
|
656
697
|
// Filter properties
|
|
657
|
-
|
|
698
|
+
SearchTerm = '';
|
|
699
|
+
/** @deprecated Use {@link SearchTerm}. */
|
|
700
|
+
get searchTerm() {
|
|
701
|
+
return this.SearchTerm;
|
|
702
|
+
}
|
|
703
|
+
/** @deprecated Use {@link SearchTerm}. */
|
|
704
|
+
set searchTerm(value) {
|
|
705
|
+
this.SearchTerm = value;
|
|
706
|
+
}
|
|
658
707
|
/** Single selected type filter (legacy, kept for backwards compat). */
|
|
659
|
-
|
|
660
|
-
|
|
708
|
+
SelectedType = '';
|
|
709
|
+
/** @deprecated Use {@link SelectedType}. */
|
|
710
|
+
get selectedType() {
|
|
711
|
+
return this.SelectedType;
|
|
712
|
+
}
|
|
713
|
+
/** @deprecated Use {@link SelectedType}. */
|
|
714
|
+
set selectedType(value) {
|
|
715
|
+
this.SelectedType = value;
|
|
716
|
+
}
|
|
717
|
+
SelectedSource = '';
|
|
718
|
+
/** @deprecated Use {@link SelectedSource}. */
|
|
719
|
+
get selectedSource() {
|
|
720
|
+
return this.SelectedSource;
|
|
721
|
+
}
|
|
722
|
+
/** @deprecated Use {@link SelectedSource}. */
|
|
723
|
+
set selectedSource(value) {
|
|
724
|
+
this.SelectedSource = value;
|
|
725
|
+
}
|
|
661
726
|
/** Map of Key → selected, used by the conditional chip system. */
|
|
662
727
|
ChipSelections = {};
|
|
663
728
|
/** Whether the overflow popover is open. */
|
|
@@ -732,7 +797,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
732
797
|
this.IsVisible = false;
|
|
733
798
|
this.cdr.markForCheck();
|
|
734
799
|
// Allow the slide-out animation to complete before emitting
|
|
735
|
-
setTimeout(() => this.
|
|
800
|
+
setTimeout(() => this.DialogClosed.emit(), 300);
|
|
736
801
|
}
|
|
737
802
|
async LoadRecordChanges(pkey, appName, entityName) {
|
|
738
803
|
if (pkey && entityName) {
|
|
@@ -751,7 +816,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
751
816
|
}
|
|
752
817
|
this.ngZone.run(() => {
|
|
753
818
|
if (changes) {
|
|
754
|
-
this.
|
|
819
|
+
this.ViewData = changes.sort((a, b) => new Date(b.ChangedAt).getTime() - new Date(a.ChangedAt).getTime());
|
|
755
820
|
this.rebuildViewDataIndex();
|
|
756
821
|
this.rebuildConditionalPills();
|
|
757
822
|
this.applyFilters();
|
|
@@ -766,9 +831,13 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
766
831
|
}
|
|
767
832
|
}
|
|
768
833
|
// ─── Filter & Search ────────────────────────────────────────────
|
|
769
|
-
|
|
834
|
+
OnSearchChange() {
|
|
770
835
|
this.applyFilters();
|
|
771
836
|
}
|
|
837
|
+
/** @deprecated Use {@link OnSearchChange}. */
|
|
838
|
+
onSearchChange() {
|
|
839
|
+
return this.OnSearchChange();
|
|
840
|
+
}
|
|
772
841
|
onFilterChange() {
|
|
773
842
|
this.applyFilters();
|
|
774
843
|
}
|
|
@@ -819,7 +888,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
819
888
|
this.cdr.markForCheck();
|
|
820
889
|
}
|
|
821
890
|
ClearFilters() {
|
|
822
|
-
this.
|
|
891
|
+
this.SearchTerm = '';
|
|
823
892
|
for (const k of Object.keys(this.ChipSelections))
|
|
824
893
|
this.ChipSelections[k] = false;
|
|
825
894
|
this.applyFilters();
|
|
@@ -831,7 +900,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
831
900
|
rebuildConditionalPills() {
|
|
832
901
|
const counts = {};
|
|
833
902
|
let restoreCount = 0;
|
|
834
|
-
for (const c of this.
|
|
903
|
+
for (const c of this.ViewData) {
|
|
835
904
|
const type = c.Type ?? 'Update';
|
|
836
905
|
counts[type] = (counts[type] ?? 0) + 1;
|
|
837
906
|
if (this.isRestoreChange(c))
|
|
@@ -869,9 +938,9 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
869
938
|
return c.Source === 'Restore' || c.RestoredFromID != null;
|
|
870
939
|
}
|
|
871
940
|
applyFilters() {
|
|
872
|
-
let filtered = [...this.
|
|
873
|
-
if (this.
|
|
874
|
-
const search = this.
|
|
941
|
+
let filtered = [...this.ViewData];
|
|
942
|
+
if (this.SearchTerm.trim()) {
|
|
943
|
+
const search = this.SearchTerm.toLowerCase();
|
|
875
944
|
filtered = filtered.filter(change => change.ChangesDescription?.toLowerCase().includes(search) ||
|
|
876
945
|
change.User?.toLowerCase().includes(search) ||
|
|
877
946
|
change.Comments?.toLowerCase().includes(search));
|
|
@@ -893,8 +962,8 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
893
962
|
return false;
|
|
894
963
|
});
|
|
895
964
|
}
|
|
896
|
-
this.
|
|
897
|
-
this.
|
|
965
|
+
this.FilteredData = filtered;
|
|
966
|
+
this.DateGroups = this.buildDateGroups(this.FilteredData);
|
|
898
967
|
this.cdr.markForCheck();
|
|
899
968
|
}
|
|
900
969
|
// ─── Version Labels ─────────────────────────────────────────────
|
|
@@ -967,7 +1036,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
967
1036
|
this.ShowCreateWizard = false;
|
|
968
1037
|
this.cdr.markForCheck();
|
|
969
1038
|
}
|
|
970
|
-
|
|
1039
|
+
GetLabelStatusClass(status) {
|
|
971
1040
|
switch (status) {
|
|
972
1041
|
case 'Active': return 'label-status-active';
|
|
973
1042
|
case 'Archived': return 'label-status-archived';
|
|
@@ -975,22 +1044,34 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
975
1044
|
default: return '';
|
|
976
1045
|
}
|
|
977
1046
|
}
|
|
1047
|
+
/** @deprecated Use {@link GetLabelStatusClass}. */
|
|
1048
|
+
getLabelStatusClass(status) {
|
|
1049
|
+
return this.GetLabelStatusClass(status);
|
|
1050
|
+
}
|
|
978
1051
|
// ─── Timeline Interaction ───────────────────────────────────────
|
|
979
|
-
|
|
980
|
-
if (this.
|
|
981
|
-
this.
|
|
1052
|
+
ToggleExpansion(changeId) {
|
|
1053
|
+
if (this.ExpandedItems.has(changeId)) {
|
|
1054
|
+
this.ExpandedItems.delete(changeId);
|
|
982
1055
|
}
|
|
983
1056
|
else {
|
|
984
|
-
this.
|
|
1057
|
+
this.ExpandedItems.add(changeId);
|
|
985
1058
|
}
|
|
986
1059
|
this.cdr.markForCheck();
|
|
987
1060
|
}
|
|
988
|
-
|
|
1061
|
+
/** @deprecated Use {@link ToggleExpansion}. */
|
|
1062
|
+
toggleExpansion(changeId) {
|
|
1063
|
+
return this.ToggleExpansion(changeId);
|
|
1064
|
+
}
|
|
1065
|
+
OnTimelineItemKeydown(event, changeId) {
|
|
989
1066
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
990
1067
|
event.preventDefault();
|
|
991
|
-
this.
|
|
1068
|
+
this.ToggleExpansion(changeId);
|
|
992
1069
|
}
|
|
993
1070
|
}
|
|
1071
|
+
/** @deprecated Use {@link OnTimelineItemKeydown}. */
|
|
1072
|
+
onTimelineItemKeydown(event, changeId) {
|
|
1073
|
+
return this.OnTimelineItemKeydown(event, changeId);
|
|
1074
|
+
}
|
|
994
1075
|
/**
|
|
995
1076
|
* Opens the embedded restore preview panel for a given change record.
|
|
996
1077
|
* The panel computes a full-record diff (current vs the change's
|
|
@@ -1044,19 +1125,23 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1044
1125
|
* currently loaded changes. Returns null when the source isn't loaded
|
|
1045
1126
|
* (e.g., it's been pruned from history) or when this row isn't a restore.
|
|
1046
1127
|
*/
|
|
1047
|
-
|
|
1128
|
+
GetRestoredFromSourceChange(change) {
|
|
1048
1129
|
const sourceId = change.RestoredFromID;
|
|
1049
1130
|
if (!sourceId)
|
|
1050
1131
|
return null;
|
|
1051
1132
|
return this.viewDataById.get(NormalizeUUID(sourceId)) ?? null;
|
|
1052
1133
|
}
|
|
1134
|
+
/** @deprecated Use {@link GetRestoredFromSourceChange}. */
|
|
1135
|
+
getRestoredFromSourceChange(change) {
|
|
1136
|
+
return this.GetRestoredFromSourceChange(change);
|
|
1137
|
+
}
|
|
1053
1138
|
/**
|
|
1054
1139
|
* Rebuilds {@link viewDataById} from the current {@link viewData}. Called once
|
|
1055
1140
|
* per data load so source-change resolution is an O(1) Map read.
|
|
1056
1141
|
*/
|
|
1057
1142
|
rebuildViewDataIndex() {
|
|
1058
1143
|
this.viewDataById.clear();
|
|
1059
|
-
for (const c of this.
|
|
1144
|
+
for (const c of this.ViewData) {
|
|
1060
1145
|
this.viewDataById.set(NormalizeUUID(c.ID), c);
|
|
1061
1146
|
}
|
|
1062
1147
|
}
|
|
@@ -1064,17 +1149,25 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1064
1149
|
* True when the row was produced by a restore operation (either the
|
|
1065
1150
|
* `Source` is `'Restore'` OR `RestoredFromID` is populated).
|
|
1066
1151
|
*/
|
|
1067
|
-
|
|
1152
|
+
IsRestoreRow(change) {
|
|
1068
1153
|
return this.isRestoreChange(change);
|
|
1069
1154
|
}
|
|
1155
|
+
/** @deprecated Use {@link IsRestoreRow}. */
|
|
1156
|
+
isRestoreRow(change) {
|
|
1157
|
+
return this.IsRestoreRow(change);
|
|
1158
|
+
}
|
|
1070
1159
|
/**
|
|
1071
1160
|
* True when this change is the most recent in the loaded history.
|
|
1072
1161
|
* `viewData` is sorted DESC by `ChangedAt`, so the most recent is index 0.
|
|
1073
1162
|
* Restoring to the most recent version is a no-op, so the timeline hides
|
|
1074
1163
|
* the Restore button on this row.
|
|
1075
1164
|
*/
|
|
1165
|
+
IsMostRecentChange(change) {
|
|
1166
|
+
return this.ViewData.length > 0 && this.ViewData[0] === change;
|
|
1167
|
+
}
|
|
1168
|
+
/** @deprecated Use {@link IsMostRecentChange}. */
|
|
1076
1169
|
isMostRecentChange(change) {
|
|
1077
|
-
return this.
|
|
1170
|
+
return this.IsMostRecentChange(change);
|
|
1078
1171
|
}
|
|
1079
1172
|
/**
|
|
1080
1173
|
* Click handler for the lineage chip — scrolls/highlights the source
|
|
@@ -1083,7 +1176,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1083
1176
|
JumpToSourceChange(sourceChange, event) {
|
|
1084
1177
|
event.stopPropagation();
|
|
1085
1178
|
this.HighlightedChangeID = sourceChange.ID;
|
|
1086
|
-
this.
|
|
1179
|
+
this.ExpandedItems.add(sourceChange.ID);
|
|
1087
1180
|
this.cdr.markForCheck();
|
|
1088
1181
|
// Auto-clear the highlight after 3s
|
|
1089
1182
|
setTimeout(() => {
|
|
@@ -1123,7 +1216,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1123
1216
|
return formatted;
|
|
1124
1217
|
}
|
|
1125
1218
|
// ─── Display Helpers ────────────────────────────────────────────
|
|
1126
|
-
|
|
1219
|
+
GetChangeTypeCardClass(type) {
|
|
1127
1220
|
switch (type) {
|
|
1128
1221
|
case 'Create': return 'type-create';
|
|
1129
1222
|
case 'Update': return 'type-update';
|
|
@@ -1132,7 +1225,11 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1132
1225
|
default: return 'type-update';
|
|
1133
1226
|
}
|
|
1134
1227
|
}
|
|
1135
|
-
|
|
1228
|
+
/** @deprecated Use {@link GetChangeTypeCardClass}. */
|
|
1229
|
+
getChangeTypeCardClass(type) {
|
|
1230
|
+
return this.GetChangeTypeCardClass(type);
|
|
1231
|
+
}
|
|
1232
|
+
GetChangeTypeBadgeText(type) {
|
|
1136
1233
|
switch (type) {
|
|
1137
1234
|
case 'Create': return 'Created';
|
|
1138
1235
|
case 'Delete': return 'Deleted';
|
|
@@ -1140,36 +1237,52 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1140
1237
|
default: return type;
|
|
1141
1238
|
}
|
|
1142
1239
|
}
|
|
1240
|
+
/** @deprecated Use {@link GetChangeTypeBadgeText}. */
|
|
1241
|
+
getChangeTypeBadgeText(type) {
|
|
1242
|
+
return this.GetChangeTypeBadgeText(type);
|
|
1243
|
+
}
|
|
1143
1244
|
/**
|
|
1144
1245
|
* Badge text for the row's primary type tag. For restore rows we override
|
|
1145
1246
|
* the underlying `Type='Update'` and show "Restore" instead — matches the
|
|
1146
1247
|
* mockup's intent of treating restore as a first-class operation in the
|
|
1147
1248
|
* timeline rather than a flavor of update.
|
|
1148
1249
|
*/
|
|
1149
|
-
|
|
1150
|
-
if (this.
|
|
1250
|
+
GetEffectiveBadgeText(change) {
|
|
1251
|
+
if (this.IsRestoreRow(change))
|
|
1151
1252
|
return 'Restore';
|
|
1152
|
-
return this.
|
|
1253
|
+
return this.GetChangeTypeBadgeText(change.Type);
|
|
1254
|
+
}
|
|
1255
|
+
/** @deprecated Use {@link GetEffectiveBadgeText}. */
|
|
1256
|
+
getEffectiveBadgeText(change) {
|
|
1257
|
+
return this.GetEffectiveBadgeText(change);
|
|
1153
1258
|
}
|
|
1154
1259
|
/**
|
|
1155
1260
|
* Optional restore reason — pulled from the dynamic `RestoreReason`
|
|
1156
1261
|
* column added by the lineage migration. Returns null when not present
|
|
1157
1262
|
* or not a restore row.
|
|
1158
1263
|
*/
|
|
1159
|
-
|
|
1160
|
-
if (!this.
|
|
1264
|
+
GetRestoreReason(change) {
|
|
1265
|
+
if (!this.IsRestoreRow(change))
|
|
1161
1266
|
return null;
|
|
1162
1267
|
const reason = change.RestoreReason;
|
|
1163
1268
|
return reason && reason.trim().length > 0 ? reason : null;
|
|
1164
1269
|
}
|
|
1165
|
-
|
|
1270
|
+
/** @deprecated Use {@link GetRestoreReason}. */
|
|
1271
|
+
getRestoreReason(change) {
|
|
1272
|
+
return this.GetRestoreReason(change);
|
|
1273
|
+
}
|
|
1274
|
+
GetSourceClass(source) {
|
|
1166
1275
|
if (source === 'Restore')
|
|
1167
1276
|
return 'source-restore';
|
|
1168
1277
|
if (source === 'Internal')
|
|
1169
1278
|
return 'source-internal';
|
|
1170
1279
|
return 'source-external';
|
|
1171
1280
|
}
|
|
1172
|
-
|
|
1281
|
+
/** @deprecated Use {@link GetSourceClass}. */
|
|
1282
|
+
getSourceClass(source) {
|
|
1283
|
+
return this.GetSourceClass(source);
|
|
1284
|
+
}
|
|
1285
|
+
GetStatusClass(status) {
|
|
1173
1286
|
switch (status) {
|
|
1174
1287
|
case 'Complete': return 'status-complete';
|
|
1175
1288
|
case 'Pending': return 'status-pending';
|
|
@@ -1177,10 +1290,18 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1177
1290
|
default: return 'status-unknown';
|
|
1178
1291
|
}
|
|
1179
1292
|
}
|
|
1293
|
+
/** @deprecated Use {@link GetStatusClass}. */
|
|
1294
|
+
getStatusClass(status) {
|
|
1295
|
+
return this.GetStatusClass(status);
|
|
1296
|
+
}
|
|
1297
|
+
GetTimelineItemLabel(change) {
|
|
1298
|
+
return `${change.Type} by ${change.User || 'Unknown User'} on ${this.FormatFullDateTime(change.ChangedAt)}`;
|
|
1299
|
+
}
|
|
1300
|
+
/** @deprecated Use {@link GetTimelineItemLabel}. */
|
|
1180
1301
|
getTimelineItemLabel(change) {
|
|
1181
|
-
return
|
|
1302
|
+
return this.GetTimelineItemLabel(change);
|
|
1182
1303
|
}
|
|
1183
|
-
|
|
1304
|
+
GetUserInitials(user) {
|
|
1184
1305
|
if (!user)
|
|
1185
1306
|
return '?';
|
|
1186
1307
|
if (user.includes('@')) {
|
|
@@ -1193,25 +1314,41 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1193
1314
|
}
|
|
1194
1315
|
return user.substring(0, 2).toUpperCase();
|
|
1195
1316
|
}
|
|
1196
|
-
|
|
1317
|
+
/** @deprecated Use {@link GetUserInitials}. */
|
|
1318
|
+
getUserInitials(user) {
|
|
1319
|
+
return this.GetUserInitials(user);
|
|
1320
|
+
}
|
|
1321
|
+
GetUserDisplayName(user) {
|
|
1197
1322
|
if (!user)
|
|
1198
1323
|
return 'Unknown';
|
|
1199
1324
|
if (user.includes('@'))
|
|
1200
1325
|
return user.split('@')[0];
|
|
1201
1326
|
return user;
|
|
1202
1327
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1328
|
+
/** @deprecated Use {@link GetUserDisplayName}. */
|
|
1329
|
+
getUserDisplayName(user) {
|
|
1330
|
+
return this.GetUserDisplayName(user);
|
|
1331
|
+
}
|
|
1332
|
+
GetUniqueContributorCount() {
|
|
1333
|
+
const users = new Set(this.ViewData.map(c => c.User).filter(Boolean));
|
|
1205
1334
|
return users.size;
|
|
1206
1335
|
}
|
|
1207
|
-
|
|
1336
|
+
/** @deprecated Use {@link GetUniqueContributorCount}. */
|
|
1337
|
+
getUniqueContributorCount() {
|
|
1338
|
+
return this.GetUniqueContributorCount();
|
|
1339
|
+
}
|
|
1340
|
+
FormatTime(date) {
|
|
1208
1341
|
return new Intl.DateTimeFormat('en-US', {
|
|
1209
1342
|
hour: 'numeric',
|
|
1210
1343
|
minute: '2-digit',
|
|
1211
1344
|
hour12: true,
|
|
1212
1345
|
}).format(new Date(date));
|
|
1213
1346
|
}
|
|
1214
|
-
|
|
1347
|
+
/** @deprecated Use {@link FormatTime}. */
|
|
1348
|
+
formatTime(date) {
|
|
1349
|
+
return this.FormatTime(date);
|
|
1350
|
+
}
|
|
1351
|
+
FormatRelativeTime(date) {
|
|
1215
1352
|
const now = new Date();
|
|
1216
1353
|
const diffMs = now.getTime() - new Date(date).getTime();
|
|
1217
1354
|
const diffMins = Math.floor(diffMs / 60000);
|
|
@@ -1231,7 +1368,11 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1231
1368
|
year: diffDays > 365 ? 'numeric' : undefined,
|
|
1232
1369
|
}).format(new Date(date));
|
|
1233
1370
|
}
|
|
1234
|
-
|
|
1371
|
+
/** @deprecated Use {@link FormatRelativeTime}. */
|
|
1372
|
+
formatRelativeTime(date) {
|
|
1373
|
+
return this.FormatRelativeTime(date);
|
|
1374
|
+
}
|
|
1375
|
+
FormatFullDateTime(date) {
|
|
1235
1376
|
return new Intl.DateTimeFormat('en-US', {
|
|
1236
1377
|
year: 'numeric',
|
|
1237
1378
|
month: 'long',
|
|
@@ -1242,16 +1383,20 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1242
1383
|
timeZoneName: 'short',
|
|
1243
1384
|
}).format(new Date(date));
|
|
1244
1385
|
}
|
|
1386
|
+
/** @deprecated Use {@link FormatFullDateTime}. */
|
|
1387
|
+
formatFullDateTime(date) {
|
|
1388
|
+
return this.FormatFullDateTime(date);
|
|
1389
|
+
}
|
|
1245
1390
|
// ─── Change Summary ─────────────────────────────────────────────
|
|
1246
|
-
|
|
1391
|
+
GetChangeSummary(change) {
|
|
1247
1392
|
// Restore rows get a distinctive summary so the timeline reads like a
|
|
1248
1393
|
// sentence — "Restored to 5:56 PM version" rather than yet another
|
|
1249
1394
|
// "Name and Description changed". The lineage chip carries the full
|
|
1250
1395
|
// source-version detail.
|
|
1251
|
-
if (this.
|
|
1252
|
-
const src = this.
|
|
1396
|
+
if (this.IsRestoreRow(change)) {
|
|
1397
|
+
const src = this.GetRestoredFromSourceChange(change);
|
|
1253
1398
|
if (src)
|
|
1254
|
-
return `Restored to ${this.
|
|
1399
|
+
return `Restored to ${this.FormatTime(src.ChangedAt)} version`;
|
|
1255
1400
|
return 'Restored from earlier version';
|
|
1256
1401
|
}
|
|
1257
1402
|
if (change.Type === 'Create')
|
|
@@ -1271,7 +1416,11 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1271
1416
|
return change.ChangesDescription || 'Changes made';
|
|
1272
1417
|
}
|
|
1273
1418
|
}
|
|
1274
|
-
|
|
1419
|
+
/** @deprecated Use {@link GetChangeSummary}. */
|
|
1420
|
+
getChangeSummary(change) {
|
|
1421
|
+
return this.GetChangeSummary(change);
|
|
1422
|
+
}
|
|
1423
|
+
GetCreatedFieldCount(change) {
|
|
1275
1424
|
try {
|
|
1276
1425
|
if (!change.FullRecordJSON)
|
|
1277
1426
|
return 0;
|
|
@@ -1282,6 +1431,10 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1282
1431
|
return 0;
|
|
1283
1432
|
}
|
|
1284
1433
|
}
|
|
1434
|
+
/** @deprecated Use {@link GetCreatedFieldCount}. */
|
|
1435
|
+
getCreatedFieldCount(change) {
|
|
1436
|
+
return this.GetCreatedFieldCount(change);
|
|
1437
|
+
}
|
|
1285
1438
|
extractFieldDisplayNames(changesJson) {
|
|
1286
1439
|
return Object.keys(changesJson).map(fieldKey => {
|
|
1287
1440
|
const changeInfo = changesJson[fieldKey];
|
|
@@ -1302,7 +1455,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1302
1455
|
return `${fieldNames.slice(0, 3).join(', ')}, and ${remaining} other field${remaining > 1 ? 's' : ''} changed`;
|
|
1303
1456
|
}
|
|
1304
1457
|
// ─── Field Changes (type-aware) ────────────────────────────────
|
|
1305
|
-
|
|
1458
|
+
GetFieldChanges(change) {
|
|
1306
1459
|
try {
|
|
1307
1460
|
const changesJson = JSON.parse(change.ChangesJSON || '{}');
|
|
1308
1461
|
return Object.keys(changesJson).map(fieldKey => this.buildFieldChangeInfo(changesJson[fieldKey]));
|
|
@@ -1311,12 +1464,15 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1311
1464
|
return [];
|
|
1312
1465
|
}
|
|
1313
1466
|
}
|
|
1467
|
+
/** @deprecated Use {@link GetFieldChanges}. */
|
|
1468
|
+
getFieldChanges(change) {
|
|
1469
|
+
return this.GetFieldChanges(change);
|
|
1470
|
+
}
|
|
1314
1471
|
buildFieldChangeInfo(changeInfo) {
|
|
1315
1472
|
const field = this.record.EntityInfo.Fields.find((f) => f.Name.trim().toLowerCase() === changeInfo.field?.trim().toLowerCase());
|
|
1316
1473
|
const fieldType = this.classifyFieldType(field);
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
const formattedNew = this.formatChangeValue(changeInfo.newValue, isDateField);
|
|
1474
|
+
const formattedOld = FormatChangeValue(changeInfo.oldValue, field);
|
|
1475
|
+
const formattedNew = FormatChangeValue(changeInfo.newValue, field);
|
|
1320
1476
|
let diffHtml;
|
|
1321
1477
|
if (fieldType === 'text' && formattedOld !== formattedNew) {
|
|
1322
1478
|
diffHtml = this.generateDiffHtml(formattedOld, formattedNew);
|
|
@@ -1341,7 +1497,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1341
1497
|
return 'number';
|
|
1342
1498
|
return 'text';
|
|
1343
1499
|
}
|
|
1344
|
-
|
|
1500
|
+
GetCreatedFields(change) {
|
|
1345
1501
|
try {
|
|
1346
1502
|
if (!change.FullRecordJSON)
|
|
1347
1503
|
return [];
|
|
@@ -1352,38 +1508,18 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1352
1508
|
.map((field) => ({
|
|
1353
1509
|
name: field.Name,
|
|
1354
1510
|
displayName: field.DisplayNameOrName,
|
|
1355
|
-
value:
|
|
1511
|
+
value: FormatChangeValue(record[field.Name], field),
|
|
1356
1512
|
}));
|
|
1357
1513
|
}
|
|
1358
1514
|
catch {
|
|
1359
1515
|
return [];
|
|
1360
1516
|
}
|
|
1361
1517
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
return '';
|
|
1366
|
-
if (typeof value === 'object') {
|
|
1367
|
-
const keys = Object.keys(value);
|
|
1368
|
-
if (keys.length === 0)
|
|
1369
|
-
return '';
|
|
1370
|
-
return JSON.stringify(value);
|
|
1371
|
-
}
|
|
1372
|
-
if (isDateField && typeof value === 'string') {
|
|
1373
|
-
const date = new Date(value);
|
|
1374
|
-
if (!isNaN(date.getTime())) {
|
|
1375
|
-
return new Intl.DateTimeFormat('en-US', {
|
|
1376
|
-
month: 'short',
|
|
1377
|
-
day: 'numeric',
|
|
1378
|
-
year: 'numeric',
|
|
1379
|
-
hour: 'numeric',
|
|
1380
|
-
minute: '2-digit',
|
|
1381
|
-
hour12: true,
|
|
1382
|
-
}).format(date);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
return String(value);
|
|
1518
|
+
/** @deprecated Use {@link GetCreatedFields}. */
|
|
1519
|
+
getCreatedFields(change) {
|
|
1520
|
+
return this.GetCreatedFields(change);
|
|
1386
1521
|
}
|
|
1522
|
+
// ─── Value Formatting ───────────────────────────────────────────
|
|
1387
1523
|
// ─── Diff Generation (text fields only) ─────────────────────────
|
|
1388
1524
|
generateDiffHtml(oldValue, newValue) {
|
|
1389
1525
|
if (!oldValue && !newValue) {
|
|
@@ -1420,7 +1556,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1420
1556
|
return div.innerHTML;
|
|
1421
1557
|
}
|
|
1422
1558
|
static ɵfac = function RecordChangesComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RecordChangesComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone), i0.ɵɵdirectiveInject(i1.MJNotificationService), i0.ɵɵdirectiveInject(i2.DomSanitizer)); };
|
|
1423
|
-
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RecordChangesComponent, selectors: [["mj-record-changes"]], inputs: { record: "record", AllowRestore: "AllowRestore" }, outputs: { dialogClosed: "dialogClosed", RestoreRequested: "RestoreRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 5, vars: 13, consts: [[3, "Closed", "Mode", "Title", "Visible", "Resizable", "MinWidthPx", "MaxWidthRatio"], ["text", "Loading history...", "size", "medium"], [1, "rc-container"], [3, "RestoreConfirmed", "RestoreCancelled", "Visible", "Mode", "RecordChange", "LiveRecord"], [1, "rc-wizard-overlay"], [1, "rc-labels-section"], [1, "rc-labels-header"], [1, "rc-labels-title"], ["aria-hidden", "true", 1, "fa-solid", "fa-tags"], [1, "rc-labels-count"], ["title", "Create a version label for this record", 1, "rc-create-label-btn", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "rc-labels-loading"], ["Size", "compact", "Icon", "", "Title", "No version labels for this record yet.", 1, "rc-labels-empty"], [1, "rc-labels-list"], ["Icon", "fa-solid fa-clock-rotate-left", "Title", "No Change History", "Message", "This record doesn't have any tracked changes yet. Changes will appear here automatically as edits are made.", "Size", "large"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "rc-label-chip", 3, "title"], [1, "fa-solid", "fa-tag", "rc-label-chip-icon"], [1, "rc-label-chip-name"], [1, "rc-label-chip-meta"], [1, "rc-label-chip-status"], [1, "rc-label-chip-items"], [1, "rc-empty-state-hint"], [1, "fa-solid", "fa-shield-halved"], [1, "rc-page-header"], [1, "rc-page-title"], [1, "rc-page-subtitle"], [1, "rc-header-meta"], [1, "rc-entity-badge"], ["aria-hidden", "true", 1, "fa-solid", "fa-database"], [1, "rc-change-count"], [1, "rc-filter-bar"], [1, "rc-search-wrap"], ["aria-hidden", "true", 1, "fa-solid", "fa-search"], ["type", "text", "placeholder", "Search changes...", "aria-label", "Search record changes", 1, "rc-search-box", 3, "ngModelChange", "input", "ngModel"], ["type", "button", 1, "rc-filter-pill", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-layer-group"], [1, "rc-filter-results"], [1, "rc-timeline"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left"], [1, "rc-filter-overflow-wrap"], ["type", "button", 1, "rc-filter-pill", 3, "active", "rc-filter-pill-restore"], ["aria-hidden", "true"], [1, "rc-filter-pill-count"], ["type", "button", 1, "rc-filter-pill", "rc-filter-pill-overflow", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-sliders"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-filter-overflow-caret"], [1, "rc-filter-overflow-backdrop", 3, "click"], ["role", "menu", 1, "rc-filter-overflow-popover"], [1, "rc-filter-overflow-header"], [1, "rc-filter-overflow-row", 3, "rc-filter-overflow-row-restore"], [1, "rc-filter-overflow-actions"], ["type", "button", 1, "rc-filter-overflow-clear", 3, "click"], ["type", "button", 1, "rc-filter-overflow-done", 3, "click"], [1, "rc-filter-overflow-row"], ["type", "checkbox", 3, "change", "checked"], [1, "rc-filter-overflow-label"], [1, "rc-filter-overflow-count"], [1, "rc-clear-filters-link", 3, "click"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left", 3, "Action"], [1, "rc-date-group"], [1, "rc-date-label"], [1, "rc-card", 3, "class", "expanded", "rc-card-restore", "rc-card-highlight"], [1, "rc-card", 3, "keydown"], [1, "rc-card-header", 3, "click"], [1, "rc-card-header-left"], [1, "rc-card-type-badge"], [1, "rc-card-summary"], [1, "rc-card-meta"], [1, "rc-card-user"], [1, "rc-avatar"], [1, "rc-card-time", 3, "title"], [1, "rc-card-source"], [1, "rc-card-status"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-card-chevron"], [1, "rc-card-body"], [1, "rc-restored-chip", 3, "title"], ["title", "Source version was not loaded into this view", 1, "rc-restored-chip", "rc-restored-chip-orphan"], [1, "rc-restored-chip", 3, "click", "title"], ["aria-hidden", "true", 1, "fa-solid", "fa-link"], ["aria-hidden", "true", 1, "fa-solid", "fa-clock-rotate-left"], [1, "rc-restore-reason"], [1, "rc-deletion-note"], [1, "rc-comments"], [1, "rc-errors"], [1, "rc-restore-action"], [1, "rc-restore-reason-label"], [1, "rc-restore-reason-text"], [1, "rc-field-row"], [1, "rc-field-label"], [1, "rc-field-values"], [1, "rc-val-new"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"], [1, "rc-bool-change"], [1, "rc-atomic-change"], [1, "rc-text-diff", 3, "innerHTML"], [1, "rc-bool-dot"], [1, "rc-bool-label", "old"], ["aria-hidden", "true", 1, "fa-solid", "fa-arrow-right", "rc-val-arrow"], [1, "rc-bool-label", "new"], [1, "rc-val-old"], ["aria-hidden", "true", 1, "fa-solid", "fa-comment"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle"], [1, "rc-error-log"], ["title", "Restore the entire record to its state at this version", 1, "rc-restore-btn", 3, "click"], [1, "rc-wizard-overlay", 3, "click"], [1, "rc-wizard-container", 3, "click"], [3, "Created", "Cancel", "PreselectedEntity", "PreselectedRecordIds"]], template: function RecordChangesComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1559
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RecordChangesComponent, selectors: [["mj-record-changes"]], inputs: { record: "record", AllowRestore: "AllowRestore" }, outputs: { DialogClosed: "DialogClosed", dialogClosed: "dialogClosed", RestoreRequested: "RestoreRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 5, vars: 13, consts: [[3, "Closed", "Mode", "Title", "Visible", "Resizable", "MinWidthPx", "MaxWidthRatio"], ["text", "Loading history...", "size", "medium"], [1, "rc-container"], [3, "RestoreConfirmed", "RestoreCancelled", "Visible", "Mode", "RecordChange", "LiveRecord"], [1, "rc-wizard-overlay"], [1, "rc-labels-section"], [1, "rc-labels-header"], [1, "rc-labels-title"], ["aria-hidden", "true", 1, "fa-solid", "fa-tags"], [1, "rc-labels-count"], ["title", "Create a version label for this record", 1, "rc-create-label-btn", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "rc-labels-loading"], ["Size", "compact", "Icon", "", "Title", "No version labels for this record yet.", 1, "rc-labels-empty"], [1, "rc-labels-list"], ["Icon", "fa-solid fa-clock-rotate-left", "Title", "No Change History", "Message", "This record doesn't have any tracked changes yet. Changes will appear here automatically as edits are made.", "Size", "large"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "rc-label-chip", 3, "title"], [1, "fa-solid", "fa-tag", "rc-label-chip-icon"], [1, "rc-label-chip-name"], [1, "rc-label-chip-meta"], [1, "rc-label-chip-status"], [1, "rc-label-chip-items"], [1, "rc-empty-state-hint"], [1, "fa-solid", "fa-shield-halved"], [1, "rc-page-header"], [1, "rc-page-title"], [1, "rc-page-subtitle"], [1, "rc-header-meta"], [1, "rc-entity-badge"], ["aria-hidden", "true", 1, "fa-solid", "fa-database"], [1, "rc-change-count"], [1, "rc-filter-bar"], [1, "rc-search-wrap"], ["aria-hidden", "true", 1, "fa-solid", "fa-search"], ["type", "text", "placeholder", "Search changes...", "aria-label", "Search record changes", 1, "rc-search-box", 3, "ngModelChange", "input", "ngModel"], ["type", "button", 1, "rc-filter-pill", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-layer-group"], [1, "rc-filter-results"], [1, "rc-timeline"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left"], [1, "rc-filter-overflow-wrap"], ["type", "button", 1, "rc-filter-pill", 3, "active", "rc-filter-pill-restore"], ["aria-hidden", "true"], [1, "rc-filter-pill-count"], ["type", "button", 1, "rc-filter-pill", "rc-filter-pill-overflow", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-sliders"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-filter-overflow-caret"], [1, "rc-filter-overflow-backdrop", 3, "click"], ["role", "menu", 1, "rc-filter-overflow-popover"], [1, "rc-filter-overflow-header"], [1, "rc-filter-overflow-row", 3, "rc-filter-overflow-row-restore"], [1, "rc-filter-overflow-actions"], ["type", "button", 1, "rc-filter-overflow-clear", 3, "click"], ["type", "button", 1, "rc-filter-overflow-done", 3, "click"], [1, "rc-filter-overflow-row"], ["type", "checkbox", 3, "change", "checked"], [1, "rc-filter-overflow-label"], [1, "rc-filter-overflow-count"], [1, "rc-clear-filters-link", 3, "click"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left", 3, "Action"], [1, "rc-date-group"], [1, "rc-date-label"], [1, "rc-card", 3, "class", "expanded", "rc-card-restore", "rc-card-highlight"], [1, "rc-card", 3, "keydown"], [1, "rc-card-header", 3, "click"], [1, "rc-card-header-left"], [1, "rc-card-type-badge"], [1, "rc-card-summary"], [1, "rc-card-meta"], [1, "rc-card-user"], [1, "rc-avatar"], [1, "rc-card-time", 3, "title"], [1, "rc-card-source"], [1, "rc-card-status"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-card-chevron"], [1, "rc-card-body"], [1, "rc-restored-chip", 3, "title"], ["title", "Source version was not loaded into this view", 1, "rc-restored-chip", "rc-restored-chip-orphan"], [1, "rc-restored-chip", 3, "click", "title"], ["aria-hidden", "true", 1, "fa-solid", "fa-link"], ["aria-hidden", "true", 1, "fa-solid", "fa-clock-rotate-left"], [1, "rc-restore-reason"], [1, "rc-deletion-note"], [1, "rc-comments"], [1, "rc-errors"], [1, "rc-restore-action"], [1, "rc-restore-reason-label"], [1, "rc-restore-reason-text"], [1, "rc-field-row"], [1, "rc-field-label"], [1, "rc-field-values"], [1, "rc-val-new"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"], [1, "rc-bool-change"], [1, "rc-atomic-change"], [1, "rc-text-diff", 3, "innerHTML"], [1, "rc-bool-dot"], [1, "rc-bool-label", "old"], ["aria-hidden", "true", 1, "fa-solid", "fa-arrow-right", "rc-val-arrow"], [1, "rc-bool-label", "new"], [1, "rc-val-old"], ["aria-hidden", "true", 1, "fa-solid", "fa-comment"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle"], [1, "rc-error-log"], ["title", "Restore the entire record to its state at this version", 1, "rc-restore-btn", 3, "click"], [1, "rc-wizard-overlay", 3, "click"], [1, "rc-wizard-container", 3, "click"], [3, "Created", "Cancel", "PreselectedEntity", "PreselectedRecordIds"]], template: function RecordChangesComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1424
1560
|
i0.ɵɵelementStart(0, "mj-slide-panel", 0);
|
|
1425
1561
|
i0.ɵɵlistener("Closed", function RecordChangesComponent_Template_mj_slide_panel_Closed_0_listener() { return ctx.OnClose(); });
|
|
1426
1562
|
i0.ɵɵconditionalCreate(1, RecordChangesComponent_Conditional_1_Template, 1, 0, "mj-loading", 1);
|
|
@@ -1445,7 +1581,9 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1445
1581
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RecordChangesComponent, [{
|
|
1446
1582
|
type: Component,
|
|
1447
1583
|
args: [{ standalone: false, selector: 'mj-record-changes', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<mj-slide-panel\n [Mode]=\"'slide'\"\n [Title]=\"'Record Changes History'\"\n [Visible]=\"IsVisible\"\n [Resizable]=\"true\"\n [MinWidthPx]=\"400\"\n [MaxWidthRatio]=\"0.6\"\n (Closed)=\"OnClose()\">\n\n @if (IsLoading) {\n <mj-loading text=\"Loading history...\" size=\"medium\"></mj-loading>\n }\n\n @if (!IsLoading) {\n <div class=\"rc-container\">\n <!-- Version Labels Section -->\n <div class=\"rc-labels-section\">\n <div class=\"rc-labels-header\">\n <div class=\"rc-labels-title\">\n <i class=\"fa-solid fa-tags\" aria-hidden=\"true\"></i>\n <span>Version Labels</span>\n @if (RecordLabels.length > 0) {\n <span class=\"rc-labels-count\">{{ RecordLabels.length }}</span>\n }\n </div>\n <button class=\"rc-create-label-btn\" (click)=\"OpenCreateWizard()\" title=\"Create a version label for this record\">\n <i class=\"fa-solid fa-plus\"></i> Create Label\n </button>\n </div>\n @if (IsLoadingLabels) {\n <div class=\"rc-labels-loading\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Loading labels...\n </div>\n } @else if (RecordLabels.length === 0) {\n <mj-empty-state class=\"rc-labels-empty\" Size=\"compact\"\n Icon=\"\" Title=\"No version labels for this record yet.\" />\n } @else {\n <div class=\"rc-labels-list\">\n @for (label of RecordLabels; track label.ID) {\n <div class=\"rc-label-chip\" [title]=\"label.Description || label.Name\">\n <i class=\"fa-solid fa-tag rc-label-chip-icon\"></i>\n <span class=\"rc-label-chip-name\">{{ label.Name }}</span>\n <span class=\"rc-label-chip-meta\">\n <span class=\"rc-label-chip-status\" [class]=\"getLabelStatusClass(label.Status)\">{{ label.Status }}</span>\n <span class=\"rc-label-chip-items\">{{ label.ItemCount }} item{{ label.ItemCount !== 1 ? 's' : '' }}</span>\n </span>\n </div>\n }\n </div>\n }\n </div>\n\n @if (viewData.length === 0) {\n <!-- Empty state -->\n <mj-empty-state\n Icon=\"fa-solid fa-clock-rotate-left\"\n Title=\"No Change History\"\n Message=\"This record doesn't have any tracked changes yet. Changes will appear here automatically as edits are made.\"\n Size=\"large\">\n <div class=\"rc-empty-state-hint\">\n <i class=\"fa-solid fa-shield-halved\"></i>\n <span>Record change tracking is managed at the entity level</span>\n </div>\n </mj-empty-state>\n } @else {\n <!-- Page Header -->\n <div class=\"rc-page-header\">\n <div>\n <h2 class=\"rc-page-title\">Change History</h2>\n <div class=\"rc-page-subtitle\">{{ record.EntityInfo.Name }}</div>\n </div>\n <div class=\"rc-header-meta\">\n <div class=\"rc-entity-badge\">\n <i class=\"fa-solid fa-database\" aria-hidden=\"true\"></i>\n {{ record.EntityInfo.Name }}\n </div>\n <div class=\"rc-change-count\">{{ viewData.length }} change{{ viewData.length !== 1 ? 's' : '' }} · {{ getUniqueContributorCount() }} contributor{{ getUniqueContributorCount() !== 1 ? 's' : '' }}</div>\n </div>\n </div>\n\n <!-- Filter Bar \u2014 chips render only for change types/sources actually present in loaded data.\n When more than 2 conditional chips would render, they collapse into a \"More filters\" popover. -->\n <div class=\"rc-filter-bar\">\n <div class=\"rc-search-wrap\">\n <i class=\"fa-solid fa-search\" aria-hidden=\"true\"></i>\n <input\n class=\"rc-search-box\"\n type=\"text\"\n placeholder=\"Search changes...\"\n [(ngModel)]=\"searchTerm\"\n (input)=\"onSearchChange()\"\n aria-label=\"Search record changes\"\n />\n </div>\n\n <button class=\"rc-filter-pill\" [class.active]=\"IsAllSelected\" (click)=\"SelectAllPill()\" type=\"button\">\n <i class=\"fa-solid fa-layer-group\" aria-hidden=\"true\"></i> All\n </button>\n\n @if (HasConditionalPills) {\n @if (!UseOverflowPopover) {\n <!-- 1-2 conditional pills: render inline -->\n @for (pill of ConditionalPills; track pill.Key) {\n <button class=\"rc-filter-pill\"\n [class.active]=\"ChipSelections[pill.Key]\"\n [class.rc-filter-pill-restore]=\"pill.Variant === 'restore'\"\n (click)=\"TogglePill(pill.Key)\"\n type=\"button\">\n <i class=\"fa-solid {{ pill.Icon }}\" aria-hidden=\"true\"></i>\n {{ pill.Label }}\n <span class=\"rc-filter-pill-count\">{{ pill.Count }}</span>\n </button>\n }\n } @else {\n <!-- 3+ conditional pills: overflow popover -->\n <div class=\"rc-filter-overflow-wrap\">\n <button class=\"rc-filter-pill rc-filter-pill-overflow\"\n [class.active]=\"SelectedConditionalCount > 0\"\n (click)=\"ToggleFilterOverflow()\"\n type=\"button\">\n <i class=\"fa-solid fa-sliders\" aria-hidden=\"true\"></i>\n More filters\n @if (SelectedConditionalCount > 0) {\n <span class=\"rc-filter-pill-count\">{{ SelectedConditionalCount }}</span>\n }\n <i class=\"fa-solid fa-chevron-down rc-filter-overflow-caret\" aria-hidden=\"true\"></i>\n </button>\n\n @if (ShowFilterOverflow) {\n <!-- Backdrop: clicks close the popover -->\n <div class=\"rc-filter-overflow-backdrop\" (click)=\"CloseFilterOverflow()\"></div>\n <div class=\"rc-filter-overflow-popover\" role=\"menu\">\n <div class=\"rc-filter-overflow-header\">Filter by</div>\n @for (pill of ConditionalPills; track pill.Key) {\n <label class=\"rc-filter-overflow-row\" [class.rc-filter-overflow-row-restore]=\"pill.Variant === 'restore'\">\n <input type=\"checkbox\"\n [checked]=\"ChipSelections[pill.Key]\"\n (change)=\"TogglePill(pill.Key)\" />\n <i class=\"fa-solid {{ pill.Icon }}\" aria-hidden=\"true\"></i>\n <span class=\"rc-filter-overflow-label\">{{ pill.Label }}</span>\n <span class=\"rc-filter-overflow-count\">{{ pill.Count }}</span>\n </label>\n }\n <div class=\"rc-filter-overflow-actions\">\n <button class=\"rc-filter-overflow-clear\" (click)=\"SelectAllPill(); CloseFilterOverflow()\" type=\"button\">\n Clear all\n </button>\n <button class=\"rc-filter-overflow-done\" (click)=\"CloseFilterOverflow()\" type=\"button\">\n Done\n </button>\n </div>\n </div>\n }\n </div>\n }\n }\n </div>\n\n @if (filteredData.length !== viewData.length) {\n <div class=\"rc-filter-results\">\n Showing {{ filteredData.length }} of {{ viewData.length }} changes\n <button class=\"rc-clear-filters-link\" (click)=\"ClearFilters()\">Clear filters</button>\n </div>\n }\n\n <!-- Timeline -->\n <div class=\"rc-timeline\" [attr.aria-label]=\"'Timeline of changes for ' + record.EntityInfo.Name + ' record'\">\n @if (filteredData.length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Icon=\"fa-solid fa-filter\"\n Title=\"No changes match your current filters\"\n ActionText=\"Clear filters\"\n ActionIcon=\"fa-solid fa-rotate-left\"\n (Action)=\"ClearFilters()\" />\n } @else {\n @for (group of dateGroups; track group.label) {\n <div class=\"rc-date-group\">\n <div class=\"rc-date-label\">{{ group.label }}</div>\n\n @for (change of group.changes; track change.ID) {\n <div\n class=\"rc-card\"\n [attr.data-change-id]=\"change.ID\"\n [class]=\"getChangeTypeCardClass(change.Type)\"\n [class.expanded]=\"expandedItems.has(change.ID)\"\n [class.rc-card-restore]=\"isRestoreRow(change)\"\n [class.rc-card-highlight]=\"HighlightedChangeID === change.ID\"\n [attr.tabindex]=\"0\"\n [attr.aria-expanded]=\"expandedItems.has(change.ID)\"\n [attr.aria-label]=\"getTimelineItemLabel(change)\"\n (keydown)=\"onTimelineItemKeydown($event, change.ID)\"\n >\n <!-- Card Header -->\n <div class=\"rc-card-header\" (click)=\"toggleExpansion(change.ID)\">\n <div class=\"rc-card-header-left\">\n <span class=\"rc-card-type-badge\"\n [class.rc-card-type-badge-restore]=\"isRestoreRow(change)\">{{ getEffectiveBadgeText(change) }}</span>\n <span class=\"rc-card-summary\">{{ getChangeSummary(change) }}</span>\n\n <!-- Lineage chip \u2014 visible when this row was produced by a restore -->\n @if (isRestoreRow(change)) {\n @if (getRestoredFromSourceChange(change); as srcChange) {\n <a class=\"rc-restored-chip\"\n (click)=\"JumpToSourceChange(srcChange, $event)\"\n [title]=\"'Jump to source version (' + formatFullDateTime(srcChange.ChangedAt) + ')'\">\n <i class=\"fa-solid fa-link\" aria-hidden=\"true\"></i>\n Restored from {{ formatTime(srcChange.ChangedAt) }} {{ srcChange.ChangedAt ? 'on ' + (srcChange.ChangedAt | date:'mediumDate') : '' }}\n @if (srcChange.User) {\n by {{ getUserDisplayName(srcChange.User) }}\n }\n </a>\n } @else {\n <span class=\"rc-restored-chip rc-restored-chip-orphan\"\n title=\"Source version was not loaded into this view\">\n <i class=\"fa-solid fa-clock-rotate-left\" aria-hidden=\"true\"></i>\n Restored from earlier version\n </span>\n }\n }\n </div>\n <div class=\"rc-card-meta\">\n <div class=\"rc-card-user\">\n <div class=\"rc-avatar\">{{ getUserInitials(change.User) }}</div>\n {{ getUserDisplayName(change.User) }}\n </div>\n <span class=\"rc-card-time\" [title]=\"formatFullDateTime(change.ChangedAt)\">{{ formatTime(change.ChangedAt) }}</span>\n <span class=\"rc-card-source\" [class]=\"getSourceClass(change.Source)\">{{ change.Source }}</span>\n <span class=\"rc-card-status\" [class]=\"getStatusClass(change.Status)\">{{ change.Status }}</span>\n <i class=\"fa-solid fa-chevron-down rc-card-chevron\" aria-hidden=\"true\"></i>\n </div>\n </div>\n\n <!-- Card Body (expanded) -->\n @if (expandedItems.has(change.ID)) {\n <div class=\"rc-card-body\">\n <!-- Restore reason (when present) \u2014 Notion/Linear-style\n blockquote: a thin violet left bar + italic text,\n with a tiny \"Reason\" prefix. No box, no header,\n no large vertical footprint. Reads as a\n justification note rather than a callout. -->\n @if (getRestoreReason(change); as reason) {\n <blockquote class=\"rc-restore-reason\">\n <span class=\"rc-restore-reason-label\">Reason</span>\n <span class=\"rc-restore-reason-text\">{{ reason }}</span>\n </blockquote>\n }\n\n @if (change.Type === 'Create') {\n @if (change.FullRecordJSON) {\n @for (field of getCreatedFields(change); track field.name) {\n <div class=\"rc-field-row\">\n <div class=\"rc-field-label\">{{ field.displayName }}</div>\n <div class=\"rc-field-values\">\n <span class=\"rc-val-new\">{{ field.value }}</span>\n </div>\n </div>\n }\n }\n } @else if (change.Type === 'Delete') {\n <div class=\"rc-deletion-note\">\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n This record was permanently removed from the system.\n </div>\n } @else {\n <!-- Update / Snapshot / Restore: type-aware field changes -->\n @for (fc of getFieldChanges(change); track fc.field) {\n <div class=\"rc-field-row\">\n <div class=\"rc-field-label\">{{ fc.displayName }}</div>\n <div class=\"rc-field-values\">\n @if (fc.fieldType === 'boolean') {\n <div class=\"rc-bool-change\">\n <span class=\"rc-bool-dot\" [class.on]=\"fc.oldValue === 'true' || fc.oldValue === '1'\" [class.off]=\"fc.oldValue !== 'true' && fc.oldValue !== '1'\"></span>\n <span class=\"rc-bool-label old\">{{ fc.oldValue === 'true' || fc.oldValue === '1' ? 'Yes' : 'No' }}</span>\n <i class=\"fa-solid fa-arrow-right rc-val-arrow\" aria-hidden=\"true\"></i>\n <span class=\"rc-bool-dot\" [class.on]=\"fc.newValue === 'true' || fc.newValue === '1'\" [class.off]=\"fc.newValue !== 'true' && fc.newValue !== '1'\"></span>\n <span class=\"rc-bool-label new\" [class.active]=\"fc.newValue === 'true' || fc.newValue === '1'\" [class.inactive]=\"fc.newValue !== 'true' && fc.newValue !== '1'\">{{ fc.newValue === 'true' || fc.newValue === '1' ? 'Yes' : 'No' }}</span>\n </div>\n } @else if (fc.fieldType === 'date' || fc.fieldType === 'number') {\n <div class=\"rc-atomic-change\">\n <span class=\"rc-val-old\">{{ fc.oldValue || '(empty)' }}</span>\n <i class=\"fa-solid fa-arrow-right rc-val-arrow\" aria-hidden=\"true\"></i>\n <span class=\"rc-val-new\">{{ fc.newValue || '(empty)' }}</span>\n </div>\n } @else if (fc.diffHtml) {\n <div class=\"rc-text-diff\" [innerHTML]=\"fc.diffHtml\"></div>\n } @else {\n <div class=\"rc-atomic-change\">\n <span class=\"rc-val-old\">{{ fc.oldValue || '(empty)' }}</span>\n <i class=\"fa-solid fa-arrow-right rc-val-arrow\" aria-hidden=\"true\"></i>\n <span class=\"rc-val-new\">{{ fc.newValue || '(empty)' }}</span>\n </div>\n }\n </div>\n </div>\n }\n }\n\n @if (change.Comments) {\n <div class=\"rc-comments\">\n <i class=\"fa-solid fa-comment\" aria-hidden=\"true\"></i>\n {{ change.Comments }}\n </div>\n }\n\n @if (change.ErrorLog) {\n <div class=\"rc-errors\">\n <i class=\"fa-solid fa-exclamation-triangle\" aria-hidden=\"true\"></i>\n <pre class=\"rc-error-log\">{{ change.ErrorLog }}</pre>\n </div>\n }\n\n <!-- Restore action: shown for Update, Create, and Snapshot rows.\n Hidden on Delete rows (handled via the Recycle Bin path)\n and on the most recent change (restoring to current state is a no-op). -->\n @if (AllowRestore && change.Type !== 'Delete' && !isMostRecentChange(change)) {\n <div class=\"rc-restore-action\">\n <button class=\"rc-restore-btn\"\n (click)=\"OnRestoreVersion(change, $event)\"\n title=\"Restore the entire record to its state at this version\">\n <i class=\"fa-solid fa-clock-rotate-left\" aria-hidden=\"true\"></i>\n Restore record to this version\n </button>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Restore preview slide-in (reusable component) -->\n <mj-restore-preview-panel\n [Visible]=\"RestorePreviewVisible\"\n [Mode]=\"'live'\"\n [RecordChange]=\"RestorePreviewChange\"\n [LiveRecord]=\"record\"\n (RestoreConfirmed)=\"OnRestorePanelConfirmed($event)\"\n (RestoreCancelled)=\"OnRestorePanelCancelled()\">\n </mj-restore-preview-panel>\n\n @if (ShowCreateWizard) {\n <div class=\"rc-wizard-overlay\" (click)=\"OnLabelCreateCancelled()\">\n <div class=\"rc-wizard-container\" (click)=\"$event.stopPropagation()\">\n <mj-label-create\n [PreselectedEntity]=\"record.EntityInfo\"\n [PreselectedRecordIds]=\"[record.PrimaryKey.KeyValuePairs[0].Value]\"\n (Created)=\"OnLabelCreated($event)\"\n (Cancel)=\"OnLabelCreateCancelled()\">\n </mj-label-create>\n </div>\n </div>\n }\n</mj-slide-panel>\n", styles: ["/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Record Changes \u2014 Option A \"Clean Timeline\"\n Linear / Notion-inspired minimal timeline design\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n/* \u2500\u2500\u2500 Container \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.rc-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n padding: 20px 24px;\n background: var(--mj-bg-surface-card);\n}\n\n/* \u2500\u2500\u2500 Version Labels Section \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.rc-labels-section {\n background: var(--mj-bg-surface-card);\n border-radius: 8px;\n padding: 12px 16px;\n margin-bottom: 20px;\n border: 1px solid var(--mj-border-default);\n}\n\n.rc-labels-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.rc-labels-title {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n font-size: 13px;\n color: var(--mj-text-secondary);\n}\n\n.rc-labels-title i { color: var(--mj-brand-primary); }\n\n.rc-labels-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 6px;\n border-radius: 10px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 10px;\n font-weight: 700;\n}\n\n.rc-create-label-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 5px 12px;\n border: 1px solid var(--mj-brand-primary);\n border-radius: 6px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.rc-create-label-btn:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.rc-labels-loading {\n font-size: 12px;\n color: var(--mj-text-muted);\n padding: 4px 0;\n}\n\n.rc-labels-loading i { margin-right: 4px; }\n\n.rc-labels-empty {\n font-size: 12px;\n color: var(--mj-text-disabled);\n font-style: italic;\n padding: 4px 0;\n}\n\n.rc-labels-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.rc-label-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 10px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 16px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n transition: all 0.15s ease;\n cursor: default;\n}\n\n.rc-label-chip:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.rc-label-chip-icon { color: var(--mj-brand-primary); font-size: 10px; }\n\n.rc-label-chip-name {\n font-weight: 600;\n max-width: 180px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-label-chip-meta {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n\n.rc-label-chip-status {\n padding: 1px 5px;\n border-radius: 4px;\n font-weight: 600;\n text-transform: uppercase;\n font-size: 9px;\n letter-spacing: 0.03em;\n}\n\n.label-status-active { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--mj-status-success); }\n.label-status-archived { background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary); }\n.label-status-restored { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n\n.rc-label-chip-items { white-space: nowrap; }\n\n/* \u2500\u2500\u2500 Empty State (hint projected into <mj-empty-state> default slot) \u2500\u2500\u2500 */\n.rc-empty-state-hint {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-top: var(--mj-space-4);\n padding: 8px 16px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 20px;\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n\n.rc-empty-state-hint i { color: var(--mj-text-disabled); }\n\n/* \u2500\u2500\u2500 Page Header \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.rc-page-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 24px;\n padding-bottom: 20px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-page-title {\n font-size: 20px;\n font-weight: 700;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.rc-page-subtitle {\n font-size: 13px;\n color: var(--mj-text-muted);\n margin-top: 4px;\n}\n\n.rc-header-meta { text-align: right; }\n\n.rc-entity-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 12px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, var(--mj-bg-surface));\n}\n\n.rc-change-count {\n font-size: 12px;\n color: var(--mj-text-disabled);\n margin-top: 6px;\n}\n\n/* \u2500\u2500\u2500 Filter Bar \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.rc-filter-bar {\n display: flex;\n gap: 8px;\n margin-bottom: 20px;\n flex-wrap: wrap;\n align-items: center;\n}\n\n.rc-search-wrap {\n position: relative;\n flex: 1;\n min-width: 180px;\n}\n\n.rc-search-wrap i {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-disabled);\n font-size: 13px;\n pointer-events: none;\n}\n\n.rc-search-box {\n width: 100%;\n padding: 8px 12px 8px 36px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n font-size: 13px;\n background: var(--mj-bg-surface);\n outline: none;\n color: var(--mj-text-primary);\n transition: border-color 0.15s, box-shadow 0.15s;\n}\n\n.rc-search-box:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.rc-search-box::placeholder { color: var(--mj-text-disabled); }\n\n.rc-filter-pill {\n padding: 7px 14px;\n border-radius: 8px;\n font-size: 12px;\n font-weight: 500;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n cursor: pointer;\n color: var(--mj-text-secondary);\n transition: all 0.15s;\n display: flex;\n align-items: center;\n gap: 5px;\n}\n\n.rc-filter-pill:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.rc-filter-pill.active {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary);\n}\n\n.rc-filter-results {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin-bottom: 16px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.rc-clear-filters-link {\n background: none;\n border: none;\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n padding: 0;\n text-decoration: underline;\n}\n\n.rc-clear-filters-link:hover { color: var(--mj-brand-primary-hover); }\n\n/* \u2500\u2500\u2500 Timeline \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.rc-timeline {\n position: relative;\n flex: 1;\n overflow-y: auto;\n}\n\n/* Vertical line running through the timeline */\n.rc-timeline::before {\n content: '';\n position: absolute;\n left: 15px;\n top: 24px;\n bottom: 0;\n width: 2px;\n background: var(--mj-border-default);\n}\n\n/* \u2500\u2500\u2500 Date Group \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.rc-date-group { margin-bottom: 8px; }\n\n.rc-date-label {\n position: relative;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.8px;\n color: var(--mj-text-disabled);\n padding: 16px 0 12px 44px;\n}\n\n/* Date dot on the timeline line */\n.rc-date-label::before {\n content: '';\n position: absolute;\n left: 10px;\n top: 50%;\n transform: translateY(-50%);\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--mj-border-default);\n border: 2px solid var(--mj-bg-surface-card);\n}\n\n/* \u2500\u2500\u2500 Change Card \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.rc-card {\n position: relative;\n margin-left: 44px;\n margin-bottom: 12px;\n background: var(--mj-bg-surface);\n border-radius: 12px;\n border: 1px solid var(--mj-border-default);\n transition: all 0.2s;\n overflow: hidden;\n}\n\n.rc-card:hover {\n border-color: var(--mj-border-strong);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.rc-card:focus {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n/* Colored dot on the timeline line for each card */\n.rc-card::before {\n content: '';\n position: absolute;\n left: -23px;\n top: 20px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n z-index: 1;\n}\n\n.rc-card.type-update::before {\n background: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.rc-card.type-create::before {\n background: var(--mj-status-success);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-success) 15%, transparent);\n}\n\n.rc-card.type-delete::before {\n background: var(--mj-status-error);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-error) 15%, transparent);\n}\n\n/* \u2500\u2500\u2500 Card Header \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.rc-card-header {\n padding: 14px 18px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n user-select: none;\n gap: 12px;\n}\n\n.rc-card-header-left {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n flex: 1;\n /* Allow the lineage chip to wrap to a second line on narrow panel widths\n instead of squeezing the title text into a \"Name and Description c...\"\n truncation. The badge stays on the first line; chip wraps gracefully. */\n flex-wrap: wrap;\n row-gap: 6px;\n}\n\n.rc-card-type-badge {\n padding: 3px 10px;\n border-radius: 5px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.3px;\n text-transform: uppercase;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.type-update .rc-card-type-badge { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n.type-create .rc-card-type-badge { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--mj-status-success); }\n.type-delete .rc-card-type-badge { background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface)); color: var(--mj-status-error); }\n\n.rc-card-summary {\n font-size: 13px;\n color: var(--mj-text-secondary);\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-card-meta {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 12px;\n color: var(--mj-text-disabled);\n flex-shrink: 0;\n}\n\n.rc-card-user {\n display: flex;\n align-items: center;\n gap: 5px;\n white-space: nowrap;\n}\n\n.rc-avatar {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 8px;\n font-weight: 700;\n color: var(--mj-text-inverse);\n flex-shrink: 0;\n}\n\n.rc-card-time { white-space: nowrap; }\n\n.rc-card-source {\n font-size: 10px;\n padding: 2px 6px;\n border-radius: 4px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.source-internal { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n.source-external { background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface)); color: var(--mj-status-warning); }\n\n.rc-card-status {\n font-size: 10px;\n padding: 2px 6px;\n border-radius: 4px;\n font-weight: 600;\n text-transform: uppercase;\n white-space: nowrap;\n}\n\n.status-complete { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--mj-status-success); }\n.status-pending { background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface)); color: var(--mj-status-warning); }\n.status-error { background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface)); color: var(--mj-status-error); }\n.status-unknown { background: var(--mj-bg-surface-sunken); color: var(--mj-text-muted); }\n\n.rc-card-chevron {\n color: var(--mj-text-disabled);\n transition: transform 0.2s;\n font-size: 12px;\n flex-shrink: 0;\n}\n\n.rc-card.expanded .rc-card-chevron { transform: rotate(180deg); }\n\n/* \u2500\u2500\u2500 Card Body \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.rc-card-body {\n border-top: 1px solid var(--mj-border-default);\n}\n\n/* \u2500\u2500\u2500 Field Row \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.rc-field-row {\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-field-row:last-child { border-bottom: none; }\n\n.rc-field-label {\n width: 140px;\n min-width: 140px;\n padding: 12px 16px;\n font-size: 12px;\n font-weight: 600;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-card);\n display: flex;\n align-items: center;\n border-right: 1px solid var(--mj-border-default);\n}\n\n.rc-field-values {\n flex: 1;\n padding: 12px 16px;\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n/* \u2500\u2500\u2500 Atomic Change (numbers, dates) \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.rc-atomic-change {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.rc-val-old {\n font-size: 13px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-disabled);\n text-decoration: line-through;\n font-weight: 400;\n}\n\n.rc-val-arrow {\n color: var(--mj-border-strong);\n font-size: 11px;\n flex-shrink: 0;\n}\n\n.rc-val-new {\n font-size: 13px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-primary);\n font-weight: 500;\n}\n\n/* \u2500\u2500\u2500 Boolean Change \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.rc-bool-change {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.rc-bool-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.rc-bool-dot.on { background: var(--mj-status-success); }\n.rc-bool-dot.off { background: var(--mj-border-strong); }\n\n.rc-bool-label {\n font-size: 13px;\n font-weight: 500;\n}\n\n.rc-bool-label.old {\n color: var(--mj-text-disabled);\n text-decoration: line-through;\n}\n\n.rc-bool-label.new.active { color: var(--mj-status-success); }\n.rc-bool-label.new.inactive { color: var(--mj-text-disabled); }\n\n/* \u2500\u2500\u2500 Text Diff (strings only) \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.rc-text-diff {\n font-size: 13px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n line-height: 1.6;\n word-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.rc-diff-added {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n padding: 1px 3px;\n border-radius: 3px;\n}\n\n.rc-diff-removed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n padding: 1px 3px;\n border-radius: 3px;\n text-decoration: line-through;\n}\n\n.rc-diff-unchanged {\n color: var(--mj-text-secondary);\n}\n\n/* \u2500\u2500\u2500 Deletion Note \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.rc-deletion-note {\n padding: 16px 18px;\n color: var(--mj-text-muted);\n font-size: 13px;\n font-style: italic;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.rc-deletion-note i {\n color: var(--mj-status-error);\n font-size: 14px;\n}\n\n/* \u2500\u2500\u2500 Comments & Errors \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.rc-comments {\n padding: 12px 18px;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n font-size: 13px;\n color: var(--mj-brand-primary);\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.rc-comments i {\n color: var(--mj-brand-primary);\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n.rc-errors {\n padding: 12px 18px;\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n font-size: 13px;\n color: var(--mj-status-error);\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.rc-errors i {\n color: var(--mj-status-error);\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n.rc-error-log {\n background: var(--mj-bg-surface);\n padding: 8px;\n border-radius: 4px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n font-size: 12px;\n color: var(--mj-status-error);\n margin: 0;\n white-space: pre-wrap;\n overflow-x: auto;\n flex: 1;\n}\n\n/* \u2500\u2500\u2500 Create Wizard Overlay \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.rc-wizard-overlay {\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: rc-fadeIn 0.15s ease;\n}\n\n.rc-wizard-container {\n background: var(--mj-bg-surface);\n border-radius: 12px;\n box-shadow: var(--mj-shadow-lg);\n width: 90%;\n max-width: 600px;\n max-height: 80vh;\n overflow-y: auto;\n padding: 24px;\n animation: rc-slideUp 0.2s ease;\n}\n\n@keyframes rc-fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes rc-slideUp {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* \u2500\u2500\u2500 Responsive \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@media (max-width: 768px) {\n .rc-card-header {\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n }\n\n .rc-card-meta {\n flex-wrap: wrap;\n gap: 8px;\n }\n\n .rc-field-row {\n flex-direction: column;\n }\n\n .rc-field-label {\n width: 100%;\n min-width: unset;\n border-right: none;\n border-bottom: 1px solid var(--mj-border-default);\n padding: 8px 16px;\n }\n\n .rc-field-values {\n padding: 8px 16px;\n }\n\n .rc-atomic-change {\n flex-wrap: wrap;\n }\n\n .rc-filter-bar {\n flex-direction: column;\n align-items: stretch;\n }\n\n .rc-search-wrap { min-width: unset; }\n\n .rc-filter-pill { justify-content: center; }\n\n .rc-page-header {\n flex-direction: column;\n gap: 12px;\n }\n\n .rc-header-meta { text-align: left; }\n}\n\n@media (max-width: 480px) {\n .rc-container { padding: 12px 16px; }\n\n .rc-card { margin-left: 36px; }\n\n .rc-date-label { padding-left: 36px; }\n\n .rc-card::before { left: -19px; }\n\n .rc-timeline::before { left: 12px; }\n\n .rc-date-label::before { left: 7px; }\n}\n\n/* \u2500\u2500\u2500 Restore Action \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.rc-restore-action {\n display: flex;\n justify-content: flex-end;\n padding-top: 10px;\n margin-top: 10px;\n /* Breathing room from the card's bottom and right edges so the button\n doesn't sit flush against the border. */\n padding-right: 10px;\n padding-bottom: 10px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.rc-restore-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n font-size: 12px;\n font-weight: 500;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n border-radius: 6px;\n cursor: pointer;\n transition: all 150ms ease;\n font-family: inherit;\n line-height: 1;\n}\n\n.rc-restore-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n box-shadow: 0 2px 6px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.rc-restore-btn:active {\n transform: translateY(1px);\n box-shadow: none;\n}\n\n.rc-restore-btn i {\n font-size: 13px;\n}\n\n/* \u2500\u2500\u2500 Restore Preview Panel \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.rc-restore-preview-backdrop {\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: rc-fadeIn 0.15s ease;\n}\n\n.rc-restore-preview-panel {\n background: var(--mj-bg-surface);\n border-radius: 12px;\n box-shadow: var(--mj-shadow-lg);\n width: 90%;\n max-width: 700px;\n max-height: 80vh;\n overflow-y: auto;\n padding: 0;\n animation: rc-slideUp 0.2s ease;\n}\n\n.rc-restore-preview-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px 20px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-preview-title {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.rc-restore-preview-title i {\n color: var(--mj-brand-primary);\n}\n\n.rc-restore-preview-close {\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 16px;\n transition: color 0.15s, background 0.15s;\n}\n\n.rc-restore-preview-close:hover {\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface-hover);\n}\n\n.rc-restore-preview-meta {\n display: flex;\n gap: 20px;\n padding: 12px 20px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-preview-empty {\n padding: 24px 20px;\n text-align: center;\n color: var(--mj-text-muted);\n font-size: 13px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n}\n\n.rc-restore-preview-empty i {\n color: var(--mj-text-disabled);\n}\n\n/* Restore Preview Table */\n.rc-restore-preview-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n\n.rc-restore-preview-table th {\n text-align: left;\n padding: 10px 16px;\n font-weight: 600;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-preview-table td {\n padding: 10px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n vertical-align: top;\n}\n\n.rc-restore-preview-table tr:last-child td {\n border-bottom: none;\n}\n\n.rc-restore-preview-table tr.rc-diff-changed {\n background: color-mix(in srgb, var(--mj-status-warning) 5%, var(--mj-bg-surface));\n}\n\n.rc-restore-preview-table tr.rc-diff-unchanged {\n opacity: 0.6;\n}\n\n.rc-restore-field-name {\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n}\n\n.rc-restore-current {\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-secondary);\n max-width: 200px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-restore-version {\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-primary);\n font-weight: 500;\n max-width: 200px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-restore-indicator {\n text-align: center;\n width: 30px;\n}\n\n.rc-diff-changed .rc-restore-indicator i {\n color: var(--mj-status-warning);\n}\n\n.rc-diff-unchanged .rc-restore-indicator i {\n color: var(--mj-status-success);\n font-size: 12px;\n}\n\n/* Restore Preview Actions */\n.rc-restore-preview-actions {\n display: flex;\n gap: 10px;\n padding: 16px 20px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-confirm-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-inverse);\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s, box-shadow 0.15s;\n font-family: inherit;\n}\n\n.rc-restore-confirm-btn:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n}\n\n.rc-restore-confirm-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.rc-restore-cancel-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 500;\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.15s;\n font-family: inherit;\n}\n\n.rc-restore-cancel-btn:hover:not(:disabled) {\n border-color: var(--mj-border-strong);\n color: var(--mj-text-primary);\n}\n\n.rc-restore-cancel-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\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\n * Restore lineage + conditional filter chips + overflow popover\n * (added with the Restore Prior Version feature)\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 */\n\n/* \u2500\u2500\u2500 Filter pill enhancements \u2500\u2500\u2500 */\n\n.rc-filter-pill-count {\n margin-left: 4px;\n background: color-mix(in srgb, currentColor 12%, transparent);\n padding: 1px 6px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 700;\n}\n\n.rc-filter-pill.active .rc-filter-pill-count {\n background: color-mix(in srgb, var(--mj-text-inverse, #fff) 22%, transparent);\n}\n\n/* Restore-variant pill: violet treatment to set it apart from type pills */\n.rc-filter-pill-restore {\n background: color-mix(in srgb, var(--mj-status-info) 8%, var(--mj-bg-surface));\n color: color-mix(in srgb, var(--mj-status-info) 80%, var(--mj-text-secondary));\n border-color: color-mix(in srgb, var(--mj-status-info) 30%, var(--mj-border-default));\n}\n.rc-filter-pill-restore:hover {\n border-color: var(--mj-status-info);\n color: var(--mj-status-info);\n}\n.rc-filter-pill-restore.active {\n background: var(--mj-status-info);\n color: var(--mj-text-inverse, #fff);\n border-color: var(--mj-status-info);\n}\n\n/* \u2500\u2500\u2500 Overflow filter popover \u2500\u2500\u2500 */\n\n.rc-filter-overflow-wrap {\n position: relative;\n display: inline-block;\n}\n\n.rc-filter-pill-overflow {\n /* Overflow trigger inherits the standard filter pill look */\n}\n\n.rc-filter-overflow-caret {\n font-size: 9px;\n margin-left: 2px;\n}\n\n.rc-filter-overflow-backdrop {\n position: fixed;\n inset: 0;\n z-index: 10;\n}\n\n.rc-filter-overflow-popover {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 11;\n min-width: 240px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n box-shadow: 0 14px 40px rgba(0, 0, 0, 0.18);\n padding: 8px;\n}\n\n.rc-filter-overflow-header {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-text-muted);\n font-weight: 600;\n padding: 6px 8px 8px;\n}\n\n.rc-filter-overflow-row {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 8px;\n border-radius: 6px;\n font-size: 13px;\n color: var(--mj-text-primary);\n cursor: pointer;\n}\n\n.rc-filter-overflow-row:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.rc-filter-overflow-row input[type=\"checkbox\"] {\n margin: 0;\n accent-color: var(--mj-brand-primary);\n}\n\n.rc-filter-overflow-row i {\n color: var(--mj-text-muted);\n font-size: 12px;\n width: 14px;\n text-align: center;\n}\n\n.rc-filter-overflow-row-restore i {\n color: var(--mj-status-info);\n}\n\n.rc-filter-overflow-label {\n flex: 1;\n}\n\n.rc-filter-overflow-count {\n color: var(--mj-text-muted);\n font-size: 11px;\n font-weight: 500;\n}\n\n.rc-filter-overflow-actions {\n display: flex;\n justify-content: space-between;\n border-top: 1px solid var(--mj-border-subtle);\n padding-top: 8px;\n margin-top: 6px;\n}\n\n.rc-filter-overflow-clear,\n.rc-filter-overflow-done {\n background: none;\n border: none;\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n padding: 4px 10px;\n border-radius: 5px;\n}\n\n.rc-filter-overflow-clear {\n color: var(--mj-text-muted);\n}\n.rc-filter-overflow-clear:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.rc-filter-overflow-done {\n color: var(--mj-brand-primary);\n}\n.rc-filter-overflow-done:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n}\n\n/* \u2500\u2500\u2500 Snapshot type badge + card accent \u2500\u2500\u2500 */\n\n.type-snapshot .rc-card-type-badge {\n background: color-mix(in srgb, var(--mj-text-muted) 15%, var(--mj-bg-surface));\n color: var(--mj-text-secondary);\n}\n\n/* \u2500\u2500\u2500 Restore Source pill (in card meta) \u2500\u2500\u2500 */\n\n.source-restore {\n background: color-mix(in srgb, var(--mj-status-info) 12%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n}\n\n/* \u2500\u2500\u2500 Restore card variant: violet left accent \u2500\u2500\u2500 */\n\n.rc-card.rc-card-restore {\n border-left: 3px solid var(--mj-status-info);\n}\n\n/* \u2500\u2500\u2500 Lineage chip \u2500\u2500\u2500 */\n\n/* Lineage chip \u2014 outline-only \"interactive link\" treatment so it reads as\n distinct from the filled RESTORE badge sitting next to it. The badge says\n \"this row IS a restore\" (filled, static); the chip says \"click here to\n see the source version\" (outline, hoverable). */\n.rc-restored-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n margin-left: 8px;\n padding: 2px 9px;\n border-radius: 11px;\n font-size: 11px;\n font-weight: 500;\n background: transparent;\n color: color-mix(in srgb, var(--mj-status-info) 90%, var(--mj-text-primary));\n border: 1px solid color-mix(in srgb, var(--mj-status-info) 40%, var(--mj-border-default));\n cursor: pointer;\n text-decoration: none;\n white-space: nowrap;\n transition: background-color 0.12s, border-color 0.12s, color 0.12s;\n}\n\n.rc-restored-chip:hover {\n background: color-mix(in srgb, var(--mj-status-info) 12%, transparent);\n border-color: var(--mj-status-info);\n color: var(--mj-status-info);\n}\n\n.rc-restored-chip i {\n font-size: 10px;\n}\n\n.rc-restored-chip-orphan {\n cursor: default;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-muted);\n border-color: var(--mj-border-default);\n}\n\n.rc-restored-chip-orphan:hover {\n background: var(--mj-bg-surface-card);\n border-color: var(--mj-border-default);\n}\n\n/* \u2500\u2500\u2500 Card highlight (when jumped-to via lineage chip) \u2500\u2500\u2500 */\n\n.rc-card.rc-card-highlight {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n animation: rc-card-pulse 1.4s ease-in-out;\n}\n\n@keyframes rc-card-pulse {\n 0%, 100% { background: var(--mj-bg-surface); }\n 50% { background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface)); }\n}\n\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\n * Restore-row badge override + Restore Reason block\n * (mockup-aligned treatment for restore rows in the timeline)\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 */\n\n/* The base type-update badge styling would otherwise paint a restore row's\n badge blue with \"Update\" text. Override to violet + \"Restore\" text so it\n matches the source pill, lineage chip, and card border. */\n.rc-card-type-badge.rc-card-type-badge-restore {\n background: color-mix(in srgb, var(--mj-status-info) 14%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n}\n\n/* Restore reason \u2014 minimal blockquote treatment.\n A thin violet left bar + italic text reads like a margin note rather than\n a heavy callout. Visually connected to the restore family (left bar uses\n the same violet as the badge / source pill / card border) without\n competing with the field-diff rows that follow. */\n.rc-restore-reason {\n /* Indented in from the card body edge so it visually nests one level\n deeper than the field-diff rows that follow, and given generous\n vertical breathing room so it doesn't crowd the lineage chip above\n or the first field row below. */\n margin: 14px 8px 18px 18px;\n padding: 0 0 0 12px;\n border-left: 2px solid color-mix(in srgb, var(--mj-status-info) 55%, var(--mj-border-default));\n display: flex;\n align-items: baseline;\n gap: 8px;\n font-size: 13px;\n line-height: 1.5;\n}\n\n.rc-restore-reason-label {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n color: color-mix(in srgb, var(--mj-status-info) 75%, var(--mj-text-muted));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.rc-restore-reason-text {\n color: var(--mj-text-secondary);\n font-style: italic;\n}\n"] }]
|
|
1448
|
-
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i1.MJNotificationService }, { type: i2.DomSanitizer }], {
|
|
1584
|
+
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i1.MJNotificationService }, { type: i2.DomSanitizer }], { DialogClosed: [{
|
|
1585
|
+
type: Output
|
|
1586
|
+
}], dialogClosed: [{
|
|
1449
1587
|
type: Output
|
|
1450
1588
|
}], record: [{
|
|
1451
1589
|
type: Input
|
|
@@ -1454,5 +1592,5 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1454
1592
|
}], RestoreRequested: [{
|
|
1455
1593
|
type: Output
|
|
1456
1594
|
}] }); })();
|
|
1457
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RecordChangesComponent, { className: "RecordChangesComponent", filePath: "src/lib/ng-record-changes.component.ts", lineNumber:
|
|
1595
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RecordChangesComponent, { className: "RecordChangesComponent", filePath: "src/lib/ng-record-changes.component.ts", lineNumber: 121 }); })();
|
|
1458
1596
|
//# sourceMappingURL=ng-record-changes.component.js.map
|