cmEngage Hub

Class Level Documentation

LWC Components


/**
 * @class       ActivityPanel
 * @description Root orchestrator LWC for the Activity Panel product. This is the only
 *              component exposed to the Lightning App Builder and the sole entry point
 *              placed on record pages.
 *
 *              On load, it performs two async operations in parallel:
 *                - loadMetadata()  — imperative Apex calls to fetch button config
 *                                    (Activity_Buttons_Configuration) and default filter
 *                                    settings (Default_Activity_Filters) from Custom Metadata.
 *                - getSchema()     — imperative Apex call to resolve the schema model
 *                                    (Default_Schema_Model or object/record-type-specific
 *                                    override) and store it in this.schemaToUse.
 *
 *              Six wire adapters fire reactively in parallel:
 *                - getWhatObjects          → resolves API names for Related To lookup objects.
 *                - getObjects('who')        → reflects Task.WhoId reference types.
 *                - getObjects('assignedTo') → reflects Task.OwnerId reference types.
 *                - getObjectInfos (x3)      → resolves SLDS icons, colors, and labels for
 *                                             each set of lookup objects, building actionConfig.
 *                - getUserAndCurrentRecord  → resolves logged-in user, timezone, current
 *                                             record's object API name, record type developer
 *                                             name, and pre-fill values for Who and What.
 *                - getPicklistValue(Task.Status) → loads Task Status picklist into actionConfig
 *                                                  for use by the ReOpen modal.
 *
 *              Button routing in handleClick():
 *                - Buttons with a screenFlowName launch a lightning-flow component inside
 *                  a desktop SLDS modal or a full-screen mobile panel.
 *                - The reserved names (log_a_call, task, event, note) open the built-in
 *                  c-activity-action-modal with pre-populated activity data.
 *
 *              Button visibility in _filterByVisibility():
 *                - Each button may carry a hiddenFor array of { objectName, recordTypes[] }.
 *                - A button is hidden when the current object matches and either recordTypes
 *                  is empty (hidden for all types) or the current record type is in the list.
 *
 *              Filter state is maintained as two parallel sets:
 *                - selected*  — the currently applied values that drive the timeline.
 *                - pending*   — staging values held while the filter modal is open.
 *              On Apply, pending is validated and committed to selected, and
 *              filterConfig is rebuilt and pushed down to c-activity-record-logs.
 *
 *              Real-time refresh is achieved by subscribing to the
 *              /event/Activity_UI_Event__e channel via the EMP API.
 *              Events are filtered by Parent_Record_Id__c to ignore events from other
 *              record pages open in other tabs. Supported event types are
 *              'Close And Refresh' (closes any open flow, then refreshes after 2 s)
 *              and 'Refresh' (immediate timeline reload).
 *
 *              Responsive breakpoint is 600 px (MOBILE_BREAKPOINT constant).
 *              Button labels are suppressed below 520 px component width, measured
 *              via getBoundingClientRect in renderedCallback.
 *
 * @dependencies
 *   Apex  : ActivityPanelController.getMetadataValue
 *           ActivityPanelController.getObjects
 *           ActivityPanelController.getWhatObjects
 *           ActivityPanelController.getUserAndCurrentRecord
 *           ActivityRecordLogsController.getSchema
 *           ActivityUtils.getPicklistValue
 *   LDS   : lightning/uiObjectInfoApi.getObjectInfos
 *   EMP   : lightning/empApi  (subscribe, unsubscribe, onError)
 *   Child : c-activity-record-logs
 *           c-activity-action-modal
 *           lightning-flow
 *
 * @usage
 *   <!-- Placed on a Record Page via Lightning App Builder -->
 *   <c-activity-panel record-id={recordId}></c-activity-panel>
 */


/**
 * @class       ActivityTimeline   (activityRecordLogs)
 * @description Child timeline-renderer LWC. Responsible for fetching, deduplicating,
 *              grouping, sorting, filtering, and rendering all activity records for
 *              a given record context. Exposes a public @api surface so the parent
 *              activityPanel can trigger operations imperatively.
 *
 *              Data loading (loadActivityTimeline):
 *                1. Calls getActivityRecordConfigs() to obtain all configured
 *                   ActivityRecordConfiguration entries from Custom Metadata.
 *                2. Calls getSObjectTypeById() to resolve the host object API name.
 *                3. Fires two Promise.all batches in parallel:
 *                     - getCurrentAndParentActivities() per config   (direct + parent roll-up)
 *                     - fetchChildActivities()          per config   (child roll-up)
 *                4. Global Id-based deduplication is applied separately to the
 *                   upcomingAndOverdue and pastActivities buckets using Set<id> before
 *                   any processing. This prevents the same activity from appearing
 *                   twice when multiple configs or child traversals return overlapping records.
 *
 *              Processing (processActivityTimeline):
 *                - Upcoming & Overdue: sorted by sortingDate DESC (null last).
 *                - Past Activities: grouped into month sections by M/YYYY key,
 *                  each section sorted by sortingDate DESC, sections ordered by
 *                  month DESC. Each section carries a human-readable relativeLabel
 *                  (This Month, Last Month, N Months Ago, N Years Ago).
 *                - menuDisabled is set to true for any non-Task/Event/ContentNote
 *                  sObject so that read-only custom records cannot be edited or deleted.
 *
 *              Filtering (applyFilters — exposed as @api):
 *                Filters are applied in a fixed pipeline order on each call:
 *                  1. filterByDateRange   — date boundaries converted to GMT after
 *                                           being calculated in the user's timezone.
 *                  2. filterByAssignedTo  — 'My activities' compares assignedTo.recordId
 *                                           against loggedInUser.recordId.
 *                  3. filterByActivityTypes — Task records are matched by subtype
 *                                             ('Task' or 'Call'), not by sObjectName alone.
 *                  4. Sort order          — 'Oldest dates first' reverses the default DESC.
 *                  5. updateVisibleList   — applies the compact-view thresholds:
 *                                           open activities: show last 2 (or first 2 if oldest-first).
 *                                           past activities: show first 4 across sections.
 *
 *              Parent-exposed @api methods:
 *                - refreshActivityTimeline()       — full Apex reload + re-process.
 *                - applyFilters()                  — re-filters existing data without Apex.
 *                - expandActivityTimeline(expand)  — toggles isOpen/showMoreIcon on all rows.
 *                - toggleViewAllActivities(showAll) — switches between compact and full view,
 *                                                     using slice(-2) for open and
 *                                                     getFirstNActivities(4) for past.
 *
 *              filterConfig setter auto-triggers applyFilters() on every assignment,
 *              so the parent only needs to set the property; filtering is implicit.
 *
 *              Navigation uses NavigationMixin.Navigate to standard__recordPage
 *              for both subject links and related record links in summaries.
 *
 * @dependencies
 *   Apex  : ActivityRecordLogsController.getActivityRecordConfigs
 *           ActivityRecordLogsController.getCurrentAndParentActivities
 *           ActivityRecordLogsController.getChildActivities
 *           ActivityRecordLogsController.saveTask  (task completion checkbox)
 *           ActivityRecordLogsController.getTaskDetails
 *           ActivityRecordLogsController.getEventDetails
 *           ActivityRecordLogsController.getNoteDetails
 *           ActivityUtils.getSObjectTypeById
 *   Child : c-activity-action-modal
 *
 * @usage
 *   <!-- Instantiated by activityPanel; not placed directly on pages -->
 *   <c-activity-record-logs
 *       record-id={recordId}
 *       action-config={actionConfig}
 *       filter-config={filterConfig}
 *       logged-in-user={user}
 *       user-time-zone={timeZone}
 *       selected-activity-types-string={selectedActivityTypesString}
 *       schema-to-use={schemaToUse}>
 *   </c-activity-record-logs>
 */


/**
 * @class       ActionModal   (activityActionModal)
 * @description Schema-driven form LWC for creating, editing, and deleting activity
 *              records. The complete field layout — including field types, labels,
 *              sizes, picklist sources, required flags, and dependent field relationships
 *              — is driven at runtime by the schemaToUse object keyed on the action name.
 *              No field is hardcoded in the template.
 *
 *              Supported actions and target sObjects:
 *                'Log Call'              → Task  (subtype = Call,  insert)
 *                'New Task'              → Task  (subtype = Task,  insert)
 *                'Edit Task'             → Task  (update)
 *                'Edit Comments'         → Task  (Description only, update)
 *                'Change Date'           → Task  (ActivityDate only, update)
 *                'Change Status'         → Task  (Status only, update)
 *                'Change Priority'       → Task  (Priority only, update)
 *                'Create Follow-Up Task' → Task  (insert, pre-filled from source activity)
 *                'ReOpen'                → Task  (Status update; Completed is filtered out)
 *                'New Event'             → Event (insert)
 *                'Edit Event'            → Event (update)
 *                'Create Follow-Up Event'→ Event (insert, pre-filled from source activity)
 *                'New Note'              → ContentNote (insert + ContentDocumentLink)
 *                'Edit Note'             → ContentNote (update)
 *                'Delete'                → Any   (Database.delete by recordId)
 *
 *              Picklist loading (loadPicklistOptions):
 *                Runs once on connectedCallback. Iterates all isCombobox and
 *                isCustomCombobox fields in the active schema. For fields with a
 *                controllingField, calls getDependentPicklistValues() and stores the
 *                result as Map<controllingValue, List<{label,value}>> in optionsMap.
 *                For independent fields, calls getPicklistValue() and stores
 *                List<{label,value}>.  isOptionsLoaded is set to true only after all
 *                fields have resolved, preventing partial renders.
 *
 *              computedFields getter:
 *                Merges schema field definitions with live formData values on every
 *                render cycle. Handles startDate/endDate type switching based on
 *                allDayEvent (date vs datetime). Resolves objects arrays for lookup
 *                fields from actionConfig. Auto-disables a dependent combobox when
 *                no options are available for the current controlling value.
 *
 *              Date adjustment:
 *                Whenever startDate or endDate changes, adjustDateTimes() enforces
 *                that end > start. For all-day events it equalises the changed date
 *                to the other; for timed events it offsets the unchanged end by +1 h
 *                or the unchanged start by -1 h.
 *
 *              Dependent field clearing:
 *                clearDependentFields() resets any field whose controllingField
 *                matches the field that just changed, preventing stale dependent values.
 *
 *              Mobile rendering:
 *                useModalView = true when NOT mobile OR when action is 'Delete'.
 *                Delete always uses the modal on mobile to avoid a disruptive full-screen
 *                confirmation. All other actions on mobile use a fixed full-screen panel
 *                with a sticky header and footer.
 *
 *              On save success, dispatches a 'closemodal' CustomEvent with
 *              { detail: { refresh: true } }. On cancel, dispatches with
 *              { detail: { refresh: false } }.
 *
 * @dependencies
 *   Apex  : ActivityRecordLogsController.saveTask
 *           ActivityRecordLogsController.saveEvent
 *           ActivityRecordLogsController.saveNote
 *           ActivityRecordLogsController.deleteActivity
 *           ActivityUtils.getPicklistValue
 *           ActivityUtils.getDependentPicklistValues
 *   Child : c-record-lookup
 *
 * @usage
 *   <!-- Instantiated by activityPanel and activityRecordLogs; not placed directly -->
 *   <c-activity-action-modal
 *       action={action}
 *       activity={activity}
 *       action-config={actionConfig}
 *       current-record-id={recordId}
 *       schema-to-use={schemaToUse}
 *       onclosemodal={handleCloseModal}>
 *   </c-activity-action-modal>
 */


/**
 * @class       RecordLookup
 * @description Reusable custom lookup field LWC that supports multi-object search
 *              with a debounced live search, entity-type switcher, selected-record
 *              pill, and standard LWC validity API (reportValidity / checkValidity).
 *
 *              Object switching:
 *                The objects @api property accepts an array of view-model objects
 *                (apiName, label, iconUrl, color) built by activityPanel from
 *                getObjectInfos wire data. A dropdown icon next to the search input
 *                allows the user to switch the active search object. On set, the
 *                setter calls initSelectionFromProps() to either restore a pre-selected
 *                value from fieldValue or default to the object matching defaultEntity.
 *
 *              Search behaviour:
 *                - Input is ignored until searchTerm.length >= 2 to avoid
 *                  unnecessary Apex calls on single-character entry.
 *                - A 250 ms debounce prevents a query on every keystroke.
 *                - getSearchedRecords() performs a SOQL LIKE query on the
 *                  server, respecting Record_Lookup_Configuration display field
 *                  overrides (e.g. CaseNumber instead of Name).
 *                - Results are capped at 20 records server-side.
 *
 *              Selection and clearing:
 *                - Selecting a record shows a pill and dispatches a 'change' event
 *                  with { recordId, name, sObjectType } in event.detail.value.
 *                - Clearing the pill dispatches 'change' with all null values,
 *                  signalling the parent to unset the field.
 *
 *              Validity:
 *                - reportValidity() sets this.showError = true and returns false
 *                  when required = true and no record is selected. This mirrors
 *                  the lightning-input validity contract so activityActionModal
 *                  can query this component alongside native inputs.
 *                - checkValidity() performs the same check with no side effects.
 *
 *              NavigationMixin is imported but not actively used for navigation;
 *              it is included as a foundation for potential future use.
 *
 * @dependencies
 *   Apex  : ActivityRecordLogsController.getSearchedRecords
 *
 * @usage
 *   <!-- Instantiated by activityActionModal for who, what, assignedTo fields -->
 *   <c-record-lookup
 *       objects={field.objects}
 *       default-entity={field.defaultEntity}
 *       field-label={field.label}
 *       field-value={field.value}
 *       data-name={field.name}
 *       required={field.required}
 *       onchange={handleDetailChange}>
 *   </c-record-lookup>
 */


Apex Classes


/**
 * @class       ActivityPanelController
 * @description AuraEnabled controller that serves the activityPanel LWC.
 *              Handles three responsibilities: Custom Metadata retrieval,
 *              lookup-object API name resolution, and current record context resolution.
 *
 *              getMetadataValue(devName):
 *                Reads a single named entry from the shared mapActivityConfigurations
 *                map (populated by ActivityUtils). Returns a JSON string shaped as
 *                { isSuccess: true, <devName>: "<raw JSON value>" }  on success,
 *                or { isSuccess: false, errorMessage: "..." } on failure or missing key.
 *                Not cacheable because Custom Metadata values may be updated by admins.
 *
 *              getObjects(fieldName):
 *                Uses Schema reflection to read the referenceTo types of Task.WhoId
 *                ('who') or Task.OwnerId ('assignedTo'). Returns the list to drive
 *                the lightning/uiObjectInfoApi wire adapter in the LWC. Cacheable.
 *
 *              getWhatObjects(devName):
 *                Reads the Related_To_Objects comma-delimited string from the
 *                Activity_Record_Configuration metadata entry and splits it into a
 *                List<String>. Cacheable.
 *
 *              getUserAndCurrentRecord(recordId):
 *                The most complex method. Resolves five values in a single call to
 *                minimise round-trips:
 *                  1. loggedInUser — built from UserInfo.
 *                  2. userTimeZone — IANA timezone ID from UserInfo.
 *                  3. objectApiName — derived from the record Id prefix.
 *                  4. recordTypeDeveloperName — queried from the record itself.
 *                  5. who / what  — pre-fill references for new activity forms.
 *
 *                Who/What resolution follows this precedence:
 *                  - If Record_Lookup_Configuration defines a whoIdField for the
 *                    object, that field's value on the record is used as Who.
 *                  - If the field is blank but the object itself is in the whoSet
 *                    (Contact, Lead), the record itself becomes Who.
 *                  - If no whoIdField is configured but the object is in whoSet,
 *                    the record becomes Who directly.
 *                  - Same pattern applies to What, using whatIdField and whatSet
 *                    (Related_To_Objects list).
 *
 *                deriveDisplayPath(idField) is a private helper that converts a
 *                lookup field API name to its standard Name traversal path
 *                (e.g. 'AccountId' → 'Account.Name', custom '__c' → '__r.Name').
 *
 * @sharing     with sharing — respects record-level security.
 *
 * @usage
 *   // Called via @wire in activityPanel.js
 *   @wire(getUserAndCurrentRecord, { recordId: '$recordId' })
 *   wiredUserAndCurrentRecord({ data, error }) { ... }
 */


/**
 * @class       ActivityRecordLogsController
 * @description The core data layer for the Activity Panel product. Serves the
 *              activityRecordLogs and activityActionModal LWCs. Handles five concerns:
 *              schema resolution, activity fetching (direct + parent + child roll-up),
 *              record search, CRUD operations, and type-safe field conversion.
 *
 *              getSchema(recordId):
 *                Resolves which schema model JSON to use for the current record context.
 *                Resolution order:
 *                  1. If the object has no active (non-master) record types, query
 *                     Activity_Panel_Configuration__mdt by objectName alone.
 *                  2. If the object has active record types, build a lookup set with
 *                     three keys: objectName::RecordTypeName, objectName::DeveloperName,
 *                     and objectName. Query for any matching metadata record.
 *                  3. If no specific entry found, fall back to the
 *                     'Default Schema Model' metadata record.
 *                Returns JSON: { isSuccess: true, sMessage: "<schema JSON>" }.
 *
 *              getActivityRecordConfigs():
 *                Deserializes the Activity_Record_Configuration array from Custom Metadata
 *                into List<ActivityRecordConfiguration> inner class instances.
 *
 *              getCurrentAndParentActivities(recordId, configJson):
 *                Dispatches to getTasks(), getEvents(), getNotes(), or getCustomRecords()
 *                based on config.sObjectName. Each method:
 *                  - Queries the host record with any configured parentFields.
 *                  - Classifies the record into setWhoId or setWhatId based on its type.
 *                  - Follows each parentField value and classifies those Ids as well.
 *                  - Builds a WHERE clause covering both WhoId IN and WhatId IN sets.
 *                  - For ContentNote, resolves ContentDocumentLink records first, then
 *                    queries ContentNote WHERE Id IN docIds.
 *                  - Task records with status 'Completed' or non-null CompletedDateTime
 *                    go to pastActivities; all others go to upcomingAndOverdue.
 *                  - Event records whose startDate is in the past go to pastActivities.
 *
 *              getChildActivities(recordId, configJson, childObjectName, relationField, whereClause):
 *                Queries the child object using the relationField to collect related Ids,
 *                then builds the same Who/What set as getCurrentAndParentActivities and
 *                fetches activities for those Ids.
 *
 *              saveTask / saveEvent / saveNote:
 *                Accept a JSON string, deserialize to a Map<String,Object>, and
 *                use Schema.DescribeFieldResult + convertValue() to coerce each
 *                value to the correct Apex type before putting it on the SObject.
 *                This avoids hardcoded field mappings: any field in the schema JSON
 *                is automatically handled as long as it exists on the object.
 *                Upsert logic: if the map contains 'Id', the record is updated;
 *                otherwise it is inserted. For ContentNote, an insert also creates
 *                a ContentDocumentLink to the relatedToId with ShareType 'V'.
 *
 *              convertValue(value, fieldDescribe):
 *                Type-switch on Schema.DisplayType. Handles Date, DateTime, Boolean,
 *                Double/Currency/Percent, Integer, BASE64. Falls through to string for
 *                all other types including Picklist, Lookup, and TextArea.
 *
 *              getSearchedRecords(sObjectName, searchTerm):
 *                Builds a dynamic SOQL LIKE query. Consults Record_Lookup_Configuration
 *                to resolve the correct display field for each object (e.g. CaseNumber
 *                instead of Name). Returns List<Map<String,String>> with Id and Name.
 *
 *              deleteActivity(recordId):
 *                Calls Database.delete. Throws AuraHandledException on failure so
 *                the LWC can display a user-facing error message.
 *
 *              Inner classes (defined at end of file):
 *                ActivityRecordConfiguration, ObjectConfig, ChildConfig, TemplateItem.
 *
 * @sharing     with sharing — respects record-level security.
 *
 * @usage
 *   // Imperative call from activityRecordLogs.js
 *   const configs = await getActivityRecordConfigs();
 *   const result  = await getCurrentAndParentActivities({
 *       recordId: this.recordId,
 *       configJson: JSON.stringify(config)
 *   });
 */


/**
 * @class       ActivityWrapper
 * @description Global data transfer object (DTO) container for the Activity Panel
 *              product. Holds no business logic — all fields are @AuraEnabled so
 *              they are automatically serialized to JSON when returned from
 *              AuraEnabled methods and deserialized into JavaScript objects in LWCs.
 *
 *              Top-level class fields (returned by getUserAndCurrentRecord):
 *                loggedInUser            — RelatedRecord for the running user.
 *                who                     — Pre-fill value for the Name (Who) field.
 *                what                    — Pre-fill value for the Related To (What) field.
 *                userTimeZone            — IANA timezone string (e.g. 'America/Los_Angeles').
 *                objectApiName           — API name of the current page's host object.
 *                recordTypeDeveloperName — Developer name of the record's record type.
 *
 *              Inner classes:
 *
 *                RelatedRecord
 *                  Lightweight reference to any Salesforce record.
 *                  Used for who, what, assignedTo, and loggedInUser across the product.
 *                  Fields: recordId, name, sObjectType.
 *
 *                ActivityCollection
 *                  Return type for all activity-fetch methods in
 *                  ActivityRecordLogsController. Initialises both lists in its
 *                  constructor so callers never need to null-check.
 *                  Fields: upcomingAndOverdue List<ActivityRecord>,
 *                          pastActivities     List<ActivityRecord>.
 *
 *                ActivityRecord
 *                  Full UI-ready representation of a single activity row.
 *                  Carries all fields needed by the timeline template including
 *                  computed display properties (icon, bgColor, iconColor, dateLabel,
 *                  dateClass, showMoreIcon, isOpen, menuButtons) alongside the raw
 *                  record data (subject, status, priority, dates, description).
 *                  Event-specific fields (location, startDate, endDate, allDayEvent)
 *                  are present on all instances but populated only for Event records.
 *
 *                ActivityDetail
 *                  Represents a single token in a summaryTemplate or detailTemplate
 *                  row. The label field is populated only for detail rows. The
 *                  recordId field, when present, causes the LWC to render the value
 *                  as a clickable Salesforce navigation link. The href field, when
 *                  present, renders an external anchor (mailto:, tel:). The scrollable
 *                  flag causes the LWC to wrap the value in a max-height container.
 *
 *                ActivitySection
 *                  Groups ActivityRecord instances into a collapsible section.
 *                  Used by the LWC to render month groups in the past-activities
 *                  list. Carries sectionLabel (e.g. 'May 2026'), relativeLabel
 *                  (e.g. 'This Month'), isOpen, and icon (chevron indicator).
 *
 *                ComboBoxOption
 *                  Simple label/value pair returned by ActivityUtils picklist methods.
 *                  Serializes to the { label, value } shape expected by
 *                  lightning-combobox and lightning-radio-group.
 *
 * @usage
 *   // Returned from Apex, automatically deserialized in LWC
 *   @wire(getUserAndCurrentRecord, { recordId: '$recordId' })
 *   wiredUserAndCurrentRecord({ data, error }) {
 *       if (data) {
 *           this.user     = data.loggedInUser;   // ActivityWrapper.RelatedRecord
 *           this.who      = data.who;
 *           this.timeZone = data.userTimeZone;
 *       }
 *   }
 */


/**
 * @class       ActivityUtils
 * @description Shared utility library for the Activity Panel product. Contains
 *              no AuraEnabled query methods of its own except getPicklistValue,
 *              getDependentPicklistValues, and getSObjectTypeById. All other methods
 *              are called internally by ActivityRecordLogsController and
 *              ActivityPanelController to avoid code duplication.
 *
 *              mapActivityConfigurations (static lazy-loaded property):
 *                The single point of truth for all Custom Metadata values across
 *                the product. Loaded once per transaction from
 *                Activity_Panel_Configuration__mdt and cached as Map<DeveloperName, Value__c>.
 *                Both ActivityPanelController and ActivityRecordLogsController
 *                reference this map via a private static final field, so all
 *                metadata reads within the same transaction share the same map instance.
 *
 *              createActivityFromTask(task):
 *                Builds a fully populated ActivityWrapper.ActivityRecord from a Task
 *                SObject. Sets icon to 'standard:task' or 'standard:log_a_call' based
 *                on TaskSubtype. Sets bgColor to #4abf75 (Task) or #48c3cc (Call).
 *                Constructs the default menuButtons list including all task-specific
 *                context menu actions.
 *
 *              createActivityFromEvent(event):
 *                Builds an ActivityWrapper.ActivityRecord from an Event SObject.
 *                Sets icon to 'standard:event', bgColor to #cb65ff.
 *                Menu buttons are limited to Edit and Delete.
 *
 *              buildTaskEventSummary(activity, isPast, recordId):
 *                Builds the inline summary sentence tokens (List<ActivityDetail>)
 *                for Task and Event records. Uses ActivityWrapper.RelatedRecord
 *                references already populated on the ActivityRecord. Shows 'You'
 *                for the running user, and omits Who and What links when their
 *                recordId matches the current page's recordId (avoids self-referential
 *                links on the record being viewed).
 *
 *              buildDetails(oRec, lstTemplateItem):
 *                Processes a List<TemplateItem> from Custom Metadata config into
 *                a List<ActivityDetail> for either summaryTemplate or detailTemplate.
 *                Delegates to processTemplateItem() which handles all six types:
 *                  text, record, owner, conditionalText, conditionalGroup, link.
 *                conditionalGroup is recursive — its nested items are each passed
 *                back through processTemplateItem().
 *
 *              buildSubject(oRec, subjectTemplate):
 *                Resolves the activity subject string from a TemplateItem.
 *                Supports 'text' (field read or static) and 'conditionalText'
 *                (map lookup by dependsOn field value). Returns '[No Subject]'
 *                as a safe fallback.
 *
 *              getFieldValueAsString(record, fieldPath):
 *                Traverses dot-notation paths on SObjects by splitting on '.'
 *                and calling getSObject() for each intermediate relationship
 *                before calling get() on the terminal field. Returns null safely
 *                if any intermediate relationship is null.
 *
 *              formatDateTime(sortingDate, showTime, defaultValue, isAllDayEvent):
 *                Produces human-readable date labels: 'Today', 'Tomorrow',
 *                'Yesterday', or 'MMM d' format for all other dates.
 *                When showTime = true, prepends the time in 'hh:mm a' format
 *                or 'All-Day Event' for all-day events, separated by ' | '.
 *
 *              getDependentPicklistValues(sObjectName, controllingFieldName, dependentFieldName):
 *                Decodes the Salesforce picklist validFor base64 bitmask to map
 *                each dependent value to its controlling parent values.
 *                Returns Map<controllingValue, List<ComboBoxOption>> as JSON.
 *
 * @usage
 *   // Access the shared metadata map from any controller
 *   private static final Map<String,String> configs = ActivityUtils.mapActivityConfigurations;
 *
 *   // Build an activity record from a queried Task
 *   ActivityWrapper.ActivityRecord activity = ActivityUtils.createActivityFromTask(oTask);
 *
 *   // Resolve a dot-notation field path
 *   String ownerName = ActivityUtils.getFieldValueAsString(record, 'Owner.Name');
 */


/**
 * @class       InvocableCreateActivityUIEvent
 * @description Global invocable Apex class that allows Salesforce Flows to trigger
 *              real-time UI updates on the Activity Panel component by publishing
 *              Activity_UI_Event__e platform events via EventBus.publish().
 *
 *              This class is the integration bridge between Flow automation and the
 *              EMP API subscription in activityPanel.js. Without it, a Flow that
 *              creates or updates an activity would have no way to notify the
 *              already-rendered Activity Panel to refresh its timeline.
 *
 *              Input wrapper (ActivityEventWrapper):
 *                parentRecordId  (required) — 18-character Id of the record page
 *                                             hosting the Activity Panel. The LWC
 *                                             uses this to filter out events from
 *                                             other record pages in other browser tabs.
 *                eventType       (required) — Controls the action taken by the LWC:
 *                                             'Close And Refresh' — closes any open
 *                                             Screen Flow panel on the Activity Panel,
 *                                             then refreshes the timeline after 2 seconds.
 *                                             'Refresh' — immediately triggers a full
 *                                             timeline reload without closing any UI.
 *
 *              The method accepts a List<ActivityEventWrapper> to comply with the
 *              Invocable framework's bulk input requirement. All wrappers in the
 *              list are published in a single EventBus.publish() call to avoid
 *              unnecessary DML statements.
 *
 *              Exceptions are caught and written to debug logs rather than re-thrown
 *              so that a publishing failure does not interrupt the calling Flow.
 *
 * @sharing     with sharing — no record queries are performed; sharing has no effect.
 *
 * @usage
 *   // From Flow Builder — add an Action element, search for:
 *   // "Create Activity UI Event"
 *   //
 *   // Input variables:
 *   //   parentRecordId = {!recordId}          (the current record page Id)
 *   //   eventType      = "Close And Refresh"  (or "Refresh")
 *
 *   // From Apex (e.g. test or batch):
 *   InvocableCreateActivityUIEvent.ActivityEventWrapper w =
 *       new InvocableCreateActivityUIEvent.ActivityEventWrapper();
 *   w.parentRecordId = recordId;
 *   w.eventType      = 'Refresh';
 *   InvocableCreateActivityUIEvent.createActivityUIEvent(
 *       new List<InvocableCreateActivityUIEvent.ActivityEventWrapper>{ w }
 *   );
 */