Angular widgets for cloning a MemberJunction record together with the records it owns: an embeddable clone wizard, a slide-in wrapper around it, and the smaller pieces they are built from (plan tree, values form, review, progress, result, lineage chip).
Layering note. This package is UI only. The graph walk, policies, field mapping and writes live on the server in
@memberjunction/record-cloning(engine) and@memberjunction/record-cloning-base(client-safe contracts). The widgets reach the engine through four MJ remote operations (RecordClone.Describe,.Plan,.Execute,.GetLineage) whose typed clients CodeGen emits into@memberjunction/core-entities. There is no custom GraphQL resolver. Seeplans/record-cloning/README.mdfor the full design.
The package is a widgets-layer (mjUILayer: "widgets") library. It never imports the Router or anything under packages/Angular/Explorer; every navigation is an output event the host maps onto its own routing.
graph TD
TB["ng-base-forms
mj-form-toolbar"] -->|ShowCloneButton| SI["mj-record-clone-slide-in"]
SI --> SP["mj-slide-panel
(ng-ui-components)"]
SI --> P["mj-record-clone-panel
(embeddable wizard)"]
P --> SC["mj-clone-scope-controls"]
P --> PT["mj-clone-plan-tree"]
P --> V["mj-clone-values"]
P --> R["mj-clone-review"]
P --> PR["mj-clone-progress"]
P --> RS["mj-clone-result"]
P --> S["RecordCloneService"]
S -->|remote operations| SRV["MJAPI → record-cloning engine"]
LC["mj-clone-lineage-chip"] --> S
| Component | Selector | Use it when |
|---|---|---|
RecordClonePanelComponent |
mj-record-clone-panel |
You want the wizard inline: a dialog, a dashboard card, a side pane, a test harness |
RecordCloneSlideInComponent |
mj-record-clone-slide-in |
You want the standard right-hand slide-in with one element |
CloneLineageChipComponent |
mj-clone-lineage-chip |
You want a "Cloned from X" / "3 clones" chip with a lineage popover |
RecordCloneService |
— | You want to call the remote operations directly |
The building blocks (mj-clone-scope-controls, mj-clone-plan-tree, mj-clone-values, mj-clone-review, mj-clone-progress, mj-clone-result) are exported for hosts that compose their own flow.
npm install @memberjunction/ng-record-clone
Every component is standalone. Import the ones you use, or RecordCloneModule for all of them.
ng-base-forms hosts the slide-in in the record toolbar. The Clone button appears when all of these hold:
ShowCloneButton: true. It is on in DEFAULT_TOOLBAR_CONFIG, EXPLORER_TOOLBAR_CONFIG and CUSTOM_LAYOUT_TOOLBAR_CONFIG; a form turns it off with Toolbar: { ShowCloneButton: false } in its form config.Configuration.Clone.Enabled is true on its MJ: Entities record.RecordClone.Describe says the current user may clone it (cached per provider and entity for the session).The toolbar re-emits the clone's navigation requests through its own Navigate output as a RecordNavigationEvent, so existing form hosts need no changes. It also emits BeforeClone (cancellable) and CloneCompleted.
<button (click)="ShowClone = true">Clone</button>
<mj-record-clone-slide-in
[(Visible)]="ShowClone"
[Record]="record"
[Provider]="provider"
(CloneCompleted)="OnCloned($event)"
(NavigateToRecord)="OpenRecord($event)">
</mj-record-clone-slide-in>
import { CompositeKey } from '@memberjunction/core';
import type { CloneCompletedEvent, CloneNavigationEvent } from '@memberjunction/ng-record-clone';
OnCloned(e: CloneCompletedEvent): void {
console.log(`Created ${e.CreatedRecordsCount} records, log ${e.CloneLogID}`);
}
OpenRecord(e: CloneNavigationEvent): void {
const entity = this.provider.EntityByName(e.EntityName);
this.myNavigation.OpenRecord(e.EntityName, CompositeKey.FromURLSegment(entity, e.RecordKey));
}
The slide-in creates the panel on first open and restarts it on every later open, so each open plans against the record's current state. It refuses to close (X, Escape, backdrop, or Close()) while a clone is executing.
<mj-record-clone-panel
[EntityName]="'MJ: AI Prompts'"
[RecordKey]="promptId"
(StateChange)="State = $event"
(CloneCompleted)="OnCloned($event)"
(NavigateToRecord)="OpenRecord($event)"
(CloseRequested)="HidePanel()">
</mj-record-clone-panel>
Give the panel a source with Record, or with EntityName plus RecordKey. RecordKey accepts a RecordCloneKey or a record-id string in compact URL-segment form: a bare value for a single-column key, Field1|Value1||Field2|Value2 for a composite one. It is parsed with CompositeKey.FromURLSegment against the entity's metadata.
With AutoStart on (the default) the panel describes and plans as soon as it has a source. Set [AutoStart]="false" and call Start() to control timing.
<mj-clone-lineage-chip
[EntityName]="record.EntityInfo.Name"
[RecordKey]="record.PrimaryKey.ToURLSegment()"
(NavigateToRecord)="OpenRecord($event)">
</mj-clone-lineage-chip>
Renders nothing when the record has no lineage.
mj-record-clone-panel — RecordClonePanelComponentThe wizard: Scope (preset, caps, toggles, planned record tree) → Values (clone name, prompted fields, retarget pickers, reason) → Review (counts, warnings, per-node field diff) → Executing → Done or Failed. An entity that can't be cloned shows the server's reason instead.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
Record |
BaseEntity | null |
null |
Record to clone. Supplies the key and entity name when those inputs are not set. |
EntityName |
string |
— | Source entity, e.g. 'MJ: Users'. |
RecordKey |
RecordCloneKey | string |
— | Source key; see Embedded panel. |
AutoStart |
boolean |
true |
Start as soon as a source is set. |
ShowStepTabs |
boolean |
true |
Show the Scope / Values / Review tabs. |
Provider |
IMetadataProvider | null |
null |
Inherited from BaseAngularComponent; every server call uses it. |
Outputs
| Output | Payload | Fires when |
|---|---|---|
StateChange |
RecordClonePanelState |
The wizard state changes (loading, scope, values, review, executing, done, failed, not_cloneable, …). |
StepChange |
RecordCloneStep |
The active step changes. |
PlanChanged |
RecordClonePlanDetails |
A plan (re)computation succeeds. |
CloneCompleted |
CloneCompletedEvent |
The clone commits. The panel also raises the standard BaseEntity save / create event for the root so open grids refresh. |
CloneFailed |
CloneFailedEvent |
Describe, plan or execute fails. ResultCode carries PLAN_CHANGED, FORBIDDEN, etc. when the server sent one. |
NavigateToRecord |
CloneNavigationEvent |
The user opens the new clone or its clone log. Followed by CloseRequested. |
CloseRequested |
void |
The user pressed Close or Cancel. The panel never hides itself. |
Methods and properties
| Member | Description |
|---|---|
Start(): Promise<void> |
Describe, plan, land on Scope. Calling it again starts over. |
Reset(): Promise<void> |
Clear entered values and start over ("Clone another"). |
Replan(): Promise<void> |
Re-plan with the current ScopeOptions. |
GoToStep(step) |
Move to a step. Review is refused while prompted values are invalid, and entering it re-plans with the entered values. |
ExecuteClone(): Promise<void> |
Execute the reviewed plan with its hash. The server refuses with PLAN_CHANGED if the graph moved since review. |
CurrentState, CurrentStep |
Current wizard position. |
IsBusy |
True while loading or executing. |
ActivePlan, DescribeDetails, ExecutionResult |
Latest server answers. |
ScopeOptions |
Options sent with every plan and execute. |
TargetRecordKey |
Record-id string of the root clone after success. |
Who can change the scope. The scope step always shows what the clone will copy, taken from the entity's Configuration.Clone, with anything that differs highlighted. Anyone may narrow a plan by switching a Deep branch to Skip in the tree, when the entity's UserEditable is scope or all. A holder of Clone Records: Override Scope (the Developer role by default) also gets a Developer overrides section to raise depth or the record cap, include soft links, subtypes or the hierarchy subtree, and switch Skip branches back to Deep. Entity Actions run on the cloned rows by default (AI Actions don't), and the entity's Hooks configuration can turn them off. The panel doesn't offer a per-clone override; a caller holding Clone Records: Fire Hooks can set EntityActions on the RecordClone.Plan / Execute request. RecordClone.Describe returns CanOverrideScope and CanFireHooks, and the server applies the same rules to every request, ignoring anything a caller isn't allowed with a warning on the plan.
Where the values step gets its fields. Nothing on the values step is specific to any one entity. The prompted fields come from the entity's Configuration.Clone.Fields.PromptFor (for MJ: Users: Email, FirstName, LastName). They start empty unless the engine proposed a value, because a prompted field such as a unique Email must not keep the source row's value. The suggested clone name comes from the naming strategy (Clone.Naming; by default the name field plus any unique string field). When a prompted field is also a Clone.Naming.Fields entry, typing in it keeps the clone name in step until the user edits the name directly. Retarget pickers come from Clone.UI.RetargetFields.
mj-record-clone-slide-in — RecordCloneSlideInComponentInputs: Visible (two-way with VisibleChange), Record, EntityName, RecordKey, Title (default Clone <entity>), WidthPx (default 720), ShowStepTabs, Provider.
Outputs: VisibleChange, Closed, plus every panel output re-emitted unchanged: StateChange, StepChange, PlanChanged, CloneCompleted, CloneFailed, NavigateToRecord.
Methods: Open(), Close() (ignored while executing), Panel (the embedded RecordClonePanelComponent once opened), CanCloseGuard (passed to mj-slide-panel).
| Component | Inputs | Outputs |
|---|---|---|
mj-clone-scope-controls |
Presets, SelectedPreset, MaxDepth, MaxRecords, Subtypes, Hierarchy, SoftLinks, EntityActions, ConfiguredScope, EntityName, CanOverrideScope |
ScopeChanged: RecordClonePlanOptions, ResetToDefaults |
mj-clone-plan-tree |
Plan, SelectedNodeKey, AllowNarrowing, AllowWidening |
NodeSelected: RecordClonePlanNode, EdgePolicyChanged: CloneEdgePolicyChange |
mj-clone-values |
EntityName, RootName, NamingStrategyReason, PromptedFields, PromptedValues, RetargetFields, Reason |
RootNameChange, PromptedValuesChange, RetargetFieldsChange, ReasonChange, ValidityChange |
mj-clone-review |
Plan, RootName, Reason, IsExecuting |
Confirm, Cancel, NodeClicked: string |
mj-clone-progress |
Progress: CloneProgressUpdate |
— |
mj-clone-result |
Result, EntityName, TargetKey, RootRecordName |
OpenClone, CloneAnother, Close, OpenCloneLog, NavigateToRecord |
mj-clone-lineage-chip |
EntityName, RecordKey, AutoLoad, LineageData |
NavigateToRecord |
The plan tree also exposes ExpandAll() and CollapseAll(). The lineage chip exposes LoadLineage(), TogglePopover() and ClosePopover().
RecordCloneServiceprovidedIn: 'root'. Every method takes an optional IMetadataProvider; when it's omitted, the service uses Metadata.Provider.
| Method | Remote operation | Notes |
|---|---|---|
DescribeRecord(input, provider?, forceRefresh?) |
RecordClone.Describe |
Entity-level answers (no Key) are cached per provider and entity. Concurrent callers share one request, and failures are not cached. |
PlanClone(input, provider?) |
RecordClone.Plan |
Dry run; writes nothing. |
ExecuteClone(input, provider?) |
RecordClone.Execute |
Pass ExpectedPlanHash from the reviewed plan. |
GetLineage(input, provider?) |
RecordClone.GetLineage |
Ancestors and clones of a record. |
ClearDescribeCache(entityName?, provider?) |
— | Drop one entity's cached answer, or all of them. |
| Type | Description |
|---|---|
RecordClonePanelState, RecordCloneStep |
Wizard state and step unions. |
CloneScopeValues |
The six scope values the scope step shows and edits. |
CloneEdgePolicyChange |
{ RelationshipID, Policy } — a branch the user switched, sent as an edge override. |
CloneCompletedEvent |
EntityName, TargetKey, CloneLogID, CreatedRecordsCount, Result. |
CloneFailedEvent |
EntityName, Message, ResultCode?. |
CloneNavigationEvent |
{ Kind: 'record', EntityName, RecordKey }. RecordKey is a record-id string for CompositeKey.FromURLSegment. |
ClonePromptFieldItem, CloneRetargetFieldItem, CloneProgressUpdate, CloneTreeNodeViewModel |
View models for the building blocks. |
EntityToRecordCloneKey(record), CompositeKeyToRecordCloneKey(key), RecordCloneKeyToString(key) |
Key helpers that keep every primary key column. |
CloneNavigationEvent, CloseRequested and friends. The base-forms toolbar turns navigation into its own Navigate output; Explorer's form host routes it from there.Describe, Plan and Execute re-check every permission, cap and lock on the server.Execute carries the reviewed plan's hash, so a graph that changed after review is refused with PLAN_CHANGED rather than cloned blind.--mj-bg-surface, --mj-border-default, --mj-text-*, --mj-status-*, --mj-space-*, --mj-radius-*), so light, dark and custom themes apply without overrides.BaseAngularComponent or takes a provider, and the service never assumes the global provider when one is given.| Package | Why |
|---|---|
@memberjunction/core |
BaseEntity, CompositeKey, IMetadataProvider |
@memberjunction/core-entities |
Generated remote-operation clients and their input/output types |
@memberjunction/global |
Event bus for the post-clone save event |
@memberjunction/ng-base-types |
BaseAngularComponent (provider plumbing) |
@memberjunction/ng-ui-components |
mj-slide-panel, mj-tab-nav, mj-switch, mjButton |
Peer dependencies: @angular/core, @angular/common, @angular/forms.
cd packages/Angular/Generic/record-clone
npm run build # ngc
npm test # vitest (DOM + unit)
npm run test:types # type-check the specs
BUSL-1.1