Files
gh-secondsky-sap-skills-ski…/references/javascript-patterns.md
2025-11-30 08:55:30 +08:00

26 KiB

SAP Analytics Cloud - JavaScript Patterns for Planning

Sources:


Table of Contents

  1. Application Patterns
  2. Member Filtering Patterns
  3. Data Entry Patterns
  4. Version Management Patterns
  5. Data Action Patterns
  6. Navigation Patterns
  7. Error Handling Patterns
  8. Performance Patterns

Application Patterns

Application Initialization

// onInitialization event
Application.showBusyIndicator();

try {
    // Set default filters
    var currentYear = new Date().getFullYear().toString();
    Table_1.getDataSource().setDimensionFilter("Year", currentYear);

    // Get user info for personalization
    var userInfo = Application.getUserInfo();
    Text_Welcome.setText("Welcome, " + userInfo.displayName);

    // Load user preferences from app state
    loadUserPreferences();

} catch (e) {
    console.log("Initialization error: " + e.message);
} finally {
    Application.hideBusyIndicator();
}

Show/Hide Busy Indicator

// Wrap long operations
function performLongOperation() {
    Application.showBusyIndicator();

    // Do work...
    processData();

    Application.hideBusyIndicator();
}

// With error handling
function safeOperation() {
    Application.showBusyIndicator();
    try {
        riskyOperation();
    } catch (e) {
        Application.showMessage("Error: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}

Message Handling

// Simple toast message
Application.showMessage("Operation completed successfully");

// For critical messages, use custom dialog
function showErrorDialog(title, message) {
    // Assuming Dialog_Error is a popup widget
    Text_ErrorTitle.setText(title);
    Text_ErrorMessage.setText(message);
    Popup_Error.open();
}

Member Filtering Patterns

Find Active Member by Attribute

// Find version marked as "Active" in master data
function findActivePlanVersion() {
    var allVersions = PlanningModel_1.getMembers("Version");

    for (var i = 0; i < allVersions.length; i++) {
        if (allVersions[i].properties.Active === "X") {
            return allVersions[i].id;
        }
    }
    return null; // No active version found
}

// Usage
var activeVersion = findActivePlanVersion();
if (activeVersion) {
    Table_1.getDataSource().setDimensionFilter("Version",
        "[Version].[parentId].&[public." + activeVersion + "]");
}

Get Booked Values Only

// Performance optimization: only get members with data
var bookedAccounts = Table_1.getDataSource().getMembers("Account", {
    accessMode: MemberAccessMode.BookedValues
});

console.log("Found " + bookedAccounts.length + " accounts with data");

Filter by Property Value

// Get all cost centers for a specific region
function getCostCentersByRegion(region) {
    var allCCs = PlanningModel_1.getMembers("CostCenter", {
        limit: 10000
    });

    var regionCCs = [];
    for (var i = 0; i < allCCs.length; i++) {
        if (allCCs[i].properties.Region === region) {
            regionCCs.push(allCCs[i].id);
        }
    }
    return regionCCs;
}

// Apply as filter
var emeaCCs = getCostCentersByRegion("EMEA");
Table_1.getDataSource().setDimensionFilter("CostCenter", emeaCCs);

Dynamic Planning Cycle Filter

// Set filter based on active planning cycle from master data
function setActivePlanningCycleFilter() {
    Application.showBusyIndicator();
    Table_1.setVisible(false);

    var cycles = PlanningModel_1.getMembers("PlanningCycle");
    var activeCycle = "";

    for (var i = 0; i < cycles.length; i++) {
        if (cycles[i].properties.Flag === "PC+0") {
            activeCycle = cycles[i].id;
            break;
        }
    }

    if (activeCycle) {
        // MDX filter syntax
        Table_1.getDataSource().setDimensionFilter("Date",
            "[Date].[YQM].&[" + activeCycle + "]");
    }

    Table_1.setVisible(true);
    Application.hideBusyIndicator();
}

Populate Dropdown from Members

// Fill dropdown with dimension members
function populateDropdown(dropdown, dimensionId, filterProperty, filterValue) {
    dropdown.removeAllItems();

    var members = PlanningModel_1.getMembers(dimensionId, {limit: 1000});

    for (var i = 0; i < members.length; i++) {
        var member = members[i];

        // Optional: filter by property
        if (filterProperty && member.properties[filterProperty] !== filterValue) {
            continue;
        }

        dropdown.addItem(member.id, member.description || member.id);
    }
}

// Usage
populateDropdown(Dropdown_CostCenter, "CostCenter", "Status", "Active");

Data Entry Patterns

Check Planning Enabled

// Verify planning is enabled before operations
function verifyPlanningEnabled() {
    var planning = Table_1.getPlanning();

    if (!planning || !planning.isEnabled()) {
        Application.showMessage("Planning is not enabled for this table");
        return false;
    }
    return true;
}

Set Cell Value

// Set value for selected cell
function setSelectedCellValue(value) {
    if (!verifyPlanningEnabled()) return;

    var selections = Table_1.getSelections();
    if (selections.length === 0) {
        Application.showMessage("Please select a cell first");
        return;
    }

    var selection = selections[0];

    // Check data locking
    var dataLocking = Table_1.getPlanning().getDataLocking();
    var lockState = dataLocking.getState(selection);

    if (lockState === DataLockingState.Locked) {
        Application.showMessage("This cell is locked and cannot be edited");
        return;
    }

    // Set the value
    Table_1.getPlanning().setUserInput(selection, value.toString());
}

Apply Factor to Value

// Multiply existing value by factor
function applyFactor(factor) {
    if (!verifyPlanningEnabled()) return;

    var selection = Table_1.getSelections()[0];
    if (!selection) return;

    // Prefix with "*" to multiply
    Table_1.getPlanning().setUserInput(selection, "*" + factor.toString());
}

// Usage: increase by 10%
applyFactor(1.1);

// Usage: decrease by 50%
applyFactor(0.5);

Enable/Disable Planning Based on Business Rules

// Disable planning in Q4 (budget freeze)
function checkPlanningAvailability() {
    var currentMonth = new Date().getMonth() + 1;

    if (currentMonth >= 10 && currentMonth <= 12) {
        Table_Budget.getPlanning().setEnabled(false);
        Button_Save.setEnabled(false);
        Application.showMessage("Budget changes locked in Q4");
    } else {
        Table_Budget.getPlanning().setEnabled(true);
        Button_Save.setEnabled(true);
    }
}

Submit Data Changes

// Submit pending changes
function submitPlanningData() {
    if (!verifyPlanningEnabled()) return;

    Application.showBusyIndicator();

    try {
        Table_1.getPlanning().submitData();
        Application.showMessage("Data submitted successfully");
    } catch (e) {
        Application.showMessage("Submit failed: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}

Bulk Data Entry

// Set values for multiple cells
function setBulkValues(valueMap) {
    // valueMap: [{selection: {...}, value: "100"}, ...]

    Application.showBusyIndicator();

    var planning = Table_1.getPlanning();
    var successCount = 0;
    var errorCount = 0;

    for (var i = 0; i < valueMap.length; i++) {
        try {
            planning.setUserInput(valueMap[i].selection, valueMap[i].value);
            successCount++;
        } catch (e) {
            errorCount++;
            console.log("Error setting value: " + e.message);
        }
    }

    // Submit all changes at once
    planning.submitData();

    Application.showMessage("Set " + successCount + " values, " + errorCount + " errors");
    Application.hideBusyIndicator();
}

Version Management Patterns

Get All Versions

// List all available versions
function listVersions() {
    var planning = Table_1.getPlanning();

    console.log("=== Public Versions ===");
    var publicVersions = planning.getPublicVersions();
    for (var i = 0; i < publicVersions.length; i++) {
        console.log(publicVersions[i].id + ": " + publicVersions[i].description);
    }

    console.log("=== Private Versions ===");
    var privateVersions = planning.getPrivateVersions();
    for (var j = 0; j < privateVersions.length; j++) {
        console.log(privateVersions[j].id + ": " + privateVersions[j].description);
    }
}

Publish Version

// Publish private version or edit mode
function publishVersion() {
    var planning = Table_1.getPlanning();
    var privateVersion = planning.getPrivateVersion();

    if (!privateVersion) {
        Application.showMessage("No private version to publish");
        return;
    }

    if (!privateVersion.isDirty()) {
        Application.showMessage("No changes to publish");
        return;
    }

    Application.showBusyIndicator();

    try {
        privateVersion.publish();
        Application.showMessage("Version published successfully");
    } catch (e) {
        Application.showMessage("Publish failed: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}

Publish As New Version

// Create new public version from private
function publishAsNewVersion(newVersionId, newDescription) {
    var planning = Table_1.getPlanning();
    var privateVersion = planning.getPrivateVersion();

    if (!privateVersion) {
        Application.showMessage("No private version available");
        return;
    }

    Application.showBusyIndicator();

    try {
        privateVersion.publishAs(newVersionId, newDescription);
        Application.showMessage("New version '" + newVersionId + "' created");
    } catch (e) {
        Application.showMessage("Publish As failed: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}

// Usage
publishAsNewVersion("Budget_2025_v2", "Revised Budget 2025");

Revert Changes

// Discard all changes
function revertChanges() {
    var planning = Table_1.getPlanning();
    var privateVersion = planning.getPrivateVersion();

    if (!privateVersion) {
        Application.showMessage("No private version to revert");
        return;
    }

    // Confirm before reverting
    // (In real app, use custom confirmation dialog)
    privateVersion.revert();
    Application.showMessage("Changes reverted");
}

Check Dirty Status Before Publishing

// Use isDirty() to avoid unnecessary publish attempts
function publishIfDirty() {
    var version = Table_1.getPlanning().getPublicVersion("Forecast");

    if (!version.isDirty()) {
        Application.showMessage("No changes to publish");
        return;
    }

    Application.showBusyIndicator();
    var success = version.publish();
    Application.hideBusyIndicator();

    if (success) {
        Application.showMessage("Published successfully");
    } else {
        Application.showMessage("Publish failed");
    }
}

Copy Version with Options

// Create new version from existing
function copyVersionAsNewBudget(sourceId, targetId) {
    var planning = Table_1.getPlanning();
    var source = planning.getPublicVersion(sourceId);

    if (!source) {
        Application.showMessage("Source version not found");
        return;
    }

    Application.showBusyIndicator();

    var success = source.copy(
        targetId,
        PlanningCopyOptions.AllData,
        PlanningCategory.Budget
    );

    Application.hideBusyIndicator();

    if (success) {
        Application.showMessage("Budget " + targetId + " created");
        Application.refreshData();
    }
}

Version Selection UI

// Populate version dropdown and handle selection
function setupVersionSelector() {
    var planning = Table_1.getPlanning();
    var versions = planning.getPublicVersions();

    Dropdown_Version.removeAllItems();
    for (var i = 0; i < versions.length; i++) {
        Dropdown_Version.addItem(versions[i].id, versions[i].description);
    }
}

// onSelect event for Dropdown_Version
function onVersionSelected() {
    var selectedVersion = Dropdown_Version.getSelectedKey();

    Table_1.getDataSource().setDimensionFilter("Version",
        "[Version].[parentId].&[public." + selectedVersion + "]");
}

Data Action Patterns

Execute with Parameters

// Execute data action with parameters
function executeDataAction() {
    // Set parameters
    DataAction_Copy.setParameterValue("SourceVersion", "Actual");
    DataAction_Copy.setParameterValue("TargetVersion", Dropdown_TargetVersion.getSelectedKey());
    DataAction_Copy.setParameterValue("Year", InputField_Year.getValue());

    Application.showBusyIndicator();
    DataAction_Copy.execute();
}

// In onExecutionComplete event
function onDataActionComplete() {
    Application.hideBusyIndicator();
    Application.showMessage("Data action completed");
    Application.refreshData();
}

Execute in Background

// Non-blocking execution for long-running actions
function executeInBackground() {
    DataAction_LargeCalculation.setParameterValue("Scope", "ALL");
    DataAction_LargeCalculation.executeInBackground();

    Application.showMessage("Data action started in background");
}

// Monitor status in onExecutionStatusUpdate event
function onStatusUpdate(status) {
    // status can be: Running, Success, Failed, Cancelled
    console.log("Status: " + status);

    if (status === "Success" || status === "Failed") {
        Application.refreshData();
    }
}

Conditional Execution

// Execute different actions based on conditions
function executeConditionalAction() {
    var selectedAction = Dropdown_ActionType.getSelectedKey();
    var targetVersion = Dropdown_Version.getSelectedKey();

    switch (selectedAction) {
        case "COPY":
            DataAction_Copy.setParameterValue("TargetVersion", targetVersion);
            DataAction_Copy.execute();
            break;
        case "ALLOCATE":
            DataAction_Allocate.setParameterValue("TargetVersion", targetVersion);
            DataAction_Allocate.execute();
            break;
        case "CLEAR":
            DataAction_Clear.setParameterValue("TargetVersion", targetVersion);
            DataAction_Clear.execute();
            break;
        default:
            Application.showMessage("Unknown action type");
    }
}

Navigation Patterns

Open Another Story

// Navigate to detail story with context
function openDetailStory(entityId) {
    // Pass parameters via URL
    var storyId = "story_id_here";
    var params = "?p_entity=" + entityId;

    NavigationUtils.openStory(storyId + params);
}

Open External URL

// Open SAP documentation
function openDocumentation() {
    NavigationUtils.openUrl(
        "[https://help.sap.com/docs/SAP_ANALYTICS_CLOUD"](https://help.sap.com/docs/SAP_ANALYTICS_CLOUD")
    );
}

Refresh Data

// Refresh all data sources after external changes
function refreshAllData() {
    Application.showBusyIndicator();
    Application.refreshData();
    Application.hideBusyIndicator();
    Application.showMessage("Data refreshed");
}

Error Handling Patterns

Try-Catch Pattern

// Wrap risky operations
function safeOperation() {
    try {
        riskyFunction();
        Application.showMessage("Success");
    } catch (error) {
        console.log("Error: " + error.message);
        Application.showMessage("Operation failed: " + error.message);
    }
}

Validation Pattern

// Validate before operation
function validateAndExecute() {
    // Check required selections
    var selection = Table_1.getSelections();
    if (selection.length === 0) {
        Application.showMessage("Please select data first");
        return false;
    }

    // Check required inputs
    var year = InputField_Year.getValue();
    if (!year || year.length !== 4) {
        Application.showMessage("Please enter valid year (YYYY)");
        return false;
    }

    // Check planning enabled
    if (!Table_1.getPlanning().isEnabled()) {
        Application.showMessage("Planning not enabled");
        return false;
    }

    // All validations passed
    executeAction();
    return true;
}

Null Check Pattern

// Handle potentially null objects
function safeGetVersion() {
    var planning = Table_1.getPlanning();
    if (!planning) {
        console.log("Planning object not available");
        return null;
    }

    var privateVersion = planning.getPrivateVersion();
    if (!privateVersion) {
        console.log("No private version exists");
        return null;
    }

    return privateVersion;
}

Performance Patterns

Limit Member Retrieval

// Always set limit for getMembers
var members = PlanningModel_1.getMembers("CostCenter", {
    limit: 5000  // Don't load unlimited members
});

// Use offset for pagination
var page2 = PlanningModel_1.getMembers("CostCenter", {
    limit: 1000,
    offset: 1000
});

Cache Member Lists

// Store frequently used member lists
var memberCache = {};

function getCachedMembers(dimensionId) {
    if (!memberCache[dimensionId]) {
        memberCache[dimensionId] = PlanningModel_1.getMembers(dimensionId, {
            limit: 10000
        });
    }
    return memberCache[dimensionId];
}

// Clear cache when model changes
function clearMemberCache() {
    memberCache = {};
}

Batch UI Updates

// Hide table during batch updates
function batchUpdate() {
    Table_1.setVisible(false);
    Application.showBusyIndicator();

    // Perform multiple operations
    updateFilters();
    updateSorting();
    updateFormatting();

    Table_1.setVisible(true);
    Application.hideBusyIndicator();
}

Debounce Pattern

// Avoid rapid repeated calls (pseudo-code concept)
var debounceTimer = null;

function debouncedSearch(searchTerm) {
    // Clear previous timer
    if (debounceTimer) {
        // In SAC, you'd use a different approach
        // as clearTimeout isn't directly available
    }

    // Delay execution
    // Note: SAC doesn't have setTimeout, use onTimeout event instead
    performSearch(searchTerm);
}

Data Locking Patterns

Check Lock State Before Editing

// Verify cell can be edited before allowing user input
function canEditCell(selection) {
    var dataLocking = Table_1.getPlanning().getDataLocking();

    if (!dataLocking) {
        return true;  // Data locking not enabled, allow edit
    }

    var lockState = dataLocking.getState(selection);

    switch (lockState) {
        case DataLockingState.Open:
            return true;
        case DataLockingState.Restricted:
            Application.showMessage("Only data owner can edit this cell");
            return false;
        case DataLockingState.Locked:
            Application.showMessage("This data is locked and cannot be edited");
            return false;
        case DataLockingState.Mixed:
            Application.showMessage("Selection contains mixed lock states");
            return false;
        default:
            console.log("Unknown lock state");
            return false;
    }
}

Set Lock State on Public Version

// Lock data after approval
function lockApprovedData() {
    var selection = Table_1.getSelections()[0];
    var dataLocking = Table_1.getPlanning().getDataLocking();

    if (!dataLocking) {
        Application.showMessage("Data locking not enabled on this model");
        return;
    }

    // Note: Can only set lock state on public versions
    var success = dataLocking.setState(selection, DataLockingState.Locked);

    if (success) {
        Application.showMessage("Data locked successfully");
    } else {
        Application.showMessage("Failed to lock data - ensure you're on a public version");
    }
}

Members on the Fly Patterns

Create New Dimension Member

// Dynamically add new cost center
function createCostCenter(id, description, region) {
    Application.showBusyIndicator();

    try {
        PlanningModel_1.createMembers("CostCenter", {
            id: id,
            description: description,
            properties: {
                "CUSTOM_Region": region,
                "CUSTOM_Status": "Active"
            }
        });

        // IMPORTANT: Refresh to see new member
        Application.refreshData();

        Application.showMessage("Cost center created: " + id);
    } catch (e) {
        Application.showMessage("Error: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}

Update Member with Data Locking Owner

// Assign data locking ownership
function assignDataOwner(dimensionId, memberId, userId) {
    PlanningModel_1.updateMembers(dimensionId, {
        id: memberId,
        dataLockingOwners: [{
            id: userId,
            type: UserType.User
        }]
    });

    Application.refreshData();
    Application.showMessage("Data owner assigned");
}

Get Members with Pagination

// Load members in pages for large dimensions
function loadMembersInPages(dimensionId, pageSize) {
    var allMembers = [];
    var offset = 0;
    var hasMore = true;

    while (hasMore) {
        var page = PlanningModel_1.getMembers(dimensionId, {
            offset: offset.toString(),
            limit: pageSize.toString()
        });

        if (page.length > 0) {
            allMembers = allMembers.concat(page);
            offset += pageSize;
        } else {
            hasMore = false;
        }
    }

    return allMembers;
}

Utility Patterns

Type Conversion

// Convert string to integer
var planningCycle = "2025";
var cycleNumber = ConvertUtils.stringToInteger(planningCycle);
cycleNumber++;
var nextCycle = ConvertUtils.numberToString(cycleNumber); // "2026"

Date Formatting

// Format current date
var today = new Date();
var formatted = DateFormat.format(today, "yyyy-MM-dd");
console.log("Today: " + formatted);

Number Formatting

// Format currency value
var amount = 1234567.89;
var formatted = NumberFormat.format(amount, "#,##0.00");
console.log("Amount: " + formatted); // "1,234,567.89"

Array Operations

// Check if array contains value
var selectedItems = ["A", "B", "C"];
if (ArrayUtils.contains(selectedItems, "B")) {
    console.log("B is selected");
}

// Find index
var index = ArrayUtils.indexOf(selectedItems, "C"); // 2

Complete Example: Budget Entry Application

// === onInitialization Event ===
Application.showBusyIndicator();

// Set default version
var versions = Table_Budget.getPlanning().getPublicVersions();
for (var i = 0; i < versions.length; i++) {
    Dropdown_Version.addItem(versions[i].id, versions[i].description);
}

// Set current year
var currentYear = new Date().getFullYear().toString();
InputField_Year.setValue(currentYear);

// Load user's cost centers
var userInfo = Application.getUserInfo();
loadUserCostCenters(userInfo.userId);

Application.hideBusyIndicator();


// === Version Selection ===
function onVersionSelected() {
    var version = Dropdown_Version.getSelectedKey();
    Table_Budget.getDataSource().setDimensionFilter("Version",
        "[Version].[parentId].&[public." + version + "]");
}


// === Save Button ===
function onSaveClick() {
    if (!Table_Budget.getPlanning().isEnabled()) {
        Application.showMessage("Planning not enabled");
        return;
    }

    Application.showBusyIndicator();

    try {
        Table_Budget.getPlanning().submitData();
        Application.showMessage("Budget saved successfully");
    } catch (e) {
        Application.showMessage("Save failed: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}


// === Publish Button ===
function onPublishClick() {
    var privateVer = Table_Budget.getPlanning().getPrivateVersion();

    if (!privateVer || !privateVer.isDirty()) {
        Application.showMessage("No changes to publish");
        return;
    }

    Application.showBusyIndicator();

    try {
        privateVer.publish();
        Application.showMessage("Budget published successfully");
    } catch (e) {
        Application.showMessage("Publish failed: " + e.message);
    } finally {
        Application.hideBusyIndicator();
    }
}


// === Apply Growth Rate ===
function onApplyGrowthClick() {
    var growthRate = ConvertUtils.stringToNumber(InputField_GrowthRate.getValue());

    if (isNaN(growthRate)) {
        Application.showMessage("Invalid growth rate");
        return;
    }

    var factor = 1 + (growthRate / 100);

    var selections = Table_Budget.getSelections();
    if (selections.length === 0) {
        Application.showMessage("Select cells to apply growth");
        return;
    }

    Application.showBusyIndicator();

    for (var i = 0; i < selections.length; i++) {
        Table_Budget.getPlanning().setUserInput(selections[i], "*" + factor);
    }

    Table_Budget.getPlanning().submitData();
    Application.showMessage("Growth rate applied");
    Application.hideBusyIndicator();
}

Documentation Links: