Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/docs/docs/load/update-records.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ After selecting an object, you need to configure the fields that you would like
- **Only if not blank**
- **Customer criteria** allows you to provide a custom `soql` WHERE clause to target the exact records you want. If you are unfamiliar with doing this by hand, you can use the Query Builder to build this for you.

### Limiting how many records are updated

If you are working with a very large number of records, you can limit how many are updated at one time.

**Maximum records to update** limits how many records will be included. Leave this blank to update every record that meets your criteria.

The number of impacted records shown when you validate takes the limit into account, so you will always see exactly how many records will be updated.

:::tip

To work through a large data volume in chunks, use criteria that no longer matches a record once it has been updated, for example **Only if blank** or a custom criteria, and run the update again to process the next set of records.

:::

After you have configured your field, you will need to validate the results. Validating the results will make sure your configuration is accurate and let you know how many records will be modified with this change.

If you have specified a custom criteria that is not valid, Salesforce will return an error message. You will need to resolve this before continuing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ const CONFIG_DISPLAY_FIELDS: Array<{
},
{ key: 'batchSize', label: 'Batch Size' },
{ key: 'serialMode', label: 'Serial Mode' },
{ key: 'recordLimit', label: 'Record Limit' },
{ key: 'insertNulls', label: 'Insert Null Values' },
// Only meaningful when a date field was part of the load
{ key: 'dateFormat', label: 'Date Format', show: (_, config) => config.hasDateFieldMapped !== false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const MassUpdateRecordsDeployment = () => {
deployResults={row.deployResults}
sobject={row.sobject}
configuration={row.configuration}
limit={row.limit}
validationResults={row.validationResults}
batchSize={batchSize ?? 1000}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ListItem, SalesforceOrgUi } from '@jetstream/types';
import { ListItem, Maybe, SalesforceOrgUi } from '@jetstream/types';
import { Grid, GridCol, Tooltip } from '@jetstream/ui';
import { MassUpdateRecordObjectHeading, MassUpdateRecordsObjectRow, MetadataRow } from '@jetstream/ui-core';
import { FunctionComponent, useCallback } from 'react';
Expand All @@ -10,6 +10,7 @@ export interface MassUpdateRecordsObjectProps {
commonFields: ListItem[];
onFieldSelected: ReturnType<typeof useMassUpdateFieldItems>['onFieldSelected'];
handleOptionChange: ReturnType<typeof useMassUpdateFieldItems>['handleOptionChange'];
handleRecordLimitChange: (sobject: string, limit: Maybe<number>) => void;
onLoadChildFields: (sobject: string, item: ListItem) => Promise<ListItem[]>;
validateRowRecords: (sobject: string) => void;
handleAddField: (sobject: string) => void;
Expand All @@ -24,6 +25,7 @@ export const MassUpdateRecordsObject: FunctionComponent<MassUpdateRecordsObjectP
onFieldSelected,
onLoadChildFields,
handleOptionChange,
handleRecordLimitChange,
validateRowRecords,
handleAddField,
handleRemoveField,
Expand All @@ -45,6 +47,10 @@ export const MassUpdateRecordsObject: FunctionComponent<MassUpdateRecordsObjectP
valueFields={row.valueFields}
fieldConfigurations={row.configuration}
validationResults={row.validationResults}
recordLimit={{
limit: row.limit,
onChange: (limit) => handleRecordLimitChange(row.sobject, limit),
}}
onFieldChange={(index, selectedField) => onFieldSelected(index, row.sobject, selectedField)}
onOptionsChange={(index, sobject, options) => handleOptionChange(index, sobject, options)}
onLoadChildFields={handleLoadChildFields}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ListItem, SalesforceOrgUi } from '@jetstream/types';
import { ListItem, Maybe, SalesforceOrgUi } from '@jetstream/types';
import { AutoFullHeightContainer, EmptyState, OpenRoadIllustration } from '@jetstream/ui';
import { MetadataRow, TransformationOptions } from '@jetstream/ui-core';
import { Fragment, FunctionComponent } from 'react';
Expand All @@ -16,6 +16,7 @@ export interface MassUpdateRecordsObjectsProps {
applyCommonOption: ReturnType<typeof useMassUpdateFieldItems>['applyCommonOption'];
applyCommonCriteria: ReturnType<typeof useMassUpdateFieldItems>['applyCommonCriteria'];
handleOptionChange: (configIndex: number, sobject: string, transformationOptions: TransformationOptions) => void;
handleRecordLimitChange: (sobject: string, limit: Maybe<number>) => void;
handleAddField: (sobject: string) => void;
handleRemoveField: (sobject: string, configIndex: number) => void;
validateRowRecords: (sobject: string) => void;
Expand All @@ -31,6 +32,7 @@ export const MassUpdateRecordsObjects: FunctionComponent<MassUpdateRecordsObject
applyCommonOption,
applyCommonCriteria,
handleOptionChange,
handleRecordLimitChange,
handleAddField,
handleRemoveField,
validateRowRecords,
Expand All @@ -57,6 +59,7 @@ export const MassUpdateRecordsObjects: FunctionComponent<MassUpdateRecordsObject
onFieldSelected={onFieldSelected}
onLoadChildFields={onLoadChildFields}
handleOptionChange={handleOptionChange}
handleRecordLimitChange={handleRecordLimitChange}
validateRowRecords={validateRowRecords}
handleAddField={handleAddField}
handleRemoveField={handleRemoveField}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const MassUpdateRecordsSelection: FunctionComponent<MassUpdateRecordsSele
applyCommonOption,
applyCommonCriteria,
handleOptionChange,
handleRecordLimitChange,
handleAddField,
handleRemoveField,
validateAllRowRecords,
Expand Down Expand Up @@ -183,6 +184,7 @@ export const MassUpdateRecordsSelection: FunctionComponent<MassUpdateRecordsSele
applyCommonOption={applyCommonOption}
applyCommonCriteria={applyCommonCriteria}
handleOptionChange={handleOptionChange}
handleRecordLimitChange={handleRecordLimitChange}
handleAddField={handleAddField}
handleRemoveField={handleRemoveField}
validateRowRecords={validateRowRecords}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Action =
type: 'TRANSFORMATION_OPTION_CHANGED';
payload: { sobject: string; transformationOptions: TransformationOptions; configIndex: number };
}
| { type: 'RECORD_LIMIT_CHANGED'; payload: { sobject: string; limit: Maybe<number> } }
| { type: 'ADD_FIELD'; payload: { sobject: string } }
| { type: 'REMOVE_FIELD'; payload: { sobject: string; configIndex: number } }
| { type: 'METADATA_LOADED'; payload: { sobject: string; metadata: DescribeSObjectResult } }
Expand Down Expand Up @@ -190,6 +191,17 @@ function reducer(state: State, action: Action): State {
rowsMap.set(sobject, row);
return { ...state, rowsMap, allRowsValid: Array.from(rowsMap.values()).every((row) => row.isValid) };
}
case 'RECORD_LIMIT_CHANGED': {
const { sobject, limit } = action.payload;
const rowsMap = new Map(state.rowsMap);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const prevRow = state.rowsMap.get(sobject)!;
// The prior validation counted a different set of records, so it has to be re-run
const row: MetadataRow = { ...prevRow, limit, validationResults: null };
row.isValid = isValidRow(row);
rowsMap.set(sobject, row);
return { ...state, rowsMap, allRowsValid: Array.from(rowsMap.values()).every((row) => row.isValid) };
}
case 'ADD_FIELD': {
const { sobject } = action.payload;
const rowsMap = new Map(state.rowsMap);
Expand Down Expand Up @@ -494,6 +506,10 @@ export function useMassUpdateFieldItems(org: SalesforceOrgUi, selectedSObjects:
dispatch({ type: 'TRANSFORMATION_OPTION_CHANGED', payload: { sobject, transformationOptions, configIndex } });
}

function handleRecordLimitChange(sobject: string, limit: Maybe<number>) {
dispatch({ type: 'RECORD_LIMIT_CHANGED', payload: { sobject, limit } });
}

function handleAddField(sobject: string) {
dispatch({ type: 'ADD_FIELD', payload: { sobject } });
}
Expand All @@ -513,6 +529,7 @@ export function useMassUpdateFieldItems(org: SalesforceOrgUi, selectedSObjects:
applyCommonOption,
applyCommonCriteria,
handleOptionChange,
handleRecordLimitChange,
handleAddField,
handleRemoveField,
validateAllRowRecords,
Expand Down
1 change: 1 addition & 0 deletions libs/shared/ui-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export * from './mass-update-records/MassUpdateRecordsDeploymentRow';
export * from './mass-update-records/MassUpdateRecordsObjectRow';
export * from './mass-update-records/MassUpdateRecordsObjectRowCriteria';
export * from './mass-update-records/MassUpdateRecordsObjectRowField';
export * from './mass-update-records/MassUpdateRecordsObjectRowLimit';
export * from './mass-update-records/MassUpdateRecordsObjectRowValue';
export * from './mass-update-records/MassUpdateRecordsObjectRowValueStaticInput';
export * from './mass-update-records/useDeployRecords';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import MassUpdateRecordTransformationText from './MassUpdateRecordTransformation
import { MetadataRow } from './mass-update-records.types';
import {
buildMassUpdateCombinedResults,
getEffectiveRecordLimit,
getMassUpdateBatchSourceRecords,
getMassUpdateQueriedFieldsHeader,
getMassUpdateResultsHeader,
Expand All @@ -41,13 +42,14 @@ export type MassUpdateRecordsDeploymentRowProps = {
batchSize: number;
omitTransformationText?: boolean;
onModalOpenChange?: (isOpen: boolean) => void;
} & Pick<MetadataRow, 'sobject' | 'deployResults' | 'configuration'>;
} & Pick<MetadataRow, 'sobject' | 'deployResults' | 'configuration' | 'limit'>;

export const MassUpdateRecordsDeploymentRow = ({
selectedOrg,
sobject,
deployResults,
configuration,
limit,
hasExternalWhereClause,
validationResults,
batchSize,
Expand All @@ -62,6 +64,7 @@ export const MassUpdateRecordsDeploymentRow = ({
const skipFrontDoorAuth = useAtomValue(selectSkipFrontdoorAuth);

const { done, processingErrors, status, fatalErrorMessage, jobInfo, processingEndTime, processingStartTime } = deployResults;
const effectiveLimit = getEffectiveRecordLimit(limit);

useEffect(() => {
onModalOpenChange && onModalOpenChange(downloadModalData.open || resultsModalData.open);
Expand Down Expand Up @@ -220,6 +223,12 @@ export const MassUpdateRecordsDeploymentRow = ({
were found matching this criteria.
</div>
)}
{!processingStartTime && !!effectiveLimit && (
<div className="slds-m-left_medium">
At most <span className="text-bold">{formatNumber(effectiveLimit)}</span> {pluralizeFromNumber('record', effectiveLimit)} will
be updated.
</div>
)}
<div className="slds-scrollable_x">
<div className="text-bold slds-m-left_medium">{status}</div>
{jobInfo?.id && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { logger } from '@jetstream/shared/client-logger';
import { ANALYTICS_KEYS } from '@jetstream/shared/constants';
import { queryAll } from '@jetstream/shared/data';
import { formatNumber } from '@jetstream/shared/ui-utils';
import { getErrorMessage, pluralizeFromNumber } from '@jetstream/shared/utils';
import { Field, ListItem, Maybe, SalesforceOrgUi } from '@jetstream/types';
Expand All @@ -14,13 +13,10 @@ import { fromJetstreamEvents } from '../jetstream-events';
import MassUpdateRecordTransformationText from './MassUpdateRecordTransformationText';
import MassUpdateRecordsObjectRowCriteria from './MassUpdateRecordsObjectRowCriteria';
import MassUpdateRecordsObjectRowField from './MassUpdateRecordsObjectRowField';
import MassUpdateRecordsObjectRowLimit from './MassUpdateRecordsObjectRowLimit';
import MassUpdateRecordsObjectRowValue from './MassUpdateRecordsObjectRowValue';
import { MetadataRow, MetadataRowConfiguration, TransformationOptions, ValidationResults } from './mass-update-records.types';
import {
composeSoqlQueryCustomWhereClause,
composeSoqlQueryOptionalCustomWhereClause,
getFieldsToQuery,
} from './mass-update-records.utils';
import { getFieldsToQuery, queryRecordsForRow } from './mass-update-records.utils';

export interface MassUpdateRecordsObjectRowProps {
org: SalesforceOrgUi;
Expand All @@ -32,6 +28,11 @@ export interface MassUpdateRecordsObjectRowProps {
fieldConfigurations: MetadataRowConfiguration[];
validationResults?: Maybe<ValidationResults>;
hasExternalWhereClause?: boolean;
/**
* When provided, inputs to limit / skip records are shown. Omitted where the records to update are
* already scoped by something else, such as the query results entry point.
*/
recordLimit?: { limit: Maybe<number>; onChange: (limit: Maybe<number>) => void };
disabled?: boolean;
onFieldChange: (index: number, selectedField: string, fieldMetadata: Field) => void;
onOptionsChange: (index: number, sobject: string, options: TransformationOptions) => void;
Expand All @@ -53,6 +54,7 @@ export const MassUpdateRecordsObjectRow: FunctionComponent<MassUpdateRecordsObje
fieldConfigurations,
validationResults,
hasExternalWhereClause,
recordLimit,
disabled,
onFieldChange,
onOptionsChange,
Expand All @@ -78,33 +80,18 @@ export const MassUpdateRecordsObjectRow: FunctionComponent<MassUpdateRecordsObje
try {
setDownloadRecordsLoading(true);
const fieldsToQuery = getFieldsToQuery(fieldConfigurations);
const row = { sobject, configuration: fieldConfigurations } as MetadataRow;
const standardQuery = composeSoqlQueryOptionalCustomWhereClause(row, fieldsToQuery);
const customQuery = composeSoqlQueryCustomWhereClause(row, fieldsToQuery);

const recordsById: Record<string, any> = {};

if (standardQuery) {
const result = await queryAll(org, standardQuery);
result.queryResults.records.forEach((record) => {
recordsById[record.Id] = record;
});
}

if (customQuery) {
const result = await queryAll(org, customQuery);
result.queryResults.records.forEach((record) => {
recordsById[record.Id] = record;
});
}
const row = { sobject, configuration: fieldConfigurations, limit: recordLimit?.limit } as MetadataRow;
// Shares the deploy path's fetch so the previewed records are exactly the records that will be updated.
// Custom criteria membership is skipped because the download shows the raw queried records.
const { records } = await queryRecordsForRow(row, fieldsToQuery, org, { resolveCustomCriteria: false });

setDownloadModalData({
open: true,
data: Object.values(recordsById),
data: records,
header: fieldsToQuery,
fileNameParts: ['mass-update', sobject.toLowerCase(), 'validation-records'],
});
trackEvent(ANALYTICS_KEYS.mass_update_DownloadRecords, { type: 'validation', numRows: Object.keys(recordsById).length });
trackEvent(ANALYTICS_KEYS.mass_update_DownloadRecords, { type: 'validation', numRows: records.length });
} catch (ex) {
logger.error('[DOWNLOAD VALIDATION RECORDS]', ex);
fireToast({ type: 'error', message: `Failed to download records. ${getErrorMessage(ex)}` });
Expand Down Expand Up @@ -181,6 +168,16 @@ export const MassUpdateRecordsObjectRow: FunctionComponent<MassUpdateRecordsObje
</button>
</div>
</GridCol>
{recordLimit && (
<GridCol size={12} className="slds-m-top_small">
<MassUpdateRecordsObjectRowLimit
sobject={sobject}
limit={recordLimit.limit}
disabled={disabled}
onChange={recordLimit.onChange}
/>
</GridCol>
)}
</Grid>
{validationResults && (
<footer className="slds-card__footer">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { css } from '@emotion/react';
import { useIntegerInput } from '@jetstream/shared/ui-utils';
import { Maybe } from '@jetstream/types';
import { Grid, Input } from '@jetstream/ui';
import { FunctionComponent } from 'react';
import { getRecordLimitError } from './mass-update-records.utils';

export interface MassUpdateRecordsObjectRowLimitProps {
sobject: string;
limit: Maybe<number>;
disabled?: boolean;
onChange: (limit: Maybe<number>) => void;
}

/**
* Optional per-object `LIMIT`, which allows updating a data volume that is too large to process in
* one pass by working through it a chunk at a time.
*/
export const MassUpdateRecordsObjectRowLimit: FunctionComponent<MassUpdateRecordsObjectRowLimitProps> = ({
sobject,
limit,
disabled,
onChange,
}) => {
const limitInput = useIntegerInput(limit, onChange);
const limitError = getRecordLimitError(limit);

return (
<Grid verticalAlign="end">
<Grid verticalAlign="end" className="text-bold slds-m-horizontal_medium slds-m-bottom_x-small">
LIMIT
</Grid>
<Grid
verticalAlign="end"
css={css`
min-width: 240px;
max-width: 500px;
`}
>
<div className="slds-m-horizontal_x-small slds-grow">
<Input
id={`${sobject}-limit`}
label="Maximum records to update"
hasError={!!limitError}
errorMessage={limitError}
errorMessageId={`${sobject}-limit-error`}
labelHelp="Limit how many records are updated at one time. Leave blank to update every record that meets your criteria."
>
<input
id={`${sobject}-limit`}
className="slds-input"
placeholder="All matching records"
value={limitInput.inputValue}
aria-describedby={limitError ? `${sobject}-limit-error` : undefined}
disabled={disabled}
onChange={limitInput.handleChange}
onBlur={limitInput.handleBlur}
/>
</Input>
</div>
</Grid>
</Grid>
);
};

export default MassUpdateRecordsObjectRowLimit;
Loading
Loading