feat: redesign Load Records to Multiple Objects experience - #1933
Conversation
There was a problem hiding this comment.
Pull request overview
Reworks the “Load Records to Multiple Objects” feature into a 2-step Review → Load flow, with richer validation UX (cell/row-level), visible grouping semantics, cancellable sequential loading, retries, and durable state via jotai atoms. It also adds a new “Load template (Excel)” download format so query results can be exported directly into the multi-object load template shape.
Changes:
- Added “Load template (Excel)” as a new download format and a shared utility to generate a multi-object load template from in-browser records.
- Rewrote multi-object load parsing/validation/graph derivation and the UI into a 2-step experience with group visibility, actionable errors, sequential load progress, cancel, and retry.
- Added substantial unit test coverage for parsing/graph building/review mapping and updated docs + analytics keys.
Reviewed changes
Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| libs/ui/src/lib/file-download-modal/RecordDownloadModal.tsx | Adds “Load template (Excel)” format and client-only generation path. |
| libs/ui/src/lib/file-download-modal/download-modal-utils.ts | Introduces a distinct radio value for the load-template XLSX option. |
| libs/ui/src/lib/data-table/DataTableRenderers.tsx | Re-exports withCellValidation for cell-level validation UI. |
| libs/shared/ui-utils/src/lib/shared-ui-utils.ts | Adds prepareLoadMultiObjectTemplate to build template-shaped XLSX sheets from records. |
| libs/shared/ui-utils/src/lib/tests/prepare-load-multi-object-template.spec.ts | Unit tests for template generation + XLSX round-trip. |
| libs/shared/ui-core/src/record/ViewEditCloneRecord.tsx | Updates download callback typing to include the load-template format. |
| libs/shared/constants/src/lib/shared-constants.ts | Adds analytics keys for the redesigned multi-object load flow. |
| libs/features/query/src/QueryResults/QueryResultsDownloadButton.tsx | Enables query-results → load-template export via loadTemplateOption. |
| libs/features/load-records-multi-object/src/useProcessLoadFile.ts | Moves parsing + graph derivation into atoms; preserves datasets even with errors. |
| libs/features/load-records-multi-object/src/useLoadFile.ts | Implements runs (initial/retry), progress tracking, cancellation, and result merging. |
| libs/features/load-records-multi-object/src/useDownloadResults.ts | Removes old results download hook (moved into load/). |
| libs/features/load-records-multi-object/src/review/review-utils.ts | Builds grid-ready preview rows and maps validation errors to cells/rows/banners. |
| libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectSheetPreview.tsx | Renders per-worksheet preview grid with inline cell/row validation UI. |
| libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectReview.tsx | Step 1 review screen: summary, options popover, error summary, sheet/group tabs. |
| libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectGroupsOverview.tsx | Groups tab: exposes computed group membership and sizes pre-load. |
| libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectErrorSummary.tsx | Clickable grouped error summary for fast navigation to offending sheets. |
| libs/features/load-records-multi-object/src/review/tests/review-utils.spec.ts | Unit tests for error-to-grid mapping and group summary helpers. |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObjectResultsTableRow.tsx | Removes old results table row component (replaced by new results UI). |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObjectResultsTable.tsx | Removes old results table (replaced by tabbed per-record results tables). |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObjectResults.tsx | Removes old results page (replaced by Step 2 load/results screen). |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObjectRecordModal.tsx | Removes old record modal (replaced by per-record results tables). |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObjectErrors.tsx | Removes old error display (replaced by review grids + summary). |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObjectEmptyState.tsx | Adds teaching empty state describing template layout and grouping semantics. |
| libs/features/load-records-multi-object/src/LoadRecordsMultiObject.tsx | Converts main page to 2-step flow, atom state, shortcuts, and absolute template link. |
| libs/features/load-records-multi-object/src/load/useDownloadResults.ts | New download hook using normalized result-row builder. |
| libs/features/load-records-multi-object/src/load/LoadRecordsMultiObjectResultsTables.tsx | New tabbed per-record results tables (per worksheet + combined). |
| libs/features/load-records-multi-object/src/load/LoadRecordsMultiObjectLoad.tsx | Step 2: load execution UI (progress, cancel, retry, downloads, nav guard). |
| libs/features/load-records-multi-object/src/load/load-results-utils.ts | Normalizes runs/requests into per-record rows + download rows + summary counts. |
| libs/features/load-records-multi-object/src/load-records-multi-object.state.ts | Adds jotai atoms for durable multi-step state, runs, progress, and derived readiness. |
| libs/features/load-records-multi-object/src/load-records-multi-object-utils.ts | Refactors parsing + graph building into pure buildDataGraph, improves error reporting and limits handling. |
| libs/features/load-records-multi-object/src/load-records-multi-object-types.ts | Expands error/result types (severity, rowIndexes, groups, runs, progress). |
| libs/features/load-records-multi-object/src/tests/load-records-multi-object-utils.spec.ts | Comprehensive unit tests for parsing/graph building/retry packing/regressions. |
| apps/docs/docs/load/load-with-related.mdx | Updates documentation to match new workflow, grouping semantics, limits, troubleshooting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
7a46be6 to
6b79a5b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (7)
libs/features/load-records-multi-object/src/load-records-multi-object-utils.ts:183
- Reference Ids are currently derived with
row[referenceColumnHeader] || uniqueId(...), which treats valid falsy values (notably numeric0) as missing and also preserves number-vs-string mismatches that break dependency lookups later. Coerce to a trimmed string and only fall back when the value is actually blank/nullish.
dataset.dataById = dataset.data.reduce((output: Record<string, any>, row, i) => {
const referenceId = row[dataset.referenceColumnHeader || ''] || uniqueId('reference_');
if (output[referenceId]) {
libs/features/load-records-multi-object/src/load-records-multi-object-utils.ts:386
- Missing Reference Id detection uses a falsy check (
!row[referenceColumnHeader]), which incorrectly flags valid values like numeric0, and doesn't trim whitespace-only strings. Since XLSX parsing usesrawNumbers: true, Reference Ids can legitimately arrive as numbers; these should be string-normalized before validation.
const missingRefIdRowIndexes = dataset.data.reduce((output: number[], row, i) => {
if (!row[referenceColumnHeader]) {
output.push(i);
}
return output;
libs/features/load-records-multi-object/src/load-records-multi-object-utils.ts:555
buildDataGraphstoresreferenceIdas the raw cell value. WithrawNumbers: true, Reference Ids can be numbers (e.g.1), but dependency values are always stringified (String(rawValue).trim()), so graph nodes may be1while dependencies are'1'. This guarantees false “Reference Id does not exist” errors and breaks grouping. Normalize Reference Ids to trimmed strings before using them as graph node IDs and object keys.
const tempData: LoadMultiObjectRecord = {
sobject: dataset.sobject,
operation: dataset.operation,
externalId: dataset.externalId,
externalIdValue,
recordIdForUpdate,
referenceId: record[dataset.referenceColumnHeader],
record: transformedRecord,
worksheet: dataset.worksheet,
libs/features/load-records-multi-object/src/load-records-multi-object-utils.ts:406
- Invalid Reference Id detection relies on truthiness (
!!row[referenceColumnHeader]) and tests the raw cell value, which can be a number (XLSX usesrawNumbers: true). This can skip validation for numeric IDs like0and can produce untrimmed values in the message. Normalize to a trimmed string before applyingVALID_REF_ID_RGX, and stringify values in the error message.
const invalidRefIdRowIndexes = dataset.data.reduce((output: number[], row, i) => {
if (!!row[referenceColumnHeader] && !VALID_REF_ID_RGX.test(row[referenceColumnHeader])) {
output.push(i);
}
return output;
libs/features/load-records-multi-object/src/review/review-utils.ts:73
- Reference Id extraction uses
record[referenceColumnKey] || null, which treats valid values like numeric0as missing and also preserves number-vs-string mismatches when looking upgroupsByRefId(whose keys are strings). Coerce to a trimmed string and only treat the value as missing when it's actually blank/nullish.
const rows: SheetPreviewRow[] = dataset.data.map((record, i) => {
const referenceId: string | null = record[referenceColumnKey] || null;
const groupInfo = referenceId ? groupsByRefId[referenceId] : null;
const groupNumber = groupInfo ? groupNumbersByGraphId[groupInfo.graphId] : null;
libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectGroupsOverview.tsx:47
- Group membership lookup uses
record[dataset.referenceColumnHeader] || null, which treats valid falsy values (notably numeric0) as missing and prevents those rows from ever showing up in the Groups tab. Since XLSX parsing can yield numbers, normalize to a trimmed string before the lookup.
const referenceId: string | null = record[dataset.referenceColumnHeader] || null;
const groupInfo = referenceId ? groupsByRefId[referenceId] : null;
libs/features/load-records-multi-object/src/load-records-multi-object-utils.ts:180
- Header normalization strips
{curly braces}when building row objects, butdataset.referenceColumnHeaderis later taken verbatim from cell A5. If A5 is written as{Reference Id}, row keys will beReference Idwhile subsequent lookups use{Reference Id}, making every Reference Id appear missing/invalid and breaking preview + graph building. Normalizedataset.referenceColumnHeaderthe same way as other headers (trim + strip braces).
This issue also appears in the following locations of the same file:
- line 181
- line 382
- line 402
- line 547
);
dataset.headers = headers.map((header) => header.replace(SURROUNDING_BRACKETS_RGX, '').trim());
dataset.referenceHeaders = new Set(
headers.filter((header) => IS_REFERENCE_RGX.test(header)).map((header) => header.replace(SURROUNDING_BRACKETS_RGX, '')),
);
|
Went through Copilot's suppressed (low-confidence) comments — they were all pointing at the same two real bugs, both now fixed:
Fixed both with a shared |
6b79a5b to
fbc78ed
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (2)
libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectErrorSummary.tsx:38
- This
<button>doesn't specifytype. If this component is ever rendered inside a<form>, the default type="submit" can trigger unintended form submissions when clicking a worksheet name. Settype="button"to make the behavior explicit.
<button
className="slds-button"
css={css`
font-weight: bold;
`}
onClick={() => onSelectWorksheet(worksheet)}
>
{worksheet}
</button>
libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectSheetPreview.tsx:105
SearchInput'sidis derived fromdataset.worksheet, which can contain spaces (e.g. "Create Accounts") and other characters that produce an invalid HTML id and can break label/ARIA associations. Sanitize the worksheet name before using it in an element id.
<SearchInput id={`sheet-filter-${dataset.worksheet}`} placeholder="Filter records" onChange={setQuickFilterText} />
|
Went through Copilot's suppressed (low-confidence) comments — two were worth applying:
The seven Reference Id normalization findings from the earlier review were already fixed by the |
Full rewrite of the multi-object load page into a 2-step flow (Upload &
Review, Load Data) with the data visible and errors actionable at every
stage.
Upload & Review
- Teaching empty state explaining the worksheet layout, Reference Ids,
{curly brace} linking, and record groups
- Per-worksheet data preview grids with validation errors shown on the
exact cells and rows, error-count badges per tab, and a clickable
error summary (preview renders even when the file has errors)
- Groups tab and per-row Group column that make the record groups
visible, with a summary line (records, groups, largest group vs the
500 per-group limit, API request count)
- Load options moved into a popover; changing them re-derives the graph
Load & Results
- Live progress (request x of y, records processed), cancel between
requests, and a navigation guard while loading
- Per-record result tables per worksheet with failures filter, retry of
failed groups, and results downloads that include worksheet and row
- State moved to jotai atoms so parsed files and results survive
navigation
Data layer fixes
- getDataGraph replaced with pure buildDataGraph that reports all
problems instead of throwing on the first one, including readable
circular-reference errors and every oversized group at once
- Fixed stateful global regex misdetecting {reference} brackets, input
mutation that dropped cell-level references when load options changed
after upload, case-sensitive reference header matching, upsert
external Id error reported at B1 instead of B3, groups of exactly 500
being rejected, object-count seeding in request batching, and the
failures download forcing success on 204 responses
- Rewrote all validation messages to explain the fix, including the
500-records-per-group limit (per group of related records, not per
file)
- First unit tests for this feature (48 specs covering parsing,
validation, graph building, batching, and retry)
Related changes
- Query results can be downloaded as a load template (record Id becomes
the Reference Id) for org-to-org migration
- Docs page rewritten with grouping semantics, limits, and a
troubleshooting table
- New load_multi_obj_* analytics events; template link now works on
desktop/extension/canvas
fbc78ed to
12a686f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (3)
libs/features/load-records-multi-object/src/LoadRecordsMultiObject.tsx:171
- changeStep uses the captured currentStepIdx value, which can go stale under rapid successive calls (keyboard shortcuts + button clicks) and cause step navigation to skip or not advance as expected. Use a functional updater so the next step is always derived from the latest atom value.
function changeStep(changeBy: number) {
setCurrentStepIdx(Math.min(Math.max(currentStepIdx + changeBy, 0), STEP_COUNT - 1));
}
libs/features/load-records-multi-object/src/load/LoadRecordsMultiObjectLoad.tsx:27
- The step label in this comment looks off-by-one: the flow is 2 steps (review = step 0, load/results = step 1), so this should not say "Step 2".
/** Step 2: confirm the target org, run the load with live progress, then review/retry per-record results */
libs/shared/ui-utils/src/lib/shared-ui-utils.ts:464
- This per-field
records.some(...)scan can become expensive on large query downloads (it potentially iterates the full record set once per field). Consider a single pass over records/fields that tracks which fields are object-valued, so each record is only visited once and fields that are already known to be object-valued are skipped.
Full rewrite of the multi-object load page into a 2-step flow (Upload & Review, Load Data) with the data visible and errors actionable at every stage.
Upload & Review
Load & Results
Data layer fixes
Related changes