Repository: splunk/addonfactory-ucc-generator (language: python)

Issue:
Feature request: Custom Mapping default value
### Description

A default value for custom mapping could be great, especially for boolean custom mapping (that could be undefined)

I can propose something like : 
"mapping": {
    "1": "Checked",
    "true": "Checked",
    "0": "Unchecked",
    "false": "Unchecked",
    "default": "Not defined"
}


Discussion / hints:
Thanks for raising it, seems like a nice feature to implement. We will take care of that and release in 2 weeks.

Fix patch:
diff --git a/docs/advanced/custom_mapping.md b/docs/advanced/custom_mapping.md
index 55413c340..746b139b7 100644
--- a/docs/advanced/custom_mapping.md
+++ b/docs/advanced/custom_mapping.md
@@ -1,5 +1,7 @@
 We can use this feature to map each field with meaningful value to display in the table. For example, the category field contains 1, 2, and 4 values, but when those values are displayed, the user might get confused as those values do not signify the meaning of their mapping. To avoid this confusion, the user can map each field with meaningful value as shown in the following example:
 
+If you have fields that are not mandatory but you would like to display them inside table, you can use default value option by providing ```"[[default]]"``` as one of parameters (check example bellow). It is a way to provide some meaningful information for form fields that have not been filled (fill empty cells in table).
+
 ### Usage
 
 ```json
@@ -22,7 +24,8 @@ We can use this feature to map each field with meaningful value to display in th
                 "mapping": {
                     "1": "Global",
                     "2": "US Gov",
-                    "4": "China"
+                    "4": "China",
+                    "[[default]]": "Unknown"
                 }
             }
         ],
@@ -47,7 +50,7 @@ We can use this feature to map each field with meaningful value to display in th
             "field": "category",
             "label": "Region Category",
             "type": "singleSelect",
-            "required": true,
+            "required": false,
             "defaultValue": 1,
             "options": {
                 "disableSearch": true,
diff --git a/ui/src/components/table/CustomTableRow.jsx b/ui/src/components/table/CustomTableRow.jsx
index 5b42da5e9..20f45300a 100644
--- a/ui/src/components/table/CustomTableRow.jsx
+++ b/ui/src/components/table/CustomTableRow.jsx
@@ -15,6 +15,7 @@ import { _ } from '@splunk/ui-utils/i18n';
 
 import CustomTableControl from './CustomTableControl';
 import { ActionButtonComponent } from './CustomTableStyle';
+import { getTableCellValue } from './table.utils';
 
 const TableCellWrapper = styled(Table.Cell)`
     padding: 2px;
@@ -171,13 +172,7 @@ function CustomTableRow(props) {
                                 data-column={header.field}
                                 key={header.field}
                             >
-                                {headerMapping[header.field] &&
-                                Object.prototype.hasOwnProperty.call(
-                                    headerMapping[header.field],
-                                    row[header.field]
-                                )
-                                    ? headerMapping[header.field][row[header.field]]
-                                    : row[header.field]}
+                                {getTableCellValue(row, header.field, headerMapping[header.field])}
                             </Table.Cell>
                         );
                     }
diff --git a/ui/src/components/table/TableConsts.ts b/ui/src/components/table/TableConsts.ts
new file mode 100644
index 000000000..d729c93d9
--- /dev/null
+++ b/ui/src/components/table/TableConsts.ts
@@ -0,0 +1,1 @@
+export const LABEL_FOR_DEFAULT_TABLE_CELL_VALUE = '[[default]]';
diff --git a/ui/src/components/table/TableExpansionRowData.jsx b/ui/src/components/table/TableExpansionRowData.jsx
index adcae33e4..2088b72f9 100644
--- a/ui/src/components/table/TableExpansionRowData.jsx
+++ b/ui/src/components/table/TableExpansionRowData.jsx
@@ -2,6 +2,8 @@ import React from 'react';
 import DL from '@splunk/react-ui/DefinitionList';
 import { _ } from '@splunk/ui-utils/i18n';
 
+import { getTableCellValue } from './table.utils';
+
 /**
  * Generates the definition list rows for the expansion view based on the provided row data and moreInfo configuration.
  *
@@ -10,23 +12,18 @@ import { _ } from '@splunk/ui-utils/i18n';
  * @returns {Array} - An array of React elements representing the definition list rows.
  */
 export function getExpansionRowData(row, moreInfo) {
-    const DefinitionLists = [];
-
-    if (moreInfo?.length) {
-        moreInfo.forEach((val) => {
+    return (
+        moreInfo?.reduce((definitionLists, val) => {
             const label = _(val.label);
-            // Remove extra rows which are empty in moreInfo
-            if (val.field in row && row[val.field] !== null && row[val.field] !== '') {
-                DefinitionLists.push(<DL.Term key={val.field}>{label}</DL.Term>);
-                DefinitionLists.push(
-                    <DL.Description key={`${val.field}_decr`}>
-                        {val.mapping && val.mapping[row[val.field]]
-                            ? val.mapping[row[val.field]]
-                            : String(row[val.field])}
-                    </DL.Description>
+            const cellValue = getTableCellValue(row, val.field, val.mapping);
+            // Remove extra rows which are empty in moreInfo and default value is not provided
+            if (cellValue) {
+                definitionLists.push(<DL.Term key={val.field}>{label}</DL.Term>);
+                definitionLists.push(
+                    <DL.Description key={`${val.field}_decr`}>{cellValue}</DL.Description>
                 );
             }
-        });
-    }
-    return DefinitionLists;
+            return definitionLists;
+        }, []) || []
+    );
 }
diff --git a/ui/src/components/table/table.utils.ts b/ui/src/components/table/table.utils.ts
index adb329550..ea0b1633f 100644
--- a/ui/src/components/table/table.utils.ts
+++ b/ui/src/components/table/table.utils.ts
@@ -1,6 +1,22 @@
 import { AcceptableFormRecord } from '../../types/components/shareableTypes';
 import { isTrue } from '../../util/considerFalseAndTruthy';
+import { LABEL_FOR_DEFAULT_TABLE_CELL_VALUE } from './TableConsts';
 
 export function isReadonlyRow(readonlyFieldId: string | undefined, row: AcceptableFormRecord) {
     return Boolean(readonlyFieldId && readonlyFieldId in row && isTrue(row[readonlyFieldId]));
 }
+
+export function getTableCellValue(
+    row: AcceptableFormRecord,
+    field: string,
+    mapping: Record<string, string>
+) {
+    const value = row[field];
+    const valueIsEmpty = value === null || value === undefined || value === '';
+    if (!valueIsEmpty) {
+        return mapping?.[String(value)] || value;
+    }
+
+    const defaultValue = mapping?.[LABEL_FOR_DEFAULT_TABLE_CELL_VALUE];
+    return defaultValue || value;
+}


Test patch:
diff --git a/ui/src/components/table/tests/TableExpansionRowData.test.tsx b/ui/src/components/table/tests/TableExpansionRowData.test.tsx
new file mode 100644
index 000000000..d7f3df03b
--- /dev/null
+++ b/ui/src/components/table/tests/TableExpansionRowData.test.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { getExpansionRowData } from '../TableExpansionRowData';
+import { LABEL_FOR_DEFAULT_TABLE_CELL_VALUE } from '../TableConsts';
+
+const moreInfo = [
+    { label: 'Name', field: 'name', mapping: { [LABEL_FOR_DEFAULT_TABLE_CELL_VALUE]: 'Unknown' } },
+    { label: 'Age', field: 'age' },
+    {
+        label: 'Country',
+        field: 'country',
+        mapping: { [LABEL_FOR_DEFAULT_TABLE_CELL_VALUE]: 'N/A' },
+    },
+];
+
+const getTermByText = (content: string) =>
+    screen.getAllByRole('term').find((term) => term.textContent === content);
+const getDefinitionByText = (content: string) =>
+    screen.getAllByRole('definition').find((definition) => definition.textContent === content);
+
+it('returns an empty array when moreInfo is undefined or empty', () => {
+    const result = getExpansionRowData({}, []);
+    expect(result).toEqual([]);
+});
+
+it('correctly processes non-empty moreInfo and returns expected React elements', async () => {
+    const row = { name: 'John Doe', age: 30, country: 'USA' };
+    render(<div>{getExpansionRowData(row, moreInfo)}</div>);
+
+    expect(screen.getAllByRole('definition')).toHaveLength(moreInfo.length);
+
+    expect(getTermByText('Name')).toBeInTheDocument();
+    expect(getDefinitionByText('John Doe')).toBeInTheDocument();
+
+    expect(getTermByText('Country')).toBeInTheDocument();
+    expect(getDefinitionByText('USA')).toBeInTheDocument();
+
+    expect(getTermByText('Age')).toBeInTheDocument();
+    expect(getDefinitionByText('30')).toBeInTheDocument();
+});
+it('excludes fields when not present in row and no default value is provided', async () => {
+    const row = { name: 'Jane Doe', country: 'Canada' };
+    render(<div>{getExpansionRowData(row, moreInfo)}</div>);
+
+    const userAge = screen.queryByText('30');
+    const ageTag = screen.queryByText('Age');
+
+    expect(userAge).not.toBeInTheDocument();
+    expect(ageTag).not.toBeInTheDocument();
+});
+
+it('includes fields with their default value when specified and field in row is empty or missing', () => {
+    const row = { age: 25 };
+    render(<div>{getExpansionRowData(row, moreInfo)}</div>);
+
+    expect(getDefinitionByText('Unknown')).toBeInTheDocument();
+    expect(getDefinitionByText('N/A')).toBeInTheDocument();
+});
+
+it('handles non-string values correctly, converting them to strings', () => {
+    const row = { name: 'Alice', age: null, country: undefined };
+    render(<div>{getExpansionRowData(row, moreInfo)}</div>);
+
+    expect(getDefinitionByText('Alice')).toBeInTheDocument();
+    expect(getDefinitionByText('N/A')).toBeInTheDocument();
+    expect(getDefinitionByText('Age')).toBeUndefined();
+});
diff --git a/ui/src/components/table/tests/table.utils.test.ts b/ui/src/components/table/tests/table.utils.test.ts
new file mode 100644
index 000000000..b4f96efa1
--- /dev/null
+++ b/ui/src/components/table/tests/table.utils.test.ts
@@ -0,0 +1,58 @@
+import { LABEL_FOR_DEFAULT_TABLE_CELL_VALUE } from '../TableConsts';
+import { getTableCellValue } from '../table.utils';
+
+it('should return row[field] if field exists in row and is not empty', () => {
+    const row = {
+        field1: 'value1',
+    };
+    const field = 'field1';
+    const mapping = {
+        [LABEL_FOR_DEFAULT_TABLE_CELL_VALUE]: 'defaultValue',
+    };
+    const result = getTableCellValue(row, field, mapping);
+    expect(result).toBe('value1');
+});
+
+it('should return mapping[LABEL_FOR_DEFAULT_TABLE_CELL_VALUE] if field does not exist in row and defaultValue exists in mapping', () => {
+    const row = {
+        field2: 'value2',
+    };
+    const field = 'field1';
+    const mapping = {
+        [LABEL_FOR_DEFAULT_TABLE_CELL_VALUE]: 'defaultValue',
+    };
+    const result = getTableCellValue(row, field, mapping);
+    expect(result).toBe('defaultValue');
+});
+
+it('should return row[field] if field exists in row and is empty but defaultValue does not exist in mapping', () => {
+    const row = {
+        field1: '',
+    };
+    const field = 'field1';
+    const mapping = {};
+    const result = getTableCellValue(row, field, mapping);
+    expect(result).toBe('');
+});
+
+it('should return undefined if field does not exist in row and defaultValue does not exist in mapping', () => {
+    const row = {
+        field2: '',
+    };
+    const field = 'field1';
+    const mapping = {};
+    const result = getTableCellValue(row, field, mapping);
+    expect(result).toBeUndefined();
+});
+
+it('should return correctly mapped value as field1 exists in row and its value should be mapped', () => {
+    const row = {
+        field1: 'someValue',
+    };
+    const field = 'field1';
+    const mapping = {
+        someValue: 'mappedValue',
+    };
+    const result = getTableCellValue(row, field, mapping);
+    expect(result).toBe('mappedValue');
+});