diff --git a/libs/features/sobject-export/src/SObjectExport.tsx b/libs/features/sobject-export/src/SObjectExport.tsx index 40778cc9..f97ca6f0 100644 --- a/libs/features/sobject-export/src/SObjectExport.tsx +++ b/libs/features/sobject-export/src/SObjectExport.tsx @@ -28,18 +28,25 @@ import { applicationCookieState, fromJetstreamEvents, selectedOrgState } from '@ import localforage from 'localforage'; import { Fragment, FunctionComponent, useEffect, useRef, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { ExportHeaderOption, ExportOptions, ExportWorksheetLayout, SavedExportOptions } from './sobject-export-types'; -import { getAttributes, getSobjectMetadata, prepareExport } from './sobject-export-utils'; +import { + ExportHeaderOption, + ExportOptions, + ExportWorksheetLayout, + SavedExportOptions, + SobjectExportFieldName, +} from './sobject-export-types'; +import { getAttributes, getChildRelationshipNames, getSobjectMetadata, prepareExport } from './sobject-export-utils'; const HEIGHT_BUFFER = 170; -const FIELD_ATTRIBUTES: ListItem[] = getAttributes().map(({ label, name, description }) => ({ +const FIELD_ATTRIBUTES: ListItem[] = getAttributes().map(({ label, name, description, tertiaryLabel }) => ({ id: name, label: `${label} (${name})`, value: name, secondaryLabel: description, + tertiaryLabel, })); -const DEFAULT_SELECTION = [ +const DEFAULT_SELECTION: SobjectExportFieldName[] = [ 'calculatedFormula', 'createable', 'custom', @@ -81,7 +88,7 @@ export const SObjectExport: FunctionComponent = () => { const [sobjects, setSobjects] = useState>(); const [selectedSObjects, setSelectedSObjects] = useState([]); - const [selectedAttributes, setSelectedAttributes] = useState([]); + const [selectedAttributes, setSelectedAttributes] = useState([]); const [exportDataModalOpen, setExportDataModalOpen] = useState(false); const [exportDataModalData, setExportDataModalData] = useState>({}); @@ -108,7 +115,7 @@ export const SObjectExport: FunctionComponent = () => { } } if (results?.fields) { - setSelectedAttributes(results.fields); + setSelectedAttributes(results.fields as SobjectExportFieldName[]); } else { setSelectedAttributes([...DEFAULT_SELECTION]); } @@ -149,7 +156,10 @@ export const SObjectExport: FunctionComponent = () => { setLoading(true); setErrorMessage(null); const metadataResults = await getSobjectMetadata(selectedOrg, selectedSObjects); - const output = prepareExport(metadataResults, selectedAttributes, options); + const sobjectsWithChildRelationships = selectedAttributes.includes('childRelationshipName') + ? await getChildRelationshipNames(selectedOrg, metadataResults) + : {}; + const output = prepareExport(metadataResults, sobjectsWithChildRelationships, selectedAttributes, options); if (options.saveAsDefaultSelection) { try { @@ -248,12 +258,12 @@ export const SObjectExport: FunctionComponent = () => { descriptorSingular: 'field attribute', descriptorPlural: 'field attributes', }} - items={FIELD_ATTRIBUTES} + items={FIELD_ATTRIBUTES as ListItem[]} selectedItems={selectedAttributes} allowRefresh lastRefreshed="Reset to default" onRefresh={resetAttributesToDefault} - onSelected={setSelectedAttributes} + onSelected={(items) => setSelectedAttributes(items as SobjectExportFieldName[])} />
diff --git a/libs/features/sobject-export/src/sobject-export-types.ts b/libs/features/sobject-export/src/sobject-export-types.ts index db281725..c20c30ea 100644 --- a/libs/features/sobject-export/src/sobject-export-types.ts +++ b/libs/features/sobject-export/src/sobject-export-types.ts @@ -1,7 +1,8 @@ -import { DescribeSObjectResult, Field, Maybe } from '@jetstream/types'; +import { ChildRelationship, DescribeSObjectResult, Field, Maybe } from '@jetstream/types'; export type SobjectExportFieldName = | keyof Field + | 'childRelationshipName' | 'dataTranslationEnabled' | 'autoNumber' | 'aiPredictionField' @@ -13,7 +14,9 @@ export interface SobjectExportField { name: SobjectExportFieldName; label: string; description?: string; + tertiaryLabel?: string; getterFn?: (value: any) => string; + childRelationshipGetterFn?: (field: Field, sobjectsWithChildRelationships: Record>) => string; } export interface SavedExportOptions { diff --git a/libs/features/sobject-export/src/sobject-export-utils.ts b/libs/features/sobject-export/src/sobject-export-utils.ts index 3932f3fb..9e504138 100644 --- a/libs/features/sobject-export/src/sobject-export-utils.ts +++ b/libs/features/sobject-export/src/sobject-export-utils.ts @@ -1,6 +1,8 @@ +import { logger } from '@jetstream/shared/client-logger'; import { describeSObject } from '@jetstream/shared/data'; -import { splitArrayToMaxSize } from '@jetstream/shared/utils'; -import { ApiResponse, DescribeSObjectResult, SalesforceOrgUi } from '@jetstream/types'; +import { logErrorToRollbar } from '@jetstream/shared/ui-utils'; +import { getErrorMessageAndStackObj, splitArrayToMaxSize } from '@jetstream/shared/utils'; +import { ApiResponse, ChildRelationship, DescribeSObjectResult, Field, SalesforceOrgUi } from '@jetstream/types'; import isFunction from 'lodash/isFunction'; import isString from 'lodash/isString'; import { ExportOptions, SobjectExportField, SobjectFetchResult } from './sobject-export-types'; @@ -39,8 +41,46 @@ export async function getSobjectMetadata(org: SalesforceOrgUi, selectedSobjects: }); } +export async function getChildRelationshipNames( + selectedOrg: SalesforceOrgUi, + metadataResults: SobjectFetchResult[] +): Promise>> { + try { + // Get Parent SObject names from all relationship fields and remove duplicates + const relatedSobjects = Array.from( + new Set( + metadataResults.flatMap( + (item) => + item.metadata?.fields + .filter((field) => field.type === 'reference' && field.referenceTo?.length === 1) + .flatMap((field) => field.referenceTo || []) || [] + ) + ) + ); + // Fetch all parent sobject metadata (hopefully from cache for many of them) and reduce into map for easy lookup + const sobjectsWithChildRelationships = await getSobjectMetadata(selectedOrg, relatedSobjects).then((results) => + results.reduce((sobjectsWithChildRelationships: Record>, { metadata, sobject }) => { + sobjectsWithChildRelationships[sobject] = (metadata?.childRelationships || []).reduce( + (acc: Record, childRelationship) => { + acc[childRelationship.field] = childRelationship; + return acc; + }, + {} + ); + return sobjectsWithChildRelationships; + }, {}) + ); + return sobjectsWithChildRelationships; + } catch (ex) { + logger.warn('Error getting child relationship names for sobject export', ex); + logErrorToRollbar('Error getting child relationship names for sobject export', getErrorMessageAndStackObj(ex)); + return {}; + } +} + export function prepareExport( sobjectMetadata: SobjectFetchResult[], + sobjectsWithChildRelationships: Record>, selectedAttributes: string[], options: ExportOptions ): Record { @@ -62,16 +102,18 @@ export function prepareExport( rowsBySobject[sobject] = metadata?.fields .filter((field) => (options.includesStandardFields ? true : field.custom)) - .flatMap((field: any) => { + .flatMap((field: Field) => { const obj = { 'Object Name': sobject } as any; - selectedAttributeFields.forEach(({ name, label, getterFn }) => { + selectedAttributeFields.forEach(({ name, label, getterFn, childRelationshipGetterFn: relationshipGetterFn }) => { const _label = options.headerOption === 'label' ? label : name; - // TODO: transform as required + const value = field[name as keyof Field]; if (isFunction(getterFn)) { - obj[_label] = getterFn(field[name]); + obj[_label] = getterFn(value); + } else if (isFunction(relationshipGetterFn)) { + obj[_label] = relationshipGetterFn(field, sobjectsWithChildRelationships); } else { - obj[_label] = field[name]; + obj[_label] = value; } }); return obj; @@ -192,6 +234,27 @@ export function getAttributes(): SobjectExportField[] { label: 'Calculated Formula', description: 'Formula definition. Only populated if field type is Formula.', }, + { + name: 'childRelationshipName', + label: 'Child Relationship Name', + description: 'Child relationship name(s) for lookup field.', + childRelationshipGetterFn: (field: Field, sobjectsWithChildRelationships: Record>) => { + const relatedSObjects = field.referenceTo || []; + if (relatedSObjects.length === 0) { + return ''; + } + return relatedSObjects + .map((relatedSObject) => { + const childRelationship = sobjectsWithChildRelationships[relatedSObject]?.[field.name]?.relationshipName; + if (childRelationship) { + return childRelationship; + } + return null; + }) + .filter(Boolean) + .join(', '); + }, + }, { name: 'controllerName', label: 'Controller Name', diff --git a/yarn.lock b/yarn.lock index 3be35393..0f1bd9dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11678,20 +11678,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001349, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001587: - version "1.0.30001636" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz" - integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== - -caniuse-lite@^1.0.30001579: - version "1.0.30001639" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz#972b3a6adeacdd8f46af5fc7f771e9639f6c1521" - integrity sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg== - -caniuse-lite@^1.0.30001646: - version "1.0.30001657" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz#29fd504bffca719d1c6b63a1f6f840be1973a660" - integrity sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001349, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001646: + version "1.0.30001687" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz" + integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== caseless@~0.12.0: version "0.12.0"