Skip to content

feat: redesign Load Records to Multiple Objects experience - #1933

Merged
paustint merged 1 commit into
mainfrom
feat/load-multi/ux-upgrade
Aug 13, 2026
Merged

feat: redesign Load Records to Multiple Objects experience#1933
paustint merged 1 commit into
mainfrom
feat/load-multi/ux-upgrade

Conversation

@paustint

Copy link
Copy Markdown
Contributor

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

Copilot AI lite review requested due to automatic review settings August 11, 2026 02:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread libs/features/load-records-multi-object/src/useLoadFile.ts Outdated
Copilot AI review requested due to automatic review settings August 11, 2026 11:49
@paustint
paustint force-pushed the feat/load-multi/ux-upgrade branch from 7a46be6 to 6b79a5b Compare August 11, 2026 11:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 numeric 0) 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 numeric 0, and doesn't trim whitespace-only strings. Since XLSX parsing uses rawNumbers: 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

  • buildDataGraph stores referenceId as the raw cell value. With rawNumbers: true, Reference Ids can be numbers (e.g. 1), but dependency values are always stringified (String(rawValue).trim()), so graph nodes may be 1 while 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 uses rawNumbers: true). This can skip validation for numeric IDs like 0 and can produce untrimmed values in the message. Normalize to a trimmed string before applying VALID_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 numeric 0 as missing and also preserves number-vs-string mismatches when looking up groupsByRefId (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 numeric 0) 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, but dataset.referenceColumnHeader is later taken verbatim from cell A5. If A5 is written as {Reference Id}, row keys will be Reference Id while subsequent lookups use {Reference Id}, making every Reference Id appear missing/invalid and breaking preview + graph building. Normalize dataset.referenceColumnHeader the 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, '')),
      );

@paustint

Copy link
Copy Markdown
Contributor Author

Went through Copilot's suppressed (low-confidence) comments — they were all pointing at the same two real bugs, both now fixed:

  • Numeric Reference Ids were broken by this PR (load-records-multi-object-utils.ts:555). dependency-graph v1 keys nodes in a Map, so 1 and '1' are different nodes. The new code stringifies the dependency side (String(rawValue).trim()) but left the node id as the raw cell value, so any workbook using numeric Reference Ids now failed with "The value "1" refers to a Reference Id that does not exist on any worksheet." On main both sides were raw, so this was a regression.
  • The Reference Id header from cell A5 was never normalized (load-records-multi-object-utils.ts:180). Row keys are trimmed and have {braces} stripped, but referenceColumnHeader was read verbatim. A trailing space in A5 made every row report "missing a Reference Id" plus a bogus "the field does not exist on the object" error, and blanked the Reference Id column in the preview grid.

Fixed both with a shared normalizeHeader + getReferenceId helper, applied at every Reference Id read site (parse, validation, graph building, review grid, Groups tab). That also covers the falsy-0 and untrimmed-value cases the other suppressed comments called out. Added regression tests for numeric ids, whitespace/braced A5 headers, and whitespace in the values.

@paustint
paustint force-pushed the feat/load-multi/ux-upgrade branch from 6b79a5b to fbc78ed Compare August 11, 2026 12:50
Copilot AI review requested due to automatic review settings August 11, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 specify type. If this component is ever rendered inside a <form>, the default type="submit" can trigger unintended form submissions when clicking a worksheet name. Set type="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's id is derived from dataset.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} />

@paustint

Copy link
Copy Markdown
Contributor Author

Went through Copilot's suppressed (low-confidence) comments — two were worth applying:

  • libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectErrorSummary.tsx:30 — added type="button" so the worksheet jump-link can never act as a submit button.
  • libs/features/load-records-multi-object/src/review/LoadRecordsMultiObjectSheetPreview.tsx:105 — worksheet names can contain spaces/punctuation, so the filter input id is now sanitized to a valid element id.

The seven Reference Id normalization findings from the earlier review were already fixed by the getReferenceId helper in the current commit.

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
@paustint
paustint force-pushed the feat/load-multi/ux-upgrade branch from fbc78ed to 12a686f Compare August 11, 2026 14:19
Copilot AI review requested due to automatic review settings August 11, 2026 14:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@paustint
paustint merged commit a8b72fa into main Aug 13, 2026
16 checks passed
@paustint
paustint deleted the feat/load-multi/ux-upgrade branch August 13, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants