Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: default value for custom mapping #1304

Merged
merged 13 commits into from
Aug 8, 2024
7 changes: 5 additions & 2 deletions docs/advanced/custom_mapping.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
}
}
],
Expand All @@ -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,
Expand Down
9 changes: 2 additions & 7 deletions ui/src/components/table/CustomTableRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
);
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/table/TableConsts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const LABEL_FOR_DEFAULT_TABLE_CELL_VALUE = '[[default]]';
29 changes: 13 additions & 16 deletions ui/src/components/table/TableExpansionRowData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
}, []) || []
);
}
16 changes: 16 additions & 0 deletions ui/src/components/table/table.utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
67 changes: 67 additions & 0 deletions ui/src/components/table/tests/TableExpansionRowData.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
58 changes: 58 additions & 0 deletions ui/src/components/table/tests/table.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
Loading