From 0c236c02cde90f89b925855c5f9d3f5880fea5df Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Fri, 17 May 2024 14:59:46 +0100 Subject: [PATCH 1/2] Add duplicate rules to Automation Control Allow duplicate rules to be included for duplicate rules. These require metadata api to be deployed, so adjusted automation deployment process to account for this. resolves #880 --- .../AutomationControlEditor.tsx | 7 +- .../AutomationControlEditorReviewModal.tsx | 5 +- .../automation-control-data-utils.tsx | 204 +++++++++++++++++- .../automation-control-soql-utils.tsx | 50 +++++ .../automation-control-types.ts | 35 ++- .../automation-control.state.ts | 3 +- .../useAutomationControlData.ts | 63 ++++-- 7 files changed, 334 insertions(+), 33 deletions(-) diff --git a/apps/jetstream/src/app/components/automation-control/AutomationControlEditor.tsx b/apps/jetstream/src/app/components/automation-control/AutomationControlEditor.tsx index a25cf4d9b..49307c602 100644 --- a/apps/jetstream/src/app/components/automation-control/AutomationControlEditor.tsx +++ b/apps/jetstream/src/app/components/automation-control/AutomationControlEditor.tsx @@ -19,18 +19,18 @@ import { ToolbarItemGroup, Tooltip, } from '@jetstream/ui'; -import { applicationCookieState, fromJetstreamEvents, selectSkipFrontdoorAuth, selectedOrgState } from '@jetstream/ui-core'; +import { applicationCookieState, fromJetstreamEvents, selectSkipFrontdoorAuth, selectedOrgState, useAmplitude } from '@jetstream/ui-core'; import classNames from 'classnames'; import { FunctionComponent, useState } from 'react'; import { Link } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { useAmplitude } from '@jetstream/ui-core'; import { RequireMetadataApiBanner } from '../core/RequireMetadataApiBanner'; import AutomationControlEditorReviewModal from './AutomationControlEditorReviewModal'; import AutomationControlEditorTable from './AutomationControlEditorTable'; import AutomationControlLastRefreshedPopover from './AutomationControlLastRefreshedPopover'; import { getAutomationDeployType, + isDuplicateRecord, isTableRow, isTableRowChild, isTableRowItem, @@ -145,6 +145,9 @@ export const AutomationControlEditor: FunctionComponent(['ApexTrigger', 'DuplicateRule']); + const COLUMNS: Column[] = [ { name: 'Object', @@ -68,7 +71,7 @@ function getDeploymentItemMap(rows: TableRowItem[]): DeploymentItemMap { : row.record.Id, activeVersionNumber: row.activeVersionNumber, value: row.isActive, - requireMetadataApi: row.type === 'ApexTrigger', + requireMetadataApi: REQUIRE_METADATA_API.has(row.type), metadataRetrieve: null, metadataDeploy: null, retrieveError: null, diff --git a/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx b/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx index 23505ee88..d18bde7ff 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx +++ b/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx @@ -4,16 +4,26 @@ import { genericRequest, getCacheItemNonHttp, query, + retrieveMetadataFromListMetadata, saveCacheItemNonHttp, } from '@jetstream/shared/data'; -import { getToolingRecords, logErrorToRollbar, pollMetadataResultsUntilDone } from '@jetstream/shared/ui-utils'; +import { + getOrgType, + getToolingRecords, + logErrorToRollbar, + pollMetadataResultsUntilDone, + pollRetrieveMetadataResultsUntilDone, +} from '@jetstream/shared/ui-utils'; import { groupByFlat, splitArrayToMaxSize } from '@jetstream/shared/utils'; -import { CompositeRequest, CompositeRequestBody, CompositeResponse, SalesforceOrgUi } from '@jetstream/types'; +import { CompositeRequest, CompositeRequestBody, CompositeResponse, ListMetadataResult, SalesforceOrgUi } from '@jetstream/types'; import { formatRelative } from 'date-fns/formatRelative'; +import JSZip from 'jszip'; +import isString from 'lodash/isString'; import { Observable, Subject, from, of } from 'rxjs'; import { catchError, mergeMap } from 'rxjs/operators'; import { getApexTriggersQuery, + getDuplicateRuleQuery, getFlowsQuery, getProcessBuildersQuery, getValidationRulesQuery, @@ -22,7 +32,9 @@ import { import { AutomationControlDeploymentItem, AutomationMetadataType, + DeploymentItem, DeploymentItemMap, + DuplicateRuleRecord, FetchErrorPayload, FetchSuccessPayload, FlowMetadata, @@ -67,6 +79,10 @@ export function isToolingApexRecord(type: AutomationMetadataType, record: any): return type === 'ApexTrigger'; } +export function isDuplicateRecord(type: AutomationMetadataType, record: any): record is DuplicateRuleRecord { + return type === 'DuplicateRule'; +} + export function isValidationRecord(type: AutomationMetadataType, record: any): record is ToolingValidationRuleRecord { return type === 'ValidationRule'; } @@ -82,7 +98,9 @@ export function isFlowRecord(type: AutomationMetadataType, record: any): record export function getAutomationTypeLabel(type: AutomationMetadataType) { switch (type) { case 'ApexTrigger': - return 'Apex Class'; + return 'Apex Trigger'; + case 'DuplicateRule': + return 'Duplicate Rule'; case 'ValidationRule': return 'Validation Rule'; case 'WorkflowRule': @@ -139,6 +157,15 @@ export function fetchAutomationData( ) ); } + if (selectedTypes.has('DuplicateRule')) { + requests.push( + from( + getDuplicateRules(selectedOrg, selectedSObjects).then((records) => ({ type: 'DuplicateRule', records } as FetchSuccessPayload)) + ).pipe( + catchError((error) => of({ type: 'DuplicateRule', error: error?.message || 'An unknown error has occurred.' } as FetchErrorPayload)) + ) + ); + } if (selectedTypes.has('ValidationRule')) { requests.push( from( @@ -203,6 +230,18 @@ export async function getApexTriggersMetadata(selectedOrg: SalesforceOrgUi, sobj return apexClassRecords; } +/** Query ApexTriggers */ +export async function getDuplicateRules(selectedOrg: SalesforceOrgUi, sobjects: string[]): Promise { + const apexClassRecords = ( + await Promise.all( + splitArrayToMaxSize(sobjects, 300).map((currSobjects) => + query(selectedOrg, getDuplicateRuleQuery(currSobjects), false) + ) + ) + ).flatMap(({ queryResults }) => queryResults.records); + return apexClassRecords; +} + /** * Query initial records, then query each record and add FullName and Metadata fields * @@ -418,15 +457,18 @@ export async function preparePayloadsForDeployment( itemsByKey: DeploymentItemMap, payloadEvent: Subject<{ key: string; deploymentItem: AutomationControlDeploymentItem }[]> ) { + const duplicateRules = Object.keys(itemsByKey) + .filter((key) => !itemsByKey[key].deploy.metadataRetrieve && itemsByKey[key].metadata.type === 'DuplicateRule') + .map((key) => itemsByKey[key]); + const hasDuplicateRule = duplicateRules.length > 0; const baseFields = ['Id', 'FullName', 'Metadata']; // Prepare composite requests const metadataFetchRequests: CompositeRequestBody[][] = splitArrayToMaxSize( Object.keys(itemsByKey) - .filter((key) => !itemsByKey[key].deploy.metadataRetrieve) + .filter((key) => !itemsByKey[key].deploy.metadataRetrieve && itemsByKey[key].metadata.type !== 'DuplicateRule') .map((key): CompositeRequestBody => { const item = itemsByKey[key].deploy; const fields: string[] = item.type === 'ApexTrigger' ? baseFields.concat(['Body', 'ApiVersion']) : baseFields; - return { method: 'GET', url: `/services/data/${apiVersion}/tooling/sobjects/${getAutomationDeployType(item.type)}/${item.id}?fields=${fields.join(',')}`, @@ -436,7 +478,13 @@ export async function preparePayloadsForDeployment( 25 ); - // fetch metadata required for deployment + // Initiate metadata API request, then pol for results after all other metadata is fetched + let fileBasedMetadataRequestId: string | undefined; + if (hasDuplicateRule) { + fileBasedMetadataRequestId = await initiateDuplicateRulesMetadataRequest(selectedOrg, duplicateRules); + } + + // fetch metadata required for deployment using tooling API for (const compositeRequest of metadataFetchRequests) { const requestBody: CompositeRequest = { allOrNone: false, @@ -474,6 +522,7 @@ export async function preparePayloadsForDeployment( deploymentItem.metadataDeploy.Metadata.active = deploymentItem.value; break; } + case 'DuplicateRule': // no tooling support, these are handled with metadata api default: break; } @@ -483,6 +532,88 @@ export async function preparePayloadsForDeployment( }); payloadEvent.next(items); } + + // Finish waiting for metadata API requests for duplicate rules + if (fileBasedMetadataRequestId) { + const results = await pollRetrieveMetadataResultsUntilDone(selectedOrg, fileBasedMetadataRequestId); + if (results.success && isString(results.zipFile)) { + const salesforcePackage = await JSZip.loadAsync(results.zipFile, { base64: true }); + const items = await prepareMetadataForDuplicateRules(itemsByKey, salesforcePackage, duplicateRules); + payloadEvent.next(items); + } else { + const items = duplicateRules.map(({ metadata }): { key: string; deploymentItem: AutomationControlDeploymentItem } => ({ + key: metadata.key, + deploymentItem: { + ...itemsByKey[metadata.key].deploy, + retrieveError: [{ message: results.errorMessage || '', errorCode: 'UNKNOWN' }], + }, + })); + payloadEvent.next(items); + } + } +} + +/** + * Initiate metadata API request for duplicate rules + * + * @param selectedOrg + * @param duplicateRules + * @returns + */ +async function initiateDuplicateRulesMetadataRequest(selectedOrg: SalesforceOrgUi, duplicateRules: DeploymentItem[]) { + const listMetadataItems = duplicateRules + .filter((item) => !item.deploy.metadataRetrieve) + .map(({ deploy: item, metadata }): ListMetadataResult => { + const record = metadata.record as DuplicateRuleRecord; + const fullName = `${record.SobjectType}.${record.DeveloperName}`; + return { + createdById: null, + createdByName: null, + createdDate: null, + fileName: `duplicateRules/${fullName}.duplicateRule`, + fullName, + id: item.id, + lastModifiedById: null, + lastModifiedByName: null, + lastModifiedDate: null, + manageableState: record.NamespacePrefix ? 'installed' : 'unmanaged', + namespacePrefix: null, + type: 'DuplicateRule', + }; + }); + return (await retrieveMetadataFromListMetadata(selectedOrg, { DuplicateRule: listMetadataItems })).id; +} + +async function prepareMetadataForDuplicateRules( + itemsByKey: DeploymentItemMap, + salesforcePackage: JSZip, + duplicateRules: DeploymentItem[] +): Promise<{ key: string; deploymentItem: AutomationControlDeploymentItem }[]> { + const output = [] as { key: string; deploymentItem: AutomationControlDeploymentItem }[]; + for (const duplicateRule of duplicateRules) { + const record = duplicateRule.metadata.record as DuplicateRuleRecord; + const deploymentItem = { ...itemsByKey[duplicateRule.metadata.key].deploy }; + const fullName = `${record.SobjectType}.${record.DeveloperName}`; + const fileName = `duplicateRules/${fullName}.duplicateRule`; + if (!salesforcePackage.files[fileName]) { + deploymentItem.retrieveError = [{ message: 'There was an error getting metadata from Salesforce', errorCode: 'MISSING_FILE' }]; + output.push({ key: duplicateRule.metadata.key, deploymentItem }); + continue; + } + const fileContent = await salesforcePackage.file(fileName)?.async('string'); + const metadata: MetadataCompositeResponseSuccess = { + FullName: fullName, + Metadata: fileContent, + }; + deploymentItem.metadataRetrieve = metadata; + deploymentItem.metadataDeployRollback = { ...metadata }; + deploymentItem.metadataDeploy = { ...metadata }; + const replaceSource = deploymentItem.value ? `false` : `true`; + const replaceTarget = deploymentItem.value ? `true` : `false`; + deploymentItem.metadataDeploy.Metadata = (deploymentItem.metadataDeploy.Metadata as string).replace(replaceSource, replaceTarget); + output.push({ key: duplicateRule.metadata.key, deploymentItem }); + } + return output; } export function deployMetadata( @@ -543,11 +674,41 @@ export function deployMetadata( } // perform deployments that are not supported using tooling api - const metadataDeployResults = await deployMetadataFileBased(selectedOrg, itemsByKey, Number(apiVersion.replace(/[^0-9\.]/g, ''))); + const metadataDeployResults = await deployMetadataFileBased(selectedOrg, itemsByKey, Number(apiVersion.replace(/[^0-9.]/g, ''))); if (metadataDeployResults) { - const deployResults = await pollMetadataResultsUntilDone(selectedOrg, metadataDeployResults.deployResultsId); - payloadEvent.next(metadataDeployResults.metadataItems.map((key) => ({ key, deploymentItem: { ...itemsByKey[key].deploy } }))); + const deployResults = await pollMetadataResultsUntilDone(selectedOrg, metadataDeployResults.deployResultsId, { + includeDetails: true, + }); + payloadEvent.next( + metadataDeployResults.metadataItems.map((key) => { + const output = { key, deploymentItem: { ...itemsByKey[key].deploy } }; + const failureItem = deployResults.details?.componentFailures.find( + (item) => item.fullName === itemsByKey[key].deploy.metadataDeploy?.FullName + ); + + if (failureItem) { + output.deploymentItem.deployError = [ + { + errorCode: failureItem.problemType, + message: failureItem.problem, + }, + ]; + return output; + } + + // everything failed, regardless of success/failure + if (!deployResults.success) { + output.deploymentItem.deployError = [ + { + errorCode: 'UNKNOWN_ERROR', + message: 'Error deploying to Salesforce', + }, + ]; + } + return output; + }) + ); } payloadEvent.complete(); @@ -588,7 +749,7 @@ export async function deployMetadataFileBased( fileBasedMetadataItems.forEach((key) => { const item = itemsByKey[key]; switch (item.deploy.type) { - case 'ApexTrigger': + case 'ApexTrigger': { deployItems['ApexTrigger'] = deployItems['ApexTrigger'] || []; deployItems['ApexTrigger'].push({ fullName: (item.metadata.record as ToolingApexTriggerRecord).Name, @@ -611,6 +772,24 @@ export async function deployMetadataFileBased( ], }); break; + } + case 'DuplicateRule': { + if (!item.deploy.metadataDeploy) { + break; + } + deployItems['DuplicateRule'] = deployItems['DuplicateRule'] || []; + deployItems['DuplicateRule'].push({ + fullName: item.deploy.metadataDeploy.FullName, + dirPath: 'duplicateRules', + files: [ + { + name: `${item.deploy.metadataDeploy.FullName}.duplicateRule`, + content: item.deploy.metadataDeploy.Metadata, + }, + ], + }); + break; + } default: break; } @@ -639,7 +818,10 @@ export async function deployMetadataFileBased( }); // deploy file - const deployResults = await deployMetadataZip(selectedOrg, files, { singlePackage: true, rollbackOnError: true }); + const deployResults = await deployMetadataZip(selectedOrg, files, { + singlePackage: true, + rollbackOnError: getOrgType(selectedOrg) === 'Production', + }); // id is only field not deprecated // https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_asyncresult.htm logger.info('deployResults', deployResults); diff --git a/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx b/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx index 2260fee6d..7ad104f4a 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx +++ b/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx @@ -54,6 +54,56 @@ export function getApexTriggersQuery(sobjects: string[]) { return soql; } +export function getDuplicateRuleQuery(sobjects: string[]) { + const soql = composeQuery({ + fields: [ + getField('Id'), + getField('CreatedBy.Id'), + getField('CreatedBy.Name'), + getField('CreatedBy.Username'), + getField('DeveloperName'), + getField('IsActive'), + getField('LastModifiedBy.Id'), + getField('LastModifiedBy.Name'), + getField('LastModifiedBy.Username'), + getField('MasterLabel'), + getField('NamespacePrefix'), + getField('SobjectSubtype'), + getField('SobjectType'), + getField('FORMAT(CreatedDate)'), + getField('FORMAT(LastModifiedDate)'), + ], + sObject: 'DuplicateRule', + where: { + left: { + field: 'SobjectType', + operator: 'IN', + value: sobjects, + literalType: 'STRING', + }, + operator: 'AND', + right: { + left: { + field: 'ManageableState', + operator: '=', + value: 'unmanaged', + literalType: 'STRING', + }, + }, + }, + orderBy: [ + { + field: 'SobjectType', + }, + { + field: 'MasterLabel', + }, + ], + }); + logger.info('getDuplicateRuleQuery()', { soql }); + return soql; +} + export function getValidationRulesQuery(sobjects: string[]) { const soql = composeQuery({ fields: [ diff --git a/apps/jetstream/src/app/components/automation-control/automation-control-types.ts b/apps/jetstream/src/app/components/automation-control/automation-control-types.ts index 7d94b1afe..e1767eb73 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control-types.ts +++ b/apps/jetstream/src/app/components/automation-control/automation-control-types.ts @@ -5,12 +5,18 @@ export type WorkflowRule = 'WorkflowRule'; export type FlowProcessBuilder = 'FlowProcessBuilder'; export type FlowRecordTriggered = 'FlowRecordTriggered'; export type ApexTrigger = 'ApexTrigger'; +export type DuplicateRule = 'DuplicateRule'; -export type AutomationMetadataType = ValidationRule | WorkflowRule | FlowProcessBuilder | FlowRecordTriggered | ApexTrigger; +export type AutomationMetadataType = DuplicateRule | ValidationRule | WorkflowRule | FlowProcessBuilder | FlowRecordTriggered | ApexTrigger; export interface FetchSuccessPayload { type: keyof StateData; - records: ToolingApexTriggerRecord[] | ToolingValidationRuleRecord[] | ToolingWorkflowRuleRecord[] | FlowViewRecord[]; + records: + | DuplicateRuleRecord[] + | ToolingApexTriggerRecord[] + | ToolingValidationRuleRecord[] + | ToolingWorkflowRuleRecord[] + | FlowViewRecord[]; } export interface FetchErrorPayload { @@ -26,6 +32,13 @@ export interface StateData { records: ToolingApexTriggerRecord[]; tableRow: TableRow; }; + DuplicateRule: { + loading: boolean; + skip: boolean; + error?: string; + records: DuplicateRuleRecord[]; + tableRow: TableRow; + }; ValidationRule: { loading: boolean; skip: boolean; @@ -82,7 +95,7 @@ export interface DeploymentItemRow extends TableRowItem { export type MetadataCompositeResponseSuccessOrError = MetadataCompositeResponseSuccess | MetadataCompositeResponseError[]; export interface MetadataCompositeResponseSuccess { - Id: string; + Id?: string; FullName: string; Body?: string; // ApexTrigger ApiVersion?: number; // ApexTrigger @@ -109,9 +122,10 @@ export interface AutomationControlDeploymentItem { } export interface DeploymentItemByType { + apexTriggers: AutomationControlDeploymentItem[]; + duplicateRules: AutomationControlDeploymentItem[]; validationRules: AutomationControlDeploymentItem[]; workflowRules: AutomationControlDeploymentItem[]; - apexTriggers: AutomationControlDeploymentItem[]; flows: AutomationControlDeploymentItem[]; } @@ -149,6 +163,17 @@ export interface ToolingApexTriggerRecord extends SystemFields { Status: 'Inactive' | 'Active' | 'Deleted'; } +/** This is not tooling */ +export interface DuplicateRuleRecord extends SystemFields { + Id: string; + DeveloperName: string; + IsActive: boolean; + MasterLabel: string; + NamespacePrefix: string; + SobjectSubtype: string; + SobjectType: string; +} + export interface ToolingWorkflowRuleRecord extends SystemFields { Id: string; Name: string; @@ -343,7 +368,7 @@ export interface TableRowItem { parentKey: string; type: AutomationMetadataType; sobject: string; - record: ToolingApexTriggerRecord | ToolingValidationRuleRecord | ToolingWorkflowRuleRecord | FlowViewRecord; + record: DuplicateRuleRecord | ToolingApexTriggerRecord | ToolingValidationRuleRecord | ToolingWorkflowRuleRecord | FlowViewRecord; link: string; /** True for Flow and PB as this is controlled from children */ readOnly: boolean; diff --git a/apps/jetstream/src/app/components/automation-control/automation-control.state.ts b/apps/jetstream/src/app/components/automation-control/automation-control.state.ts index 925c7a6ad..80a549e5c 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control.state.ts +++ b/apps/jetstream/src/app/components/automation-control/automation-control.state.ts @@ -23,6 +23,7 @@ export const automationTypes = atom[]>({ { id: 'ApexTrigger', value: 'ApexTrigger', label: 'Apex Triggers', title: 'Apex Triggers' }, { id: 'ValidationRule', value: 'ValidationRule', label: 'Validation Rules', title: 'Validation Rules' }, { id: 'WorkflowRule', value: 'WorkflowRule', label: 'Workflow Rules', title: 'Workflow Rules' }, + { id: 'DuplicateRule', value: 'DuplicateRule', label: 'Duplicate Rules', title: 'Duplicate Rules' }, { id: 'FlowRecordTriggered', value: 'FlowRecordTriggered', label: 'Record Triggered Flows', title: 'Record Triggered Flows' }, { id: 'FlowProcessBuilder', @@ -36,7 +37,7 @@ export const automationTypes = atom[]>({ export const selectedAutomationTypes = atom({ key: 'automation-control.selectedAutomationTypes', - default: ['ApexTrigger', 'ValidationRule', 'WorkflowRule', 'FlowProcessBuilder', 'FlowRecordTriggered'], + default: ['ApexTrigger', 'DuplicateRule', 'ValidationRule', 'WorkflowRule', 'FlowRecordTriggered'], }); export const hasSelectionsMade = selector({ diff --git a/apps/jetstream/src/app/components/automation-control/useAutomationControlData.ts b/apps/jetstream/src/app/components/automation-control/useAutomationControlData.ts index 587ce86f0..d56fbaa96 100644 --- a/apps/jetstream/src/app/components/automation-control/useAutomationControlData.ts +++ b/apps/jetstream/src/app/components/automation-control/useAutomationControlData.ts @@ -2,8 +2,8 @@ import { logger } from '@jetstream/shared/client-logger'; import { ANALYTICS_KEYS } from '@jetstream/shared/constants'; import { useRollbar } from '@jetstream/shared/ui-utils'; import { SalesforceOrgUi } from '@jetstream/types'; -import { useCallback, useEffect, useReducer, useRef } from 'react'; import { useAmplitude } from '@jetstream/ui-core'; +import { useCallback, useEffect, useReducer, useRef } from 'react'; import { fetchAutomationData, getAdditionalItemsWorkflowRuleText, @@ -16,6 +16,7 @@ import { } from './automation-control-data-utils'; import { AutomationMetadataType, + DuplicateRuleRecord, FetchErrorPayload, FetchSuccessPayload, FlowViewRecord, @@ -69,6 +70,7 @@ function isDirty(row: TableRow | TableRowItem | TableRowItemChild) { type MetadataRecordType = | { type: 'ApexTrigger'; records: ToolingApexTriggerRecord[] } + | { type: 'DuplicateRule'; records: DuplicateRuleRecord[] } | { type: 'ValidationRule'; records: ToolingValidationRuleRecord[] } | { type: 'WorkflowRule'; records: ToolingWorkflowRuleRecord[] } | { type: 'FlowRecordTriggered'; records: FlowViewRecord[] } @@ -85,16 +87,23 @@ function reducer(state: State, action: Action): State { loading: true, data: { ...state.data }, }; - (['ApexTrigger', 'ValidationRule', 'WorkflowRule', 'FlowRecordTriggered', 'FlowProcessBuilder'] as AutomationMetadataType[]).forEach( - (type) => { - output.data[type] = { - loading: selectedTypes.has(type), - skip: !selectedTypes.has(type), - records: [], - tableRow: getRowsForItems({ type, records: [] }, true), - }; - } - ); + ( + [ + 'ApexTrigger', + 'DuplicateRule', + 'ValidationRule', + 'WorkflowRule', + 'FlowRecordTriggered', + 'FlowProcessBuilder', + ] as AutomationMetadataType[] + ).forEach((type) => { + output.data[type] = { + loading: selectedTypes.has(type), + skip: !selectedTypes.has(type), + records: [], + tableRow: getRowsForItems({ type, records: [] }, true), + }; + }); const { keys, rows, rowsByKey } = flattenTableRows(output.data, {}); output.keys = keys; @@ -364,6 +373,28 @@ function getRowsForItems({ type, records }: MetadataRecordType, loading: boolean ); break; } + case 'DuplicateRule': { + output.items = (records as DuplicateRuleRecord[]).map( + (record): TableRowItem => ({ + path: [typeLabel, record.DeveloperName], + key: `${type}_${record.Id}`, + parentKey: type, + type, + record, + link: `/lightning/setup/DuplicateRules/page?address=${encodeURIComponent(`/${record.Id}?setupid=DuplicateRules`)}`, + sobject: record.SobjectType, + readOnly: false, + isExpanded: true, + isActive: record.IsActive, + isActiveInitialState: record.IsActive, + label: record.MasterLabel, + lastModifiedBy: `${record.LastModifiedBy.Name} ${record.LastModifiedDate}`, + description: '', + additionalData: [], + }) + ); + break; + } case 'ValidationRule': { output.items = (records as ToolingValidationRuleRecord[]).map( (record): TableRowItem => ({ @@ -397,7 +428,7 @@ function getRowsForItems({ type, records }: MetadataRecordType, loading: boolean parentKey: type, type, record, - link: `/lightning/setup/WorkflowRules/page?address=%2F${record.Id}&nodeId=WorkflowRules`, + link: `/lightning/setup/WorkflowRules/page?address=${encodeURIComponent(`/${record.Id}?nodeId=DuplicateRules`)}`, sobject: record.TableEnumOrId, readOnly: false, isExpanded: true, @@ -566,6 +597,12 @@ export function useAutomationControlData({ hasError: false, data: { ApexTrigger: { loading: true, skip: false, records: [], tableRow: getRowsForItems({ type: 'ApexTrigger', records: [] }, false) }, + DuplicateRule: { + loading: true, + skip: false, + records: [], + tableRow: getRowsForItems({ type: 'DuplicateRule', records: [] }, false), + }, ValidationRule: { loading: true, skip: false, @@ -599,7 +636,7 @@ export function useAutomationControlData({ return () => { isMounted.current = false; }; - }, []); + }, [selectedAutomationTypes, selectedSObjects.length, trackEvent]); const updateIsActiveFlag = useCallback((row: TableRowOrItemOrChild, value: boolean) => { dispatch({ type: 'UPDATE_IS_ACTIVE_FLAG', payload: { row, value } }); From f9492cac90eb290b7d307d658fdbba679898c556 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 18 May 2024 07:50:18 +0100 Subject: [PATCH 2/2] DuplicateRules - code cleanup --- .../automation-control/automation-control-data-utils.tsx | 8 +++++--- .../automation-control/automation-control-soql-utils.tsx | 6 +++--- .../automation-control/automation-control.state.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx b/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx index d18bde7ff..f30b84295 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx +++ b/apps/jetstream/src/app/components/automation-control/automation-control-data-utils.tsx @@ -230,7 +230,7 @@ export async function getApexTriggersMetadata(selectedOrg: SalesforceOrgUi, sobj return apexClassRecords; } -/** Query ApexTriggers */ +/** Query DuplicateRules */ export async function getDuplicateRules(selectedOrg: SalesforceOrgUi, sobjects: string[]): Promise { const apexClassRecords = ( await Promise.all( @@ -457,11 +457,13 @@ export async function preparePayloadsForDeployment( itemsByKey: DeploymentItemMap, payloadEvent: Subject<{ key: string; deploymentItem: AutomationControlDeploymentItem }[]> ) { + // Duplicate Rules require metadata API const duplicateRules = Object.keys(itemsByKey) .filter((key) => !itemsByKey[key].deploy.metadataRetrieve && itemsByKey[key].metadata.type === 'DuplicateRule') .map((key) => itemsByKey[key]); const hasDuplicateRule = duplicateRules.length > 0; const baseFields = ['Id', 'FullName', 'Metadata']; + // Prepare composite requests const metadataFetchRequests: CompositeRequestBody[][] = splitArrayToMaxSize( Object.keys(itemsByKey) @@ -478,7 +480,7 @@ export async function preparePayloadsForDeployment( 25 ); - // Initiate metadata API request, then pol for results after all other metadata is fetched + // Initiate metadata API request, then poll for results after all other metadata is fetched let fileBasedMetadataRequestId: string | undefined; if (hasDuplicateRule) { fileBasedMetadataRequestId = await initiateDuplicateRulesMetadataRequest(selectedOrg, duplicateRules); @@ -522,7 +524,7 @@ export async function preparePayloadsForDeployment( deploymentItem.metadataDeploy.Metadata.active = deploymentItem.value; break; } - case 'DuplicateRule': // no tooling support, these are handled with metadata api + case 'DuplicateRule': // no tooling API support, handled with metadata api default: break; } diff --git a/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx b/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx index 7ad104f4a..b24cdba12 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx +++ b/apps/jetstream/src/app/components/automation-control/automation-control-soql-utils.tsx @@ -84,10 +84,10 @@ export function getDuplicateRuleQuery(sobjects: string[]) { operator: 'AND', right: { left: { - field: 'ManageableState', + field: 'NamespacePrefix', operator: '=', - value: 'unmanaged', - literalType: 'STRING', + value: 'NULL', + literalType: 'NULL', }, }, }, diff --git a/apps/jetstream/src/app/components/automation-control/automation-control.state.ts b/apps/jetstream/src/app/components/automation-control/automation-control.state.ts index 80a549e5c..cd8711b3b 100644 --- a/apps/jetstream/src/app/components/automation-control/automation-control.state.ts +++ b/apps/jetstream/src/app/components/automation-control/automation-control.state.ts @@ -21,9 +21,9 @@ export const automationTypes = atom[]>({ key: 'automation-control.automationTypes', default: [ { id: 'ApexTrigger', value: 'ApexTrigger', label: 'Apex Triggers', title: 'Apex Triggers' }, + { id: 'DuplicateRule', value: 'DuplicateRule', label: 'Duplicate Rules', title: 'Duplicate Rules' }, { id: 'ValidationRule', value: 'ValidationRule', label: 'Validation Rules', title: 'Validation Rules' }, { id: 'WorkflowRule', value: 'WorkflowRule', label: 'Workflow Rules', title: 'Workflow Rules' }, - { id: 'DuplicateRule', value: 'DuplicateRule', label: 'Duplicate Rules', title: 'Duplicate Rules' }, { id: 'FlowRecordTriggered', value: 'FlowRecordTriggered', label: 'Record Triggered Flows', title: 'Record Triggered Flows' }, { id: 'FlowProcessBuilder',