Angular components for browsing and restoring a record's change history. Renders the RecordChange timeline as a slide-in panel with type-aware diffs, version-label chips, restore lineage, and a reusable preview panel for the actual restore operation.
flowchart LR
subgraph Timeline["RecordChangesComponent"]
TL[Slide-in panel] --> CARDS[Date-grouped change cards]
CARDS --> DIFFS[Type-aware diffs
boolean / date / number / text]
CARDS --> CHIP[Restored-from lineage chip]
FILTERS[Conditional filter pills] --> POP[Overflow popover
when 3+ pills]
end
subgraph Restore["RestorePreviewPanelComponent"]
PV[Slide-in preview] --> ROWS[Field-by-field rows
with checkboxes]
ROWS --> MODE{Mode}
MODE -->|live| DIFF[current vs snapshot]
MODE -->|undelete| SNAP[snapshot only]
ROWS --> COMMIT[BeforeRestoreCommit
cancelable]
end
Timeline -- "user clicks Restore" --> Restore
Restore -- "RestoreConfirmed event
(host applies + saves)" --> HOST[Host component]
style Timeline fill:#264FAF,stroke:#1e3f8c,color:#fff
style Restore fill:#7c3aed,stroke:#5b21b6,color:#fff
style HOST fill:#16a34a,stroke:#15803d,color:#fff
npm install @memberjunction/ng-record-changes
import { RecordChangesModule } from '@memberjunction/ng-record-changes';
@NgModule({
imports: [RecordChangesModule]
})
export class MyModule { }
<mj-record-changes
[record]="myEntity"
[AllowRestore]="true"
(dialogClosed)="showHistory = false"
(RestoreRequested)="onRestoreRequested($event)">
</mj-record-changes>
async onRestoreRequested(event: RestoreVersionEvent) {
// Apply each selected snapshot field
for (const fv of event.FieldValues) {
this.myEntity.Set(fv.FieldName, fv.Value);
}
// Mark the next save as a restore so the provider populates lineage columns
this.myEntity.SetRestoreContext(event.SourceChangeID, event.Reason);
try {
await this.myEntity.Save();
} finally {
this.myEntity.ClearRestoreContext();
}
}
The host is responsible for the actual save so consumers can intercept (custom approval, audit logging, etc.). record-form-container already wires this end-to-end.
<mj-restore-preview-panel
[Visible]="showPreview"
[Mode]="'undelete'"
[RecordChange]="deletedChange"
[EntityName]="'Customers'"
(RestoreConfirmed)="onUndelete($event)"
(RestoreCancelled)="showPreview = false">
</mj-restore-preview-panel>
RecordChangesComponent (mj-record-changes)The slide-in timeline of all changes to a single record. Hosts the reusable RestorePreviewPanelComponent for the actual restore confirmation flow.
| Input | Type | Default | Description |
|---|---|---|---|
record |
BaseEntity |
— | Required. The live record whose change history will be displayed. |
AllowRestore |
boolean |
false |
When true, renders a "Restore record to this version" button on each change card and exposes the RestoreRequested event. |
AllowFormPreview |
boolean |
true |
When true, renders a "Preview in Form" button on each change card that has FullRecordJSON so the host form can rehydrate and preview that snapshot in place. |
PreviewedChangeID |
string | null |
null |
ID of the historical MJRecordChangeEntity currently being previewed in the host form. Highlights the active version card in the timeline. |
| Output | Event Type | Description |
|---|---|---|
dialogClosed |
void |
Emitted when the user closes the slide-in. |
RestoreRequested |
RestoreVersionEvent |
Emitted after the user confirms a restore in the preview panel. The host is responsible for applying the snapshot and saving. |
PreviewVersionSelected |
PreviewVersionEvent | null |
Emitted when the user clicks "Preview in Form" on a historical version ({ Change, Index, TotalCount }) or exits preview (null). Wired automatically by <mj-record-form-container>. |
ActivityChartBuckets): When a record has 2+ changes, a collapsible 12-month bar chart appears in the timeline header showing monthly change volume. Clicking any month bar filters the timeline to changes in that month (SelectedMonthKey); clicking again clears the filter.ShowLabelsSection): Version labels are tucked behind a single-line header pill (Labels (N) + + Label quick action) and stay collapsed by default when 0 labels exist so the change timeline gets maximum vertical space.RestoreVersionEvent| Property | Type | Description |
|---|---|---|
SourceChangeID |
string |
ID of the historical RecordChange row whose state is being restored. Pass to BaseEntity.SetRestoreContext(). |
ChangedAt |
Date |
When the historical change was made. |
ChangedByUser |
string |
Display name / email of who made the historical change. |
Reason |
string | null |
Optional user-entered reason. Pass to BaseEntity.SetRestoreContext(). |
FieldValues |
Array<{ FieldName; Value }> |
Selected snapshot field values, ready to pass to BaseEntity.Set(). |
The filter bar renders only chips for change types/sources that actually exist in the loaded data — empty types never show. When more than two conditional chips would render they collapse into a "More filters ▾" popover with checkboxes. The "All" chip is always present.
Change rows where RestoredFromID is populated render a violet "Restored from {time} by {user}" chip. Clicking the chip scrolls to and highlights the source row in the same timeline.
RestorePreviewPanelComponent (mj-restore-preview-panel)Reusable slide-in that previews a restore operation against a historical MJRecordChangeEntity and lets the user confirm with field-level granularity. Used by both RecordChangesComponent (for live-record restores) and RecycleBinComponent (for un-delete inserts).
| Input | Type | Default | Description |
|---|---|---|---|
Visible |
boolean |
false |
Controls panel visibility. |
Mode |
'live' | 'undelete' |
'live' |
live shows current-vs-snapshot diff; undelete shows snapshot only (the live record no longer exists). |
RecordChange |
MJRecordChangeEntity |
null |
Required. The historical change row whose state will be restored. The component reads FullRecordJSON to determine the target state. Any Type (Create, Update, Delete, Snapshot) is a valid restore source. |
LiveRecord |
BaseEntity | null |
null |
The current live record to diff against. Required in live mode, ignored in undelete mode. |
EntityName |
string | null |
null |
Required in undelete mode (where there's no LiveRecord to read it from). Optional in live mode. |
RequireReason |
boolean |
false |
When true, the Restore button disables until the user enters a non-empty reason. |
HideReason |
boolean |
false |
When true, hides the optional reason text area entirely. |
| Output | Event Type | Description |
|---|---|---|
BeforeRestoreCommit |
BeforeRestoreCommitEvent |
Cancelable. Fires when the user clicks Restore but before RestoreConfirmed. Set cancel = true to abort. |
RestoreConfirmed |
RestoreCommitEvent |
Fires after the user confirms (and BeforeRestoreCommit was not cancelled). Host applies the field values and saves. |
RestoreCancelled |
void |
Fires when the user dismisses the preview without restoring. |
RestoreCommitEvent| Property | Type | Description |
|---|---|---|
SourceChangeID |
string |
ID of the source RecordChange row. |
Reason |
string | null |
Optional user-entered reason. |
FieldValues |
Array<{ FieldName; Value }> |
Selected field values, ready for BaseEntity.Set(). |
AllRows |
RestoreFieldRow[] |
Full preview rows including unselected, for audit/logging. |
Mode |
'live' | 'undelete' |
The mode the panel was operating in. |
The preview compares the full snapshot captured in the source change's FullRecordJSON to the current live record (or to nothing in undelete mode). It does NOT roll back a single delta — restoring v2 means "make the record look like it did at v2", not "undo v3's changes".
This also means any change row is a valid restore target: Create (the original state), Update (post-update state), Snapshot (an explicit point-in-time capture from the Version Label system), or Delete (state before deletion — used by the Recycle Bin).
Record change histories are often highly irregular over time (for example, an initial creation burst, months of quiet, and a recent cluster of edits) or span several years with hundreds of changes. <mj-record-changes> embeds the reusable <mj-burst-cluster-strip> (MJBurstClusterStripComponent) from @memberjunction/ng-ui-components:
⋯ 11 mos quiet ⋯ gap connectors instead of leaving 80%+ of linear time buckets empty.BurstEraZoomThreshold (default 10) across multiple months/years, the strip automatically coalesces into Year or Month era cards (2023, 2024, 2025, 2026). Clicking any era zooms the strip into that period's bursts with a ← All (N) breadcrumb pill.<mj-record-changes>:
[BurstResolution]: 'auto' | 'session' | 'day' | 'week' | 'month' | 'quarter' | 'year' (default 'auto')[BurstCoalesceStrategy]: 'adaptive-era' | 'merge-closest' (default 'adaptive-era')[BurstSessionGapHours]: number (default 6)[BurstMaxClusters]: number (default 5)[BurstEraZoomThreshold]: number (default 10)[AllowBurstZoom]: boolean (default true)import {
// Timeline
RecordChangesComponent,
RecordChangesModule,
RestoreVersionEvent,
FieldChangeInfo,
DateGroup,
FilterPill,
// Restore Preview
RestorePreviewPanelComponent,
RestorePreviewMode,
RestoreFieldRow,
RestoreCommitEvent,
BeforeRestoreCommitEvent,
} from '@memberjunction/ng-record-changes';
| Package | Description |
|---|---|
@memberjunction/core |
Core framework (BaseEntity, Metadata, RunView) |
@memberjunction/core-entities |
MJRecordChangeEntity definition |
@memberjunction/ng-shared-generic |
Shared generic components (mj-loading) |
@memberjunction/ng-ui-components |
Provides mj-slide-panel |
@memberjunction/ng-versions |
Version label create wizard |
@memberjunction/ng-notifications |
Toast notifications |
diff |
Word/character text diff for the timeline |
@angular/common ^21.x@angular/core ^21.x@angular/forms ^21.xcd packages/Angular/Generic/record-changes
npm run build
Business Source License 1.1 — see LICENSE for details.