From ebdec9e8eadaadea56ff1c37af69afd2d4eaf25e Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 25 Nov 2023 20:27:15 -0700 Subject: [PATCH] Added formula editor to create fields Allow evaluating formula fields when creating new fields to make it more likely the formula is valid Added icons to all card pages resolves #556 --- .../anonymous-apex/AnonymousApex.tsx | 6 +- .../create-object-and-fields/CreateFields.tsx | 2 + .../CreateFieldsFormulaEditor.tsx | 385 ++++++++++++++++++ .../CreateFieldsFormulaEditorManualField.tsx | 162 ++++++++ .../CreateFieldsRow.tsx | 14 +- .../CreateFieldsRowField.tsx | 34 +- .../CreateFieldsRowPicklistOption.tsx | 18 +- .../debug-log-viewer/DebugLogViewer.tsx | 2 + .../formula-evaluator/FormulaEvaluator.tsx | 53 +-- .../formula-evaluator.editor-utils.ts | 23 +- .../src/app/components/home/AppHome.tsx | 10 +- .../PlatformEventMonitorListenerCard.tsx | 1 + .../PlatformEventMonitorPublisherCard.tsx | 1 + .../salesforce-api/SalesforceApiRequest.tsx | 5 +- .../salesforce-api/SalesforceApiResponse.tsx | 6 +- .../create-fields/create-fields-types.ts | 13 +- .../create-fields/create-fields-utils.tsx | 4 +- .../FormulaEvaluatorRecordSearch.tsx | 5 +- .../FormulaEvaluatorResults.tsx | 72 ++++ .../FormulaEvaluatorUserSearch.tsx | 0 .../formula-evaluator.utils.ts | 330 +++++++++------ libs/icon-factory/src/lib/icon-factory.tsx | 24 +- .../constants/src/lib/shared-constants.ts | 1 + libs/ui/src/lib/card/Card.tsx | 2 +- .../layout/page-header/PageHeaderTitle.tsx | 2 +- 25 files changed, 960 insertions(+), 215 deletions(-) create mode 100644 apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditor.tsx create mode 100644 apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditorManualField.tsx rename apps/jetstream/src/app/components/{ => shared}/formula-evaluator/FormulaEvaluatorRecordSearch.tsx (96%) create mode 100644 apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorResults.tsx rename apps/jetstream/src/app/components/{ => shared}/formula-evaluator/FormulaEvaluatorUserSearch.tsx (100%) rename apps/jetstream/src/app/components/{ => shared}/formula-evaluator/formula-evaluator.utils.ts (76%) diff --git a/apps/jetstream/src/app/components/anonymous-apex/AnonymousApex.tsx b/apps/jetstream/src/app/components/anonymous-apex/AnonymousApex.tsx index c56a8fac5..c349e2390 100644 --- a/apps/jetstream/src/app/components/anonymous-apex/AnonymousApex.tsx +++ b/apps/jetstream/src/app/components/anonymous-apex/AnonymousApex.tsx @@ -211,10 +211,6 @@ export const AnonymousApex: FunctionComponent = () => { window.open(loginUrl, 'Developer Console', 'height=600,width=600'); } - function handleLogLevelChange(event: React.ChangeEvent) { - setLogLevel(event.target.value); - } - return ( = () => {
Anonymous Apex
@@ -288,6 +285,7 @@ export const AnonymousApex: FunctionComponent = () => {
Results diff --git a/apps/jetstream/src/app/components/create-object-and-fields/CreateFields.tsx b/apps/jetstream/src/app/components/create-object-and-fields/CreateFields.tsx index 9c2c6a6e9..468dffef8 100644 --- a/apps/jetstream/src/app/components/create-object-and-fields/CreateFields.tsx +++ b/apps/jetstream/src/app/components/create-object-and-fields/CreateFields.tsx @@ -196,9 +196,11 @@ export const CreateFields: FunctionComponent = () => { {rows.map((row, i) => ( 1} selectedOrg={selectedOrg} + selectedSObjects={selectedSObjects} values={row} allValid={row._allValid} onChange={(field, value) => changeRow(row._key, field, value)} diff --git a/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditor.tsx b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditor.tsx new file mode 100644 index 000000000..d491b68fc --- /dev/null +++ b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditor.tsx @@ -0,0 +1,385 @@ +import { css } from '@emotion/react'; +import { logger } from '@jetstream/shared/client-logger'; +import { ANALYTICS_KEYS } from '@jetstream/shared/constants'; +import { useNonInitialEffect } from '@jetstream/shared/ui-utils'; +import { SplitWrapper as Split } from '@jetstream/splitjs'; +import { Maybe, SalesforceOrgUi } from '@jetstream/types'; +import { Grid, KeyboardShortcut, Modal, Spinner, Tabs, Textarea } from '@jetstream/ui'; +import Editor, { OnMount, useMonaco } from '@monaco-editor/react'; +import * as formulon from 'formulon'; +import { Field, FieldType } from 'jsforce'; +import type { editor } from 'monaco-editor'; +import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; +import { useAmplitude } from '../core/analytics'; +import { registerCompletions } from '../formula-evaluator/formula-evaluator.editor-utils'; +import { NullNumberBehavior } from '../formula-evaluator/formula-evaluator.state'; +import { + FieldDefinition, + FieldValue, + FieldValueState, + FieldValues, + ManualFormulaRecord, + SalesforceFieldType, +} from '../shared/create-fields/create-fields-types'; +import FormulaEvaluatorRecordSearch from '../shared/formula-evaluator/FormulaEvaluatorRecordSearch'; +import FormulaEvaluatorResults from '../shared/formula-evaluator/FormulaEvaluatorResults'; +import FormulaEvaluatorUserSearch from '../shared/formula-evaluator/FormulaEvaluatorUserSearch'; +import { getFormulaData } from '../shared/formula-evaluator/formula-evaluator.utils'; +import CreateFieldsFormulaEditorManualField from './CreateFieldsFormulaEditorManualField'; + +export interface CreateFieldsFormulaEditorProps { + id: string; + selectedOrg: SalesforceOrgUi; + selectedSObjects: string[]; + field: FieldDefinition; + disabled: Maybe; + allValues: FieldValues; + rows: FieldValues[]; + valueState: FieldValueState; + onChange: (value: FieldValue) => void; + onBlur: () => void; +} + +export const CreateFieldsFormulaEditor = forwardRef( + ({ id, selectedOrg, selectedSObjects = [], allValues, field, valueState, disabled = false, rows, onChange, onBlur }, ref) => { + const { value, touched, errorMessage } = valueState; + const isMounted = useRef(true); + const editorRef = useRef(); + const { trackEvent } = useAmplitude(); + + const [isOpen, setIsOpen] = useState(false); + const [loading, setLoading] = useState(false); + + const [fieldErrorMessage, setFieldErrorMessage] = useState(null); + const [formulaErrorMessage, setFormulaErrorMessage] = useState(null); + const [formulaValue, setFormulaValue] = useState((value as string) || ''); + const [additionalFieldMetadata, setAdditionalFieldMetadata] = useState[]>([]); + + const [testMethod, setTestMethod] = useState<'RECORD' | 'MANUAL'>('RECORD'); + const [formulaFields, setFormulaFields] = useState([]); + const [formulaFieldValues, setFormulaFieldValues] = useState({}); + + const [results, setResults] = useState<{ formulaFields: formulon.FormulaData; parsedFormula: formulon.FormulaResult } | null>(null); + + const [selectedUserId, setSelectedUserId] = useState(''); + const [recordId, setRecordId] = useState(''); + + const monaco = useMonaco(); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + monaco && registerCompletions(monaco, selectedOrg, selectedSObjects[0], additionalFieldMetadata); + }, [monaco, selectedOrg, additionalFieldMetadata, selectedSObjects]); + + /** Reset Results */ + useEffect(() => { + setResults(null); + }, [testMethod]); + + useEffect(() => { + setFormulaFields((prevValue) => { + try { + const fields = formulon.extract(formulaValue || ''); + return fields; + } catch (ex) { + return prevValue; + } + }); + }, [formulaValue]); + + useEffect(() => { + if (isOpen) { + setTestMethod('RECORD'); + setFormulaFields([]); + setFormulaFieldValues({}); + setResults(null); + setFormulaValue((value as string) || ''); + setAdditionalFieldMetadata( + rows + .filter((row) => row.fullName.value && allValues.fullName.value !== row.fullName.value) + .map((row): Partial => { + return { + name: (row.fullName.value as string) || '', + label: `${(row.label.value as string) || ''} (NEW FIELD)`, + type: row.type.value as FieldType, + relationshipName: (row.relationshipName.value as string) || null, + referenceTo: row.referenceTo.value ? [row.referenceTo.value as string] : [], + }; + }) + ); + } + }, [allValues.fullName.value, isOpen, rows, value]); + + const handleTestFormula = useCallback( + async (value: string) => { + try { + setLoading(true); + setFieldErrorMessage(null); + setFormulaErrorMessage(null); + setResults(null); + + let formulaFieldResults: formulon.FormulaData = {}; + if (formulaFields.length) { + let payload: Parameters[0] = { + fields: formulaFields, + recordId, + selectedOrg, + selectedUserId, + sobjectName: selectedSObjects[0] || '', + numberNullBehavior: (allValues.formulaTreatBlanksAs.value as NullNumberBehavior) || 'BLANK', + }; + + if (testMethod === 'MANUAL' || !recordId) { + // ensure all fields are included in record + const record = { + ...formulaFieldValues, + }; + formulaFields.forEach((field) => { + if (!record[field]) { + record[field] = { + type: 'string', + value: null, + }; + } + }); + payload = { + fields: formulaFields, + type: 'PROVIDED_RECORD', + record, + selectedOrg, + selectedUserId, + sobjectName: selectedSObjects[0] || '', + numberNullBehavior: (allValues.formulaTreatBlanksAs.value as NullNumberBehavior) || 'BLANK', + }; + } + + const response = await getFormulaData(payload); + if (response.type === 'error') { + setFieldErrorMessage(response.message); + return; + } + formulaFieldResults = response.formulaFields; + } + const parsedFormula = formulon.parse(value, formulaFieldResults); + logger.log('results', parsedFormula); + setResults({ + formulaFields: formulaFieldResults, + parsedFormula, + }); + trackEvent(ANALYTICS_KEYS.sobj_create_field_formula_execute, { success: true, fieldCount: formulaFields.length, testMethod }); + } catch (ex) { + logger.warn(ex); + setFormulaErrorMessage(ex.message); + trackEvent(ANALYTICS_KEYS.sobj_create_field_formula_execute, { success: false, message: ex.message, stack: ex.stack }); + } finally { + setLoading(false); + } + }, + [ + allValues.formulaTreatBlanksAs.value, + formulaFieldValues, + formulaFields, + recordId, + selectedOrg, + selectedSObjects, + selectedUserId, + testMethod, + trackEvent, + ] + ); + + useNonInitialEffect(() => { + if (monaco && editorRef.current) { + editorRef.current.addAction({ + id: 'modifier-enter', + label: 'Submit', + keybindings: [monaco?.KeyMod.CtrlCmd | monaco?.KeyCode.Enter], + run: (currEditor) => { + handleTestFormula(currEditor.getValue()); + }, + }); + } + }, [handleTestFormula, monaco, selectedOrg]); + + function handleEditorChange(value, event) { + setFormulaValue(value); + } + + const handleApexEditorMount: OnMount = (currEditor, monaco) => { + editorRef.current = currEditor; + // this did not run on initial render if used in useEffect + editorRef.current.addAction({ + id: 'modifier-enter', + label: 'Submit', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + run: (currEditor) => { + handleTestFormula(currEditor.getValue()); + }, + }); + }; + + function handleClose() { + onChange(formulaValue); + setIsOpen(false); + } + + return ( +
+ + + {isOpen && ( + + + + } + onClose={handleClose} + > +
+ {loading && } + + + + +
+ +
+ +

+ Test Formula +

+ setTestMethod(value)} + initialActiveId={testMethod} + tabs={[ + { + id: 'RECORD', + title: 'Use Record', + content: ( + + ), + }, + { + id: 'MANUAL', + title: 'Use Manual Values', + content: ( + + {!formulaFields.length &&

There are no record fields in your formula.

} + {formulaFields.map((field) => ( + { + setFormulaFieldValues((prevValue) => ({ + ...prevValue, + [field]: { + type, + value, + }, + })); + }} + /> + ))} +
+ ), + }, + ]} + >
+
+ + + +
+
+
+
+ )} +
+ ); + } +); + +export default CreateFieldsFormulaEditor; diff --git a/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditorManualField.tsx b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditorManualField.tsx new file mode 100644 index 000000000..2564a1616 --- /dev/null +++ b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsFormulaEditorManualField.tsx @@ -0,0 +1,162 @@ +import { ListItem } from '@jetstream/types'; +import { Checkbox, ComboboxWithItems, DatePicker, DateTime, Grid, Input, TimePicker } from '@jetstream/ui'; +import formatISO from 'date-fns/formatISO'; +import isValid from 'date-fns/isValid'; +import parseISO from 'date-fns/parseISO'; +import isDate from 'lodash/isDate'; +import { forwardRef } from 'react'; +import { FieldValue, ManualFormulaFieldType } from '../shared/create-fields/create-fields-types'; + +const FieldTypeItems: ListItem[] = [ + { id: 'string', label: 'Text', value: 'string' }, + { id: 'double', label: 'Number', value: 'double' }, + { id: 'boolean', label: 'Checkbox', value: 'boolean' }, + { id: 'date', label: 'Date', value: 'date' }, + { id: 'datetime', label: 'Datetime', value: 'datetime' }, + { id: 'time', label: 'Time', value: 'time' }, +]; + +function getNumericValue(value: FieldValue): string { + if (typeof value === 'number') { + return String(value); + } + if (value === '-') { + return value; + } + if (typeof value === 'string') { + const currValue = parseFloat(value); + return isNaN(currValue) ? '' : String(currValue); + } + return ''; +} + +function getDateValue(value: FieldValue): Date { + if (typeof value === 'string') { + const date = parseISO(value); + return isValid(date) ? date : new Date(); + } + return new Date(); +} + +export interface CreateFieldsFormulaEditorManualFieldProps { + field: string; + fieldType: ManualFormulaFieldType; + fieldValue: FieldValue; + disabled?: boolean; + onChange: (type: ManualFormulaFieldType, value: FieldValue | null) => void; +} + +export const CreateFieldsFormulaEditorManualField = forwardRef( + ({ field, fieldType, fieldValue, disabled, onChange }, ref) => { + function handleTypeChange(item: ListItem) { + const value = fieldValue; + const newType = item.value; + if (newType === 'string') { + onChange(newType, String(value)); + } else if (newType === 'double') { + onChange(newType, getNumericValue(value)); + } else if (newType === 'boolean') { + onChange(newType, !!value); + } else if (newType === 'date') { + onChange(newType, formatISO(getDateValue(value), { representation: 'date' })); + } else if (newType === 'datetime') { + onChange(newType, formatISO(getDateValue(value))); + } else if (newType === 'time') { + onChange(newType, String(value)); + } else { + throw new Error(`Unknown type: ${newType}`); + } + } + + return ( + <> +

{field}

+ + + {fieldType === 'string' && ( + + onChange(fieldType, event.target.value)} + disabled={disabled} + /> + + )} + {fieldType === 'double' && ( + + onChange(fieldType, event.target.value)} + disabled={disabled} + /> + + )} + {fieldType === 'boolean' && ( + onChange(fieldType, value)} + /> + )} + {fieldType === 'date' && ( + onChange(fieldType, isDate(value) ? formatISO(value, { representation: 'date' }) : null)} + /> + )} + {fieldType === 'datetime' && ( + onChange(fieldType, isDate(value) ? formatISO(value) : null)} + /> + )} + {fieldType === 'time' && ( + onChange(fieldType, value)} + /> + )} + + + ); + } +); + +export default CreateFieldsFormulaEditorManualField; diff --git a/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRow.tsx b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRow.tsx index 85d97f07f..1094d2f02 100644 --- a/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRow.tsx +++ b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRow.tsx @@ -1,6 +1,6 @@ import { SalesforceOrgUi } from '@jetstream/types'; import { Grid, Icon, Tooltip } from '@jetstream/ui'; -import React, { FunctionComponent } from 'react'; +import { FunctionComponent } from 'react'; import { FieldDefinitionType, FieldValue, FieldValues, SalesforceFieldType } from '../shared/create-fields/create-fields-types'; import { baseFields, @@ -17,8 +17,10 @@ export interface CreateFieldsRowProps { rowIdx: number; enableDelete?: boolean; selectedOrg: SalesforceOrgUi; + selectedSObjects: string[]; values: FieldValues; allValid: boolean; + rows: FieldValues[]; onChange: (field: FieldDefinitionType, value: FieldValue) => void; onClone: () => void; onDelete: () => void; @@ -30,8 +32,10 @@ export const CreateFieldsRow: FunctionComponent = ({ rowIdx, enableDelete, selectedOrg, + selectedSObjects, values, allValid, + rows, onChange, onClone, onDelete, @@ -49,6 +53,8 @@ export const CreateFieldsRow: FunctionComponent = ({ = ({ = ({ = ({ void; onBlur: () => void; } -export const CreateFieldsRowField = forwardRef( - ({ selectedOrg, id, field, allValues, valueState, disabled: _disabled, onChange, onBlur }, ref) => { +export const CreateFieldsRowField = forwardRef( + ({ selectedOrg, id, selectedSObjects, field, allValues, valueState, disabled: _disabled, rows, onChange, onBlur }, ref) => { const { value, touched, errorMessage } = valueState; const disabled = _disabled || field?.disabled?.(allValues); const [values, setValues] = useState([]); @@ -196,6 +199,29 @@ export const CreateFieldsRowField = forwardRef(
); + case 'textarea-with-formula': + return ( +
+ {loadingValues && } + +
+ ); case 'text': default: return ( diff --git a/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRowPicklistOption.tsx b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRowPicklistOption.tsx index e416ec740..8a7785f5e 100644 --- a/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRowPicklistOption.tsx +++ b/apps/jetstream/src/app/components/create-object-and-fields/CreateFieldsRowPicklistOption.tsx @@ -1,15 +1,17 @@ import { SalesforceOrgUi } from '@jetstream/types'; import { Checkbox, Grid } from '@jetstream/ui'; -import React, { FunctionComponent, useRef } from 'react'; -import { FieldDefinitions, FieldDefinitionType, FieldValue, FieldValues } from '../shared/create-fields/create-fields-types'; +import { FunctionComponent, useRef } from 'react'; +import { FieldDefinitionType, FieldDefinitions, FieldValue, FieldValues } from '../shared/create-fields/create-fields-types'; import CreateFieldsRowField from './CreateFieldsRowField'; import CreateNewGlobalPicklistModal from './CreateNewGlobalPicklistModal'; export interface CreateFieldsRowPicklistOptionProps { rowIdx: number; selectedOrg: SalesforceOrgUi; + selectedSObjects: string[]; values: FieldValues; fieldDefinitions: FieldDefinitions; + rows: FieldValues[]; disabled: boolean; onChangePicklistOption: (value: boolean) => void; onChange: (field: FieldDefinitionType, value: FieldValue) => void; @@ -19,8 +21,10 @@ export interface CreateFieldsRowPicklistOptionProps { export const CreateFieldsRowPicklistOption: FunctionComponent = ({ rowIdx, selectedOrg, - values, + selectedSObjects, fieldDefinitions, + values, + rows, disabled, onChange, onChangePicklistOption, @@ -48,8 +52,10 @@ export const CreateFieldsRowPicklistOption: FunctionComponent = () => {
Debug Logs
@@ -260,6 +261,7 @@ export const DebugLogViewer: FunctionComponent = () => {
Log Results
} actions={ diff --git a/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluator.tsx b/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluator.tsx index 6898f8449..131f922a1 100644 --- a/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluator.tsx +++ b/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluator.tsx @@ -17,7 +17,6 @@ import { Radio, RadioButton, RadioGroup, - ScopedNotification, SobjectCombobox, SobjectComboboxRef, SobjectFieldCombobox, @@ -33,12 +32,13 @@ import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'rea import { useRecoilState, useRecoilValue } from 'recoil'; import { applicationCookieState, selectedOrgState } from '../../app-state'; import { useAmplitude } from '../core/analytics'; -import FormulaEvaluatorRecordSearch from './FormulaEvaluatorRecordSearch'; -import FormulaEvaluatorUserSearch from './FormulaEvaluatorUserSearch'; +import FormulaEvaluatorRecordSearch from '../shared/formula-evaluator/FormulaEvaluatorRecordSearch'; +import FormulaEvaluatorResults from '../shared/formula-evaluator/FormulaEvaluatorResults'; +import FormulaEvaluatorUserSearch from '../shared/formula-evaluator/FormulaEvaluatorUserSearch'; +import { getFormulaData } from '../shared/formula-evaluator/formula-evaluator.utils'; import FormulaEvaluatorDeployModal from './deploy/FormulaEvaluatorDeployModal'; import { registerCompletions } from './formula-evaluator.editor-utils'; import * as fromFormulaState from './formula-evaluator.state'; -import { getFormulaData } from './formula-evaluator.utils'; // Lazy import const prettier = import('prettier/standalone'); @@ -166,6 +166,7 @@ export const FormulaEvaluator: FunctionComponent = () => const onKeydown = useCallback( (event: KeyboardEvent) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!testFormulaDisabled && hasModifierKey(event as any) && isEnterKey(event as any)) { event.stopPropagation(); event.preventDefault(); @@ -290,6 +291,7 @@ export const FormulaEvaluator: FunctionComponent = () =>
Formula Evaluator
@@ -418,6 +420,7 @@ export const FormulaEvaluator: FunctionComponent = () =>
Results
} actions={ <> @@ -439,7 +442,6 @@ export const FormulaEvaluator: FunctionComponent = () => } > {loading && } - = () => fieldErrorMessage={fieldErrorMessage} onSelectedRecord={setRecordId} /> - {errorMessage && ( -
- - {errorMessage} - -
- )} - {results && ( - - {!!Object.keys(results.formulaFields).length && ( - <> -
Record Fields
-
- {Object.keys(results.formulaFields).map((field) => { - const { value } = results.formulaFields[field]; - return ( -
- {field}: {String(value) || ''} -
- ); - })} -
- - )} -
Formula Results
-
- {results.parsedFormula.type === 'error' ? ( - -
{results.parsedFormula.errorType}
-
{results.parsedFormula.message}
- {results.parsedFormula.errorType === 'NotImplementedError' && results.parsedFormula.name === 'isnull' && ( -
Use ISBLANK instead
- )} -
- ) : ( -
{String(results.parsedFormula.value)}
- )} -
-
- )} + {!errorMessage && !results && ( [] +) { if (priorCompletion) { priorCompletion.dispose(); } @@ -38,7 +43,7 @@ export async function registerCompletions(monaco: Monaco, selectedOrg: Salesforc provideCompletionItems: async (model, position, context, token) => { const characterInfo = getCharacterInfo(model, position); return { - suggestions: await fetchCompletions(monaco, characterInfo, selectedOrg, sobject), + suggestions: await fetchCompletions(monaco, characterInfo, selectedOrg, sobject, additionalFields), }; }, }); @@ -90,7 +95,8 @@ async function fetchCompletions( monaco: Monaco, characterInfo: CharacterInfo, selectedOrg: SalesforceOrgUi, - sobject?: string + sobject?: string, + additionalFields?: Partial[] ): Promise { const { textUntilPosition: textUntilPositionAll, mostRecentCharacter, range } = characterInfo; // if spaces, ignore prior words - e.x. "Log__r.ApiVersion__c != LoggedBy__r" we only care about LoggedBy__r @@ -106,7 +112,7 @@ async function fetchCompletions( } } - let currentSObjectMeta: DescribeSObjectResult; + let currentSObjectMeta: Omit & { fields: Partial[] }; /** * Handle all special words (starts with `$`) @@ -221,7 +227,10 @@ async function fetchCompletions( */ const { data } = await describeSObject(selectedOrg, sobject); // Current object metadata - will be changed when fetching related object metadata - currentSObjectMeta = data; + currentSObjectMeta = { + ...data, + fields: [...data.fields, ...(additionalFields || [])], + }; // If true, then special keywords will be included in results let isRootObject = true; // Completions are added to the list @@ -265,7 +274,7 @@ async function fetchCompletions( detail: field.type, filterText: field.name, kind: monaco.languages.CompletionItemKind.Class, - insertText: field.name, + insertText: field.name!, range, }); if (!!field.relationshipName && !!field.referenceTo?.length) { diff --git a/apps/jetstream/src/app/components/home/AppHome.tsx b/apps/jetstream/src/app/components/home/AppHome.tsx index 149a929b2..da33b6f2f 100644 --- a/apps/jetstream/src/app/components/home/AppHome.tsx +++ b/apps/jetstream/src/app/components/home/AppHome.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/react'; +import { IconName, IconType } from '@jetstream/icon-factory'; import { Badge, Icon } from '@jetstream/ui'; import classNames from 'classnames'; import { Fragment } from 'react'; @@ -76,10 +77,13 @@ export const AppHome = () => {
{card.icon && ( )}
diff --git a/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorListenerCard.tsx b/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorListenerCard.tsx index c8101eae6..ddc4ba7bd 100644 --- a/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorListenerCard.tsx +++ b/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorListenerCard.tsx @@ -34,6 +34,7 @@ export const PlatformEventMonitorListenerCard: FunctionComponent
Subscribe to Events
diff --git a/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorPublisherCard.tsx b/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorPublisherCard.tsx index 0f8c29a66..19a61fd57 100644 --- a/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorPublisherCard.tsx +++ b/apps/jetstream/src/app/components/platform-event-monitor/PlatformEventMonitorPublisherCard.tsx @@ -139,6 +139,7 @@ export const PlatformEventMonitorPublisherCard: FunctionComponent return (
Salesforce API Request
diff --git a/apps/jetstream/src/app/components/salesforce-api/SalesforceApiResponse.tsx b/apps/jetstream/src/app/components/salesforce-api/SalesforceApiResponse.tsx index e19170f5b..b664c4b7d 100644 --- a/apps/jetstream/src/app/components/salesforce-api/SalesforceApiResponse.tsx +++ b/apps/jetstream/src/app/components/salesforce-api/SalesforceApiResponse.tsx @@ -13,7 +13,11 @@ export interface SalesforceApiResponseProps { export const SalesforceApiResponse: FunctionComponent = ({ loading, request, results }) => { return ( - }> + } + > {loading && } ; + export type FieldDefinitionMetadata = Partial>; export type FieldDefinitions = Record; @@ -65,7 +76,7 @@ export type SalesforceFieldType = | 'LongTextArea' | 'Html'; -export type FieldDefinitionUiType = 'picklist' | 'text' | 'textarea' | 'radio' | 'checkbox'; +export type FieldDefinitionUiType = 'picklist' | 'text' | 'textarea' | 'textarea-with-formula' | 'radio' | 'checkbox'; export interface FieldValueState { value: FieldValue; diff --git a/apps/jetstream/src/app/components/shared/create-fields/create-fields-utils.tsx b/apps/jetstream/src/app/components/shared/create-fields/create-fields-utils.tsx index 3e6fb6cc1..bfb8a12bf 100644 --- a/apps/jetstream/src/app/components/shared/create-fields/create-fields-utils.tsx +++ b/apps/jetstream/src/app/components/shared/create-fields/create-fields-utils.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { logger } from '@jetstream/shared/client-logger'; import { describeGlobal, genericRequest, queryAllFromList, queryWithCache } from '@jetstream/shared/data'; import { REGEX, ensureBoolean, splitArrayToMaxSize } from '@jetstream/shared/utils'; @@ -301,9 +302,8 @@ export const fieldDefinitions: FieldDefinitions = { }, formula: { label: 'Formula', - type: 'textarea', + type: 'textarea-with-formula', required: true, - // TODO: would be cool to have syntax highlighting }, formulaTreatBlanksAs: { label: 'Treat Blanks As', diff --git a/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluatorRecordSearch.tsx b/apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorRecordSearch.tsx similarity index 96% rename from apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluatorRecordSearch.tsx rename to apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorRecordSearch.tsx index 185d9578b..d0cdade64 100644 --- a/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluatorRecordSearch.tsx +++ b/apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorRecordSearch.tsx @@ -1,11 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { logger } from '@jetstream/shared/client-logger'; import { describeSObject, query } from '@jetstream/shared/data'; import { CloneEditView, ListItem, Maybe, SalesforceOrgUi } from '@jetstream/types'; import { ComboboxWithItemsTypeAhead, Grid, Icon, Tooltip } from '@jetstream/ui'; import { FunctionComponent, useCallback, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; -import { applicationCookieState } from '../../app-state'; -import ViewEditCloneRecord from '../core/ViewEditCloneRecord'; +import { applicationCookieState } from '../../../app-state'; +import ViewEditCloneRecord from '../../core/ViewEditCloneRecord'; export interface FormulaEvaluatorRecordSearchProps { selectedOrg: SalesforceOrgUi; diff --git a/apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorResults.tsx b/apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorResults.tsx new file mode 100644 index 000000000..5d10e8505 --- /dev/null +++ b/apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorResults.tsx @@ -0,0 +1,72 @@ +import { Maybe } from '@jetstream/types'; +import { Grid, ScopedNotification } from '@jetstream/ui'; +import formatISO from 'date-fns/formatISO'; +import * as formulon from 'formulon'; +import { FunctionComponent } from 'react'; + +export interface FormulaEvaluatorResultsProps { + errorMessage?: Maybe; + results: { + formulaFields: formulon.FormulaData; + parsedFormula: formulon.FormulaResult; + } | null; +} + +function getValue(value: string | number | boolean | null | Date): string { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + if (value instanceof Date) { + return formatISO(value, { representation: 'date' }); + } + return 'null'; +} + +export const FormulaEvaluatorResults: FunctionComponent = ({ errorMessage, results }) => { + return ( + <> + {errorMessage && ( +
+ + {errorMessage} + +
+ )} + {results && ( + + {!!Object.keys(results.formulaFields).length && ( + <> +
Record Fields
+
+ {Object.keys(results.formulaFields).map((field) => { + const { value } = results.formulaFields[field]; + return ( +
+ {field}: {String(value) || ''} +
+ ); + })} +
+ + )} +
Formula Results
+
+ {results.parsedFormula.type === 'error' ? ( + +
{results.parsedFormula.errorType}
+
{results.parsedFormula.message}
+ {results.parsedFormula.errorType === 'NotImplementedError' && results.parsedFormula.name === 'isnull' && ( +
Use ISBLANK instead
+ )} +
+ ) : ( +
{getValue(results.parsedFormula.value) ?? ''}
+ )} +
+
+ )} + + ); +}; + +export default FormulaEvaluatorResults; diff --git a/apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluatorUserSearch.tsx b/apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorUserSearch.tsx similarity index 100% rename from apps/jetstream/src/app/components/formula-evaluator/FormulaEvaluatorUserSearch.tsx rename to apps/jetstream/src/app/components/shared/formula-evaluator/FormulaEvaluatorUserSearch.tsx diff --git a/apps/jetstream/src/app/components/formula-evaluator/formula-evaluator.utils.ts b/apps/jetstream/src/app/components/shared/formula-evaluator/formula-evaluator.utils.ts similarity index 76% rename from apps/jetstream/src/app/components/formula-evaluator/formula-evaluator.utils.ts rename to apps/jetstream/src/app/components/shared/formula-evaluator/formula-evaluator.utils.ts index 11bdb03cf..31714a7fd 100644 --- a/apps/jetstream/src/app/components/formula-evaluator/formula-evaluator.utils.ts +++ b/apps/jetstream/src/app/components/shared/formula-evaluator/formula-evaluator.utils.ts @@ -10,10 +10,12 @@ import { DataType, FormulaDataValue } from 'formulon'; import type { Field } from 'jsforce'; import lodashGet from 'lodash/get'; import isNil from 'lodash/isNil'; +import isString from 'lodash/isString'; import { composeQuery, getField } from 'soql-parser-js'; -import { fetchMetadataFromSoql } from '../query/utils/query-soql-utils'; -import { NullNumberBehavior } from './formula-evaluator.state'; -import { FormulaFieldsByType } from './formula-evaluator.types'; +import { NullNumberBehavior } from '../../formula-evaluator/formula-evaluator.state'; +import { FormulaFieldsByType } from '../../formula-evaluator/formula-evaluator.types'; +import { fetchMetadataFromSoql } from '../../query/utils/query-soql-utils'; +import { ManualFormulaRecord } from '../create-fields/create-fields-types'; const MATCH_FORMULA_SPECIAL_LABEL = /^\$[a-zA-Z]+\./; @@ -34,7 +36,7 @@ export function getFormulonTypeFromColumnType(col: QueryResultsColumn): DataType return 'text'; } -export function getFormulonTypeFromMetadata(col: Field): DataType { +export function getFormulonTypeFromMetadata(col: T): DataType { if (col.type === 'boolean') { return 'checkbox'; } else if (col.type === 'double' || col.type === 'currency' || col.type === 'percent' || col.type === 'int') { @@ -58,11 +60,16 @@ export function getFormulonTypeFromMetadata(col: Field): DataType { /** * Function that determines if the provided value is of type QueryResultsColumn or Field */ -function isQueryResultsColumn(col: QueryResultsColumn | Field): col is QueryResultsColumn { +function isQueryResultsColumn(col: QueryResultsColumn | Field | { type: Field['type'] }): col is QueryResultsColumn { return (col as QueryResultsColumn).booleanType !== undefined; } -export function getFormulonData(col: QueryResultsColumn | Field, value: any, numberNullBehavior = 'ZERO'): FormulaDataValue { +export function getFormulonData( + col: QueryResultsColumn | Field | { type: Field['type'] }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any, + numberNullBehavior = 'ZERO' +): FormulaDataValue { const dataType = isQueryResultsColumn(col) ? getFormulonTypeFromColumnType(col) : getFormulonTypeFromMetadata(col); if (dataType === 'text') { return { @@ -75,15 +82,16 @@ export function getFormulonData(col: QueryResultsColumn | Field, value: any, num }; } if (dataType === 'number') { - const { length, scale } = isQueryResultsColumn(col) - ? { - length: getPrecision(value) - 18, - scale: getPrecision(value), - } - : { - length: (isNil(col.precision) ? getPrecision(value) : col.precision) - col.scale, - scale: col.scale, - }; + const { length, scale } = + isQueryResultsColumn(col) || !('precision' in col) || !('scale' in col) + ? { + length: getPrecision(value) - 18, + scale: getPrecision(value), + } + : { + length: (isNil(col.precision) ? getPrecision(value) : col.precision) - col.scale, + scale: col.scale, + }; return { type: 'literal', dataType, @@ -107,10 +115,7 @@ export function getFormulonData(col: QueryResultsColumn | Field, value: any, num type: 'literal', dataType, value: isNil(value) ? null : parseISO(value), - options: { - length: value.length, - scale: getPrecision(value), - }, + options: {}, }; } if (dataType === 'picklist' || dataType === 'multipicklist') { @@ -123,14 +128,6 @@ export function getFormulonData(col: QueryResultsColumn | Field, value: any, num }, }; } - // FIXME: need to test a bunch of data types here - // if (isNil(value)) { - // return { - // type: 'literal', - // dataType: 'null', - // value: null, - // }; - // } return { type: 'literal', dataType, @@ -138,8 +135,11 @@ export function getFormulonData(col: QueryResultsColumn | Field, value: any, num }; } -function getPrecision(a) { - if (!isFinite(a)) return 0; +function getPrecision(a: number | string) { + a = isString(a) ? Number(a) : a; + if (!isFinite(a)) { + return 0; + } let e = 1; let p = 0; while (Math.round(a * e) / e !== a) { @@ -149,124 +149,163 @@ function getPrecision(a) { return p; } +interface FormulaDataProps { + selectedOrg: SalesforceOrgUi; + selectedUserId: string; + fields: string[]; + /** + * If recordId is provided, the record will be queried from the org + * In this case, type can be omitted, if provided it should be 'QUERY_RECORD' + */ + type?: 'QUERY_RECORD'; + recordId: string; + record?: never; + sobjectName: string; + numberNullBehavior: NullNumberBehavior; +} + +interface FormulaDataProvidedRecordProps { + selectedOrg: SalesforceOrgUi; + selectedUserId: string; + fields: string[]; + /** + * If record is provided, the record will be used directly instead of querying from the org + */ + type: 'PROVIDED_RECORD'; + recordId?: never; + record: ManualFormulaRecord; + sobjectName: string; + numberNullBehavior: NullNumberBehavior; +} + export async function getFormulaData({ selectedOrg, selectedUserId, fields, + type = 'QUERY_RECORD', recordId, + record, sobjectName, numberNullBehavior = 'ZERO', -}: { - selectedOrg: SalesforceOrgUi; - selectedUserId: string; - fields: string[]; - recordId: string; - sobjectName: string; - numberNullBehavior: NullNumberBehavior; -}): Promise< +}: FormulaDataProps | FormulaDataProvidedRecordProps): Promise< | { type: 'error'; message: string } | { type: 'success'; formulaFields: formulon.FormulaData; warnings: { type: string; message: string }[] } > { - const formulaFields: formulon.FormulaData = {}; - const warnings = []; - - const { - objectFields, - apiFields, - customMetadata, - customLabels, - organization, - customPermissions, - profile, - customSettings, - system, - user, - userRole, - } = fields.reduce( - (output: FormulaFieldsByType, field) => { - if (!field.startsWith('$')) { - output.objectFields.push(field); - } else { - const identifier = field.toLowerCase().split('.')[0]; - switch (identifier) { - case '$api': - output.apiFields.push(field); - break; - case '$custommetadata': - output.customMetadata.push(field); - break; - case '$label': - output.customLabels.push(field); - break; - case '$organization': - output.organization.push(field); - break; - case '$permission': - output.customPermissions.push(field); - break; - case '$profile': - output.profile.push(field); - break; - case '$setup': - output.customSettings.push(field); - break; - case '$system': - output.system.push(field); - break; - case '$user': - output.user.push(field); - break; - case '$userrole': - output.userRole.push(field); - break; - default: - break; + try { + const formulaFields: formulon.FormulaData = {}; + const warnings = []; + + const { + objectFields, + apiFields, + customMetadata, + customLabels, + organization, + customPermissions, + profile, + customSettings, + system, + user, + userRole, + } = fields.reduce( + (output: FormulaFieldsByType, field) => { + if (!field.startsWith('$')) { + output.objectFields.push(field); + } else { + const identifier = field.toLowerCase().split('.')[0]; + switch (identifier) { + case '$api': + output.apiFields.push(field); + break; + case '$custommetadata': + output.customMetadata.push(field); + break; + case '$label': + output.customLabels.push(field); + break; + case '$organization': + output.organization.push(field); + break; + case '$permission': + output.customPermissions.push(field); + break; + case '$profile': + output.profile.push(field); + break; + case '$setup': + output.customSettings.push(field); + break; + case '$system': + output.system.push(field); + break; + case '$user': + output.user.push(field); + break; + case '$userrole': + output.userRole.push(field); + break; + default: + break; + } } + return output; + }, + { + objectFields: [], + apiFields: [], + customMetadata: [], + customLabels: [], + organization: [], + customPermissions: [], + profile: [], + customSettings: [], + system: [], + user: [], + userRole: [], } - return output; - }, - { - objectFields: [], - apiFields: [], - customMetadata: [], - customLabels: [], - organization: [], - customPermissions: [], - profile: [], - customSettings: [], - system: [], - user: [], - userRole: [], + ); + + // TODO: this is a good candidate for unit tests + // TODO: collect warnings + // These should also be somewhat forgiving + if (type === 'QUERY_RECORD' && recordId) { + await collectBaseQueriedRecordFields({ selectedOrg, fields: objectFields, recordId, sobjectName, formulaFields, numberNullBehavior }); + } else { + await collectBaseRecordFields({ + fields, + record: record || {}, + formulaFields, + numberNullBehavior, + }); } - ); - // TODO: this is a good candidate for unit tests - // TODO: collect warnings - // These should also be somewhat forgiving - await collectBaseRecordFields({ selectedOrg, fields: objectFields, recordId, sobjectName, formulaFields, numberNullBehavior }); - collectApiFields({ selectedOrg, fields: apiFields, formulaFields, numberNullBehavior }); - await collectCustomMetadata({ selectedOrg, fields: customMetadata, formulaFields, numberNullBehavior }); - await collectCustomSettingFields({ selectedOrg, selectedUserId, fields: customSettings, formulaFields, numberNullBehavior }); - await collectCustomPermissions({ selectedOrg, selectedUserId, fields: customPermissions, formulaFields, numberNullBehavior }); - await collectLabels({ selectedOrg, fields: customLabels, formulaFields, numberNullBehavior }); - await collectOrganizationFields({ selectedOrg, fields: organization, formulaFields, numberNullBehavior }); - await collectUserProfileAndRoleFields({ - selectedOrg, - selectedUserId, - userFields: user, - profileFields: profile, - roleFields: userRole, - formulaFields, - numberNullBehavior, - }); - await collectSystemFields({ fields: system, formulaFields }); + collectApiFields({ selectedOrg, fields: apiFields, formulaFields, numberNullBehavior }); + await collectCustomMetadata({ selectedOrg, fields: customMetadata, formulaFields, numberNullBehavior }); + await collectCustomSettingFields({ selectedOrg, selectedUserId, fields: customSettings, formulaFields, numberNullBehavior }); + await collectCustomPermissions({ selectedOrg, selectedUserId, fields: customPermissions, formulaFields, numberNullBehavior }); + await collectLabels({ selectedOrg, fields: customLabels, formulaFields, numberNullBehavior }); + await collectOrganizationFields({ selectedOrg, fields: organization, formulaFields, numberNullBehavior }); + await collectUserProfileAndRoleFields({ + selectedOrg, + selectedUserId, + userFields: user, + profileFields: profile, + roleFields: userRole, + formulaFields, + numberNullBehavior, + }); + await collectSystemFields({ fields: system, formulaFields }); - logger.log({ formulaFields, warnings }); + logger.log({ formulaFields, warnings }); - return { type: 'success', formulaFields, warnings }; + return { type: 'success', formulaFields, warnings }; + } catch (ex) { + logger.error(ex); + throw ex; + } } -async function collectBaseRecordFields({ +async function collectBaseQueriedRecordFields({ selectedOrg, fields, recordId, @@ -284,6 +323,7 @@ async function collectBaseRecordFields({ if (!fields.length) { return; } + const { queryResults, columns, parsedQuery } = await query( selectedOrg, composeQuery({ @@ -325,6 +365,33 @@ async function collectBaseRecordFields({ }); } +async function collectBaseRecordFields({ + fields, + record, + formulaFields, + numberNullBehavior, +}: { + fields: string[]; + record: ManualFormulaRecord; + formulaFields: formulon.FormulaData; + numberNullBehavior: NullNumberBehavior; +}) { + if (!fields.length) { + return; + } + + fields.forEach((field) => { + // Prefer to get field from actual metadata, otherwise picklist is not handled and can cause formula errors + const recordValue = record[field]; + if (!recordValue) { + throw new Error(`Field ${field} does not exist in provided record data.`); + } + const { type, value } = recordValue; + + formulaFields[field] = getFormulonData({ type }, value, numberNullBehavior); + }); +} + function collectApiFields({ selectedOrg, fields, @@ -475,6 +542,7 @@ async function collectLabels({ fields.forEach((fieldWithIdentifier) => { const field = fieldWithIdentifier.replace(MATCH_FORMULA_SPECIAL_LABEL, ''); const recordName = field.toLowerCase(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const record: Record | undefined = recordsByApiName[recordName]; formulaFields[fieldWithIdentifier] = { type: 'literal', @@ -789,8 +857,8 @@ function getFieldsByName(columns: Maybe) { ); } -function getRecordsByLowercaseField(records: Record[], field: string): Record> { - return records.reduce((output: Record>, record) => { +function getRecordsByLowercaseField(records: Record[], field: string): Record> { + return records.reduce((output: Record>, record) => { output[record[field].toLowerCase()] = record; return output; }, {}); diff --git a/libs/icon-factory/src/lib/icon-factory.tsx b/libs/icon-factory/src/lib/icon-factory.tsx index e81dcca82..a5e73a29c 100644 --- a/libs/icon-factory/src/lib/icon-factory.tsx +++ b/libs/icon-factory/src/lib/icon-factory.tsx @@ -15,19 +15,25 @@ import DoctypeIcon_Image from './icons/doctype/Image'; import DoctypeIcon_Pack from './icons/doctype/Pack'; import DoctypeIcon_Xml from './icons/doctype/Xml'; import DoctypeIcon_Zip from './icons/doctype/Zip'; +import StandardIcon_ActionsAndButtons from './icons/standard/ActionsAndButtons'; import StandardIcon_Activations from './icons/standard/Activations'; import StandardIcon_Apex from './icons/standard/Apex'; import StandardIcon_AssetRelationship from './icons/standard/AssetRelationship'; import StandardIcon_BundleConfig from './icons/standard/BundleConfig'; import StandardIcon_DataStreams from './icons/standard/DataStreams'; import StandardIcon_Entity from './icons/standard/Entity'; +import StandardIcon_Events from './icons/standard/Events'; +import StandardIcon_Feed from './icons/standard/Feed'; import StandardIcon_Feedback from './icons/standard/Feedback'; import StandardIcon_Form from './icons/standard/Form'; +import StandardIcon_Formula from './icons/standard/Formula'; import StandardIcon_MultiPicklist from './icons/standard/MultiPicklist'; import StandardIcon_Opportunity from './icons/standard/Opportunity'; +import StandardIcon_Outcome from './icons/standard/Outcome'; import StandardIcon_Portal from './icons/standard/Portal'; import StandardIcon_ProductConsumed from './icons/standard/ProductConsumed'; import StandardIcon_Record from './icons/standard/Record'; +import StandardIcon_RecordCreate from './icons/standard/RecordCreate'; import StandardIcon_RecordLookup from './icons/standard/RecordLookup'; import StandardIcon_RelatedList from './icons/standard/RelatedList'; import StandardIcon_Settings from './icons/standard/Settings'; @@ -69,6 +75,7 @@ import UtilityIcon_Favorite from './icons/utility/Favorite'; import UtilityIcon_File from './icons/utility/File'; import UtilityIcon_Filter from './icons/utility/Filter'; import UtilityIcon_FilterList from './icons/utility/FilterList'; +import UtilityIcon_Formula from './icons/utility/Formula'; import UtilityIcon_Forward from './icons/utility/Forward'; import UtilityIcon_Help from './icons/utility/Help'; import UtilityIcon_HelpDocExt from './icons/utility/HelpDocExt'; @@ -148,21 +155,27 @@ export interface IconObj { } const standardIcons = { + actions_and_buttons: StandardIcon_ActionsAndButtons, activations: StandardIcon_Activations, apex: StandardIcon_Apex, asset_relationship: StandardIcon_AssetRelationship, bundle_config: StandardIcon_BundleConfig, data_streams: StandardIcon_DataStreams, entity: StandardIcon_Entity, + events: StandardIcon_Events, + feed: StandardIcon_Feed, feedback: StandardIcon_Feedback, form: StandardIcon_Form, + formula: StandardIcon_Formula, multi_picklist: StandardIcon_MultiPicklist, opportunity: StandardIcon_Opportunity, + outcome: StandardIcon_Outcome, portal: StandardIcon_Portal, product_consumed: StandardIcon_ProductConsumed, + record_create: StandardIcon_RecordCreate, + record_lookup: StandardIcon_RecordLookup, record: StandardIcon_Record, related_list: StandardIcon_RelatedList, - record_lookup: StandardIcon_RecordLookup, settings: StandardIcon_Settings, } as const; @@ -201,9 +214,9 @@ const utilityIcons = { close: UtilityIcon_Close, collapse_all: UtilityIcon_CollapseAll, component_customization: UtilityIcon_ComponentCustomization, + contract_alt: UtilityIcon_ContractAlt, copy_to_clipboard: UtilityIcon_CopyToClipboard, copy: UtilityIcon_Copy, - contract_alt: UtilityIcon_ContractAlt, dash: UtilityIcon_Dash, date_time: UtilityIcon_DateTime, delete: UtilityIcon_Delete, @@ -220,9 +233,10 @@ const utilityIcons = { file: UtilityIcon_File, filter: UtilityIcon_Filter, filterList: UtilityIcon_FilterList, + formula: UtilityIcon_Formula, forward: UtilityIcon_Forward, - help: UtilityIcon_Help, help_doc_ext: UtilityIcon_HelpDocExt, + help: UtilityIcon_Help, home: UtilityIcon_Home, image: UtilityIcon_Image, info: UtilityIcon_Info, @@ -231,14 +245,14 @@ const utilityIcons = { left: UtilityIcon_Left, link: UtilityIcon_Link, logout: UtilityIcon_Logout, - minimize_window: UtilityIcon_MinimizeWindow, merge_field: UtilityIcon_MergeField, + minimize_window: UtilityIcon_MinimizeWindow, moneybag: UtilityIcon_Moneybag, multi_select_checkbox: UtilityIcon_MultiSelectCheckbox, new_window: UtilityIcon_NewWindow, notification: UtilityIcon_Notification, - open: UtilityIcon_Open, open_folder: UtilityIcon_OpenFolder, + open: UtilityIcon_Open, page: UtilityIcon_Page, paste: UtilityIcon_Paste, play: UtilityIcon_Play, diff --git a/libs/shared/constants/src/lib/shared-constants.ts b/libs/shared/constants/src/lib/shared-constants.ts index 51c2eaa6d..c4df3637e 100644 --- a/libs/shared/constants/src/lib/shared-constants.ts +++ b/libs/shared/constants/src/lib/shared-constants.ts @@ -234,6 +234,7 @@ export const ANALYTICS_KEYS = { sobj_create_field_deploy: 'sobj_create_field_deploy', sobj_create_field_export_results: 'sobj_create_field_export_results', sobj_create_field_global_picklist: 'sobj_create_field_global_picklist', + sobj_create_field_formula_execute: 'sobj_create_field_formula_execute', /** FORMULA EVALUATOR */ formula_execute: 'formula_execute', formula_export_deploy: 'formula_export_deploy', diff --git a/libs/ui/src/lib/card/Card.tsx b/libs/ui/src/lib/card/Card.tsx index 21399dca0..378b7c9d0 100644 --- a/libs/ui/src/lib/card/Card.tsx +++ b/libs/ui/src/lib/card/Card.tsx @@ -43,7 +43,7 @@ export const Card = forwardRef( icon={icon.icon} title={icon.title} description={icon.description} - containerClassname={classNames('slds-icon_container', `slds-icon-${icon.type}-${icon.icon.replace('_', '-')}`)} + containerClassname={classNames('slds-icon_container', `slds-icon-${icon.type}-${icon.icon.replaceAll('_', '-')}`)} className="slds-icon slds-icon_small" />
diff --git a/libs/ui/src/lib/layout/page-header/PageHeaderTitle.tsx b/libs/ui/src/lib/layout/page-header/PageHeaderTitle.tsx index 23c6e5a7b..8ea337399 100644 --- a/libs/ui/src/lib/layout/page-header/PageHeaderTitle.tsx +++ b/libs/ui/src/lib/layout/page-header/PageHeaderTitle.tsx @@ -38,7 +38,7 @@ export const PageHeaderTitle: FunctionComponent = ({ type={icon.type} icon={icon.icon} description={icon.description} - className={`slds-icon slds-page-header__icon slds-icon-${icon.type}-${icon.icon?.replace('_', '-')}`} + className={`slds-icon slds-page-header__icon slds-icon-${icon.type}-${icon.icon?.replaceAll('_', '-')}`} />