import { EventEmitter, OnDestroy, OnInit, ChangeDetectorRef, NgZone } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { BaseEntity, CompositeKey } from '@memberjunction/core';
import { MJRecordChangeEntity } from '@memberjunction/core-entities';
import { MJNotificationService } from '@memberjunction/ng-notifications';
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
import { RestoreCommitEvent } from './restore-preview-panel/restore-preview-panel.component';
import * as i0 from "@angular/core";
/** Lightweight shape for displaying a version label associated with this record */
interface RecordLabel {
ID: string;
Name: string;
Description: string | null;
Scope: string;
Status: string;
CreatedAt: Date;
ItemCount: number;
}
/**
* Event payload emitted when the user confirms a restore.
*
* The host component (typically `record-form-container`) is responsible for:
* 1. Reloading the live record (concurrency safety),
* 2. Calling `record.SetRestoreContext(SourceChangeID, Reason)`,
* 3. Calling `record.Set(FieldName, Value)` for each entry in `FieldValues`,
* 4. Calling `record.Save()`,
* 5. Calling `record.ClearRestoreContext()` after the save returns.
*
* The provider will write a new RecordChange row with `Source='Restore'`,
* `RestoredFromID = SourceChangeID`, and `RestoreReason = Reason` —
* producing the auditable lineage chain.
*/
export interface RestoreVersionEvent {
/** ID of the historical RecordChange row whose state is being restored. */
SourceChangeID: string;
/** When the historical change was made. */
ChangedAt: Date;
/** Display name / email of who made the historical change. */
ChangedByUser: string;
/** Optional user-entered reason for the restore. */
Reason: string | null;
/** Selected field values, ready to pass to BaseEntity.Set(). */
FieldValues: Array<{
FieldName: string;
Value: unknown;
}>;
}
/** A single field change with type-aware rendering info (timeline display only). */
export interface FieldChangeInfo {
field: string;
displayName: string;
oldValue: string;
newValue: string;
fieldType: 'boolean' | 'date' | 'number' | 'text';
diffHtml?: SafeHtml;
}
/** A group of changes that share the same date. */
export interface DateGroup {
label: string;
changes: MJRecordChangeEntity[];
}
/** A conditional filter pill (one per type/source actually present in loaded data). */
export interface FilterPill {
/** Unique key for selection state ('Update', 'Create', 'Delete', 'Snapshot', 'Restore'). */
Key: string;
/** User-facing label (pluralized: 'Updates', 'Restored', etc.). */
Label: string;
/** Font Awesome icon class. */
Icon: string;
/** Number of changes of this kind in the loaded data. */
Count: number;
/** Selection style — neutral type pill or violet restore pill. */
Variant: 'type' | 'restore';
}
/**
* Slide-out timeline of all changes to a single record. Hosts the reusable
* {@link RestorePreviewPanelComponent} for the actual restore confirmation
* flow, and exposes a `RestoreRequested` event the host can act on to
* persist the restore.
*
* @example
*
*
*/
export declare class RecordChangesComponent extends BaseAngularComponent implements OnInit, OnDestroy {
private cdr;
private ngZone;
private mjNotificationService;
private sanitizer;
IsLoading: boolean;
IsVisible: boolean;
dialogClosed: EventEmitter;
record: BaseEntity;
/** Whether to show a "Restore" button on each historical version row. Default false. */
AllowRestore: boolean;
/**
* Emitted when the user confirms a restore in the preview panel.
* The host is responsible for applying the snapshot to the record and
* saving with `record.SetRestoreContext()` set first.
*/
RestoreRequested: EventEmitter;
viewData: MJRecordChangeEntity[];
filteredData: MJRecordChangeEntity[];
/**
* Change lookup keyed by NormalizeUUID(ID), rebuilt whenever {@link viewData}
* loads. Lets {@link getRestoredFromSourceChange} resolve a restore row's
* source change in O(1) instead of an O(rows) `UUIDsEqual` scan — which was
* previously executed twice per restore row per CD cycle (once in the
* template `@if` and once inside `getChangeSummary`), giving O(rows^2)/CD.
*/
private viewDataById;
dateGroups: DateGroup[];
expandedItems: Set;
/** The change record currently selected for restore preview, or null. */
RestorePreviewChange: MJRecordChangeEntity | null;
/** Visibility of the embedded restore preview slide-in. */
RestorePreviewVisible: boolean;
/** Whether the restore commit is in progress (between confirmation and host response). */
IsRestoring: boolean;
RecordLabels: RecordLabel[];
IsLoadingLabels: boolean;
ShowCreateWizard: boolean;
searchTerm: string;
/** Single selected type filter (legacy, kept for backwards compat). */
selectedType: string;
selectedSource: string;
/** Map of Key → selected, used by the conditional chip system. */
ChipSelections: Record;
/** Whether the overflow popover is open. */
ShowFilterOverflow: boolean;
/** Highlighted change ID for the lineage-jump indicator (transient). */
HighlightedChangeID: string | null;
/**
* Conditional filter pills derived from loaded data. Always includes 'All'
* implicitly. Other pills only render when at least one matching change
* exists. Overflows into a popover when the count exceeds 2.
*/
ConditionalPills: FilterPill[];
/** Threshold above which conditional chips collapse into the overflow popover. */
private readonly OVERFLOW_THRESHOLD;
/**
* Subscription to the record's BaseEntity event stream. We listen for
* 'save' events and auto-refresh the timeline so the user immediately
* sees the new RecordChange row produced by either a normal save or a
* restore. Cleaned up in ngOnDestroy.
*/
private _entitySaveSub;
constructor(cdr: ChangeDetectorRef, ngZone: NgZone, mjNotificationService: MJNotificationService, sanitizer: DomSanitizer);
ngOnInit(): void;
ngOnDestroy(): void;
/**
* Wires up an event handler on the live record so the timeline auto-
* refreshes immediately after any save lands — including the one
* produced by the restore flow. Without this the user sees their record
* update in the form but the panel above still shows the pre-restore
* change list, which is confusing.
*/
private subscribeToRecordSaves;
/**
* Reloads the record changes list from the database.
* Called by the parent container after a save completes while the drawer is open.
*/
Refresh(): void;
OnClose(): void;
LoadRecordChanges(pkey: CompositeKey, appName: string, entityName: string): Promise;
onSearchChange(): void;
onFilterChange(): void;
/**
* Toggles the "All" pill — clears every conditional selection.
*/
SelectAllPill(): void;
/**
* Toggles a conditional pill on/off.
*/
TogglePill(key: string): void;
/**
* Returns true when no conditional pills are selected (i.e. "All" mode).
*/
get IsAllSelected(): boolean;
/**
* Whether any conditional pills are actually visible at all.
*/
get HasConditionalPills(): boolean;
/**
* True when the conditional chips should collapse into an overflow popover
* because there are more than the threshold.
*/
get UseOverflowPopover(): boolean;
/** Number of currently selected conditional pills (for the popover trigger label). */
get SelectedConditionalCount(): number;
ToggleFilterOverflow(): void;
CloseFilterOverflow(): void;
ClearFilters(): void;
/**
* Builds the list of conditional pills from currently loaded data —
* a pill only appears for change types/sources that actually exist.
*/
private rebuildConditionalPills;
private isRestoreChange;
private applyFilters;
LoadRecordLabels(): Promise;
OpenCreateWizard(): void;
OnLabelCreated(event: {
LabelCount: number;
ItemCount: number;
}): void;
OnLabelCreateCancelled(): void;
getLabelStatusClass(status: string): string;
toggleExpansion(changeId: string): void;
onTimelineItemKeydown(event: KeyboardEvent, changeId: string): void;
/**
* Opens the embedded restore preview panel for a given change record.
* The panel computes a full-record diff (current vs the change's
* FullRecordJSON snapshot) — this is the semantic-correctness fix:
* we restore TO the state at that point in time, not just undo the
* one delta the user clicked.
*/
OnRestoreVersion(change: MJRecordChangeEntity, event: MouseEvent): void;
/**
* Called when the user confirms the restore in the embedded panel.
* Translates the panel's RestoreCommitEvent into the broader-shape
* RestoreVersionEvent the host is wired to handle.
*/
OnRestorePanelConfirmed(commit: RestoreCommitEvent): void;
/**
* Called when the user dismisses the restore preview without confirming.
*/
OnRestorePanelCancelled(): void;
/**
* Returns the source change for a given restored row, if found in the
* currently loaded changes. Returns null when the source isn't loaded
* (e.g., it's been pruned from history) or when this row isn't a restore.
*/
getRestoredFromSourceChange(change: MJRecordChangeEntity): MJRecordChangeEntity | null;
/**
* Rebuilds {@link viewDataById} from the current {@link viewData}. Called once
* per data load so source-change resolution is an O(1) Map read.
*/
private rebuildViewDataIndex;
/**
* True when the row was produced by a restore operation (either the
* `Source` is `'Restore'` OR `RestoredFromID` is populated).
*/
isRestoreRow(change: MJRecordChangeEntity): boolean;
/**
* True when this change is the most recent in the loaded history.
* `viewData` is sorted DESC by `ChangedAt`, so the most recent is index 0.
* Restoring to the most recent version is a no-op, so the timeline hides
* the Restore button on this row.
*/
isMostRecentChange(change: MJRecordChangeEntity): boolean;
/**
* Click handler for the lineage chip — scrolls/highlights the source
* change row in the timeline. Auto-clears after a few seconds.
*/
JumpToSourceChange(sourceChange: MJRecordChangeEntity, event: MouseEvent): void;
private buildDateGroups;
private formatDateGroupLabel;
getChangeTypeCardClass(type: string): string;
getChangeTypeBadgeText(type: string): string;
/**
* Badge text for the row's primary type tag. For restore rows we override
* the underlying `Type='Update'` and show "Restore" instead — matches the
* mockup's intent of treating restore as a first-class operation in the
* timeline rather than a flavor of update.
*/
getEffectiveBadgeText(change: MJRecordChangeEntity): string;
/**
* Optional restore reason — pulled from the dynamic `RestoreReason`
* column added by the lineage migration. Returns null when not present
* or not a restore row.
*/
getRestoreReason(change: MJRecordChangeEntity): string | null;
getSourceClass(source: string): string;
getStatusClass(status: string): string;
getTimelineItemLabel(change: MJRecordChangeEntity): string;
getUserInitials(user: string | null): string;
getUserDisplayName(user: string | null): string;
getUniqueContributorCount(): number;
formatTime(date: Date): string;
formatRelativeTime(date: Date): string;
formatFullDateTime(date: Date): string;
getChangeSummary(change: MJRecordChangeEntity): string;
getCreatedFieldCount(change: MJRecordChangeEntity): number;
private extractFieldDisplayNames;
private buildFieldListSummary;
getFieldChanges(change: MJRecordChangeEntity): FieldChangeInfo[];
private buildFieldChangeInfo;
private classifyFieldType;
getCreatedFields(change: MJRecordChangeEntity): Array<{
name: string;
displayName: string;
value: string;
}>;
private formatChangeValue;
private generateDiffHtml;
private shouldUseWordDiff;
private escapeHtml;
static ɵfac: i0.ɵɵFactoryDeclaration;
static ɵcmp: i0.ɵɵComponentDeclaration;
}
export {};
//# sourceMappingURL=ng-record-changes.component.d.ts.map