## Description **Problem** The table widget now supports the Select column type, which allows the column to contain both a label and a value. This could be useful for currency fields, foreign keys, or any other case where you want to display a different version of the column value. However, there is a problem with sorting. The table always sorts using the value, and does not give the user an option to sort using the label, where it makes sense in specific cases. **Solution** This PR adds a Sort By property to the table select cell, allowing the users to choose which value is used for sorting without affecting any functionality of the label or value of the select cell. [Additional Technical Documentation](https://www.notion.so/appsmith/Adding-sortBy-Property-to-Select-Columns-in-Table-Widget-V2-a5a41e6319a047378eb50a42be8c32ef?pvs=4) **Tested Cases** 1. Sort select column by default value Objective: Ensure that a newly added table with select column default sorts by the value **Steps:** - Drop a new table widget. - Add sample data to the table. - Set a column (e.g., role) to select type. - Add selectOptions property - Verify that when sorted in ascending or descending order, the sorting is correct 2. Sort select column by label Objective: Ensure that a newly added table with select column and sortBy property set to label sorts correctly **Steps:** - Drop a new table widget. - Add sample data to the table. - Set a column (e.g., role) to select type - Add selectOptions property - Set sortBy property to label - Verify that when sorted in ascending or descending order, the sorting is correct and based on the label value only 3. Verify that sorting of other table cells that are not select works as expected Objective: Ensure that every other cell type in the table sorts correctly **Steps:** - Drop a new table widget. - Add sample data to the table. - Set a column (e.g., role) to select type - Add selectOptions property - Set sortBy property to label - Verify that when columns other that role are sorted, they are sorted correctly 4. Verify that sorting works as expected when table data is a binding Objective: Ensure that the sorting works for all columns while using data binding in table data **Steps:** - Drop a new table widget - Link a query binding to the table data property - Verify that all columns sort correctly 5. Verify that sorting works as expected when table data is a binding and select column sorting is set to label Objective: Ensure that the sorting works for all columns while using data binding in table data with a select column set to sort by the label **Steps:** - Drop a new table widget - Link a query binding to the table data property - Set a column (e.g., role) to select type - Add selectOptions property - Set sortBy property to label - Verify that all columns sort correctly 6. Verify that sortBy logic does not take effect or break user experience until the user adds selectOptions in select cell **Steps:** - Drop a new table widget - Link a query binding to the table data property or add raw data - Set a column (e.g., role) to select type - Do not add selectOptions - Set sortBy property to label - Verify that all other columns display and sort correctly 7. Verify that sorting by label and value works correctly in deployed mode **Steps:** - Drop a new table widget - Link a query binding to the table data property or add raw data - Set a column (e.g., role) to select type - Add selectOptions - Set sortBy property to label - Deploy application - Confirm that sorting works correctly for all columns Fixes #21993 ## Automation /ok-to-test tags="@tag.Table, @tag.Binding, @tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/10165119164> > Commit: 8a4e8b238adc0d78726a811b54a209caa4990606 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=10165119164&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Table, @tag.Binding, @tag.Sanity` > Spec: > <hr>Tue, 30 Jul 2024 16:02:35 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [x] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a feature flag for dynamic table cell labeling. - Added a sorting option for select cells, allowing sort by value or label. - Enhanced user interface with new configuration options for select components. - Improved sorting functionality for select columns to sort by labels. - **Bug Fixes** - Improved sorting functionality in the table widget to ensure accurate data representation. - **Tests** - Expanded and clarified test cases for sorting functionality in the Table Widget V2. - Updated testing structure for better reliability and isolation of test scenarios. - Integrated dynamic testing capabilities based on feature flags. - **Documentation** - Added new locators for sorting controls to enhance UI interaction capabilities. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
948 lines
27 KiB
JavaScript
948 lines
27 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-unused-vars*/
|
|
export default {
|
|
getSelectedRow: (props, moment, _) => {
|
|
let index = -1;
|
|
|
|
/*
|
|
* If multiRowSelection is turned on, use the last index to
|
|
* populate the selectedRowIndex
|
|
*/
|
|
if (props.multiRowSelection) {
|
|
if (
|
|
_.isArray(props.selectedRowIndices) &&
|
|
props.selectedRowIndices.length &&
|
|
props.selectedRowIndices.every((i) => _.isNumber(i))
|
|
) {
|
|
index = props.selectedRowIndices[props.selectedRowIndices.length - 1];
|
|
} else if (_.isNumber(props.selectedRowIndices)) {
|
|
index = props.selectedRowIndices;
|
|
}
|
|
} else if (
|
|
!_.isNil(props.selectedRowIndex) &&
|
|
!_.isNaN(parseInt(props.selectedRowIndex))
|
|
) {
|
|
index = parseInt(props.selectedRowIndex);
|
|
}
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
|
|
|
const primaryColumns = props.primaryColumns;
|
|
const nonDataColumnTypes = [
|
|
"editActions",
|
|
"button",
|
|
"iconButton",
|
|
"menuButton",
|
|
];
|
|
const nonDataColumnAliases = primaryColumns
|
|
? Object.values(primaryColumns)
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
.map((column) => column.alias)
|
|
: [];
|
|
|
|
let selectedRow;
|
|
|
|
/*
|
|
* Note(Balaji): Need to include customColumn values in the selectedRow (select, rating)
|
|
* It should have updated values.
|
|
*/
|
|
if (index > -1) {
|
|
selectedRow = { ...rows[index] };
|
|
} else {
|
|
/*
|
|
* If index is not a valid, selectedRow should have
|
|
* proper row structure with empty string values
|
|
*/
|
|
selectedRow = {};
|
|
Object.keys(rows[0]).forEach((key) => {
|
|
selectedRow[key] = "";
|
|
});
|
|
}
|
|
|
|
const keysToBeOmitted = [
|
|
"__originalIndex__",
|
|
"__primaryKey__",
|
|
...nonDataColumnAliases,
|
|
];
|
|
return _.omit(selectedRow, keysToBeOmitted);
|
|
},
|
|
//
|
|
getTriggeredRow: (props, moment, _) => {
|
|
let index = -1;
|
|
const parsedTriggeredRowIndex = parseInt(props.triggeredRowIndex);
|
|
|
|
if (!_.isNaN(parsedTriggeredRowIndex)) {
|
|
index = parsedTriggeredRowIndex;
|
|
}
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
|
const primaryColumns = props.primaryColumns;
|
|
const nonDataColumnTypes = [
|
|
"editActions",
|
|
"button",
|
|
"iconButton",
|
|
"menuButton",
|
|
];
|
|
const nonDataColumnAliases = primaryColumns
|
|
? Object.values(primaryColumns)
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
.map((column) => column.alias)
|
|
: [];
|
|
let triggeredRow;
|
|
|
|
/*
|
|
* Note(Balaji): Need to include customColumn values in the triggeredRow (select, rating)
|
|
* It should have updated values.
|
|
*/
|
|
if (index > -1) {
|
|
const row = rows.find((row) => row.__originalIndex__ === index);
|
|
triggeredRow = { ...row };
|
|
} else {
|
|
/*
|
|
* If triggeredRowIndex is not a valid index, triggeredRow should
|
|
* have proper row structure with empty string values
|
|
*/
|
|
triggeredRow = {};
|
|
Object.keys(rows[0]).forEach((key) => {
|
|
triggeredRow[key] = "";
|
|
});
|
|
}
|
|
|
|
const keysToBeOmitted = [
|
|
"__originalIndex__",
|
|
"__primaryKey__",
|
|
...nonDataColumnAliases,
|
|
];
|
|
return _.omit(triggeredRow, keysToBeOmitted);
|
|
},
|
|
//
|
|
getSelectedRows: (props, moment, _) => {
|
|
if (!props.multiRowSelection) {
|
|
return [];
|
|
}
|
|
|
|
let indices = [];
|
|
|
|
if (
|
|
_.isArray(props.selectedRowIndices) &&
|
|
props.selectedRowIndices.every((i) => _.isNumber(i))
|
|
) {
|
|
indices = props.selectedRowIndices;
|
|
}
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
|
const primaryColumns = props.primaryColumns;
|
|
const nonDataColumnTypes = [
|
|
"editActions",
|
|
"button",
|
|
"iconButton",
|
|
"menuButton",
|
|
];
|
|
const nonDataColumnAliases = primaryColumns
|
|
? Object.values(primaryColumns)
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
.map((column) => column.alias)
|
|
: [];
|
|
const keysToBeOmitted = [
|
|
"__originalIndex__",
|
|
"__primaryKey__",
|
|
...nonDataColumnAliases,
|
|
];
|
|
return indices.map((index) => _.omit(rows[index], keysToBeOmitted));
|
|
},
|
|
//
|
|
getPageSize: (props, moment, _) => {
|
|
const TABLE_SIZES = {
|
|
DEFAULT: {
|
|
COLUMN_HEADER_HEIGHT: 32,
|
|
TABLE_HEADER_HEIGHT: 38,
|
|
ROW_HEIGHT: 40,
|
|
ROW_FONT_SIZE: 14,
|
|
VERTICAL_PADDING: 6,
|
|
EDIT_ICON_TOP: 10,
|
|
},
|
|
SHORT: {
|
|
COLUMN_HEADER_HEIGHT: 32,
|
|
TABLE_HEADER_HEIGHT: 38,
|
|
ROW_HEIGHT: 30,
|
|
ROW_FONT_SIZE: 12,
|
|
VERTICAL_PADDING: 0,
|
|
EDIT_ICON_TOP: 5,
|
|
},
|
|
TALL: {
|
|
COLUMN_HEADER_HEIGHT: 32,
|
|
TABLE_HEADER_HEIGHT: 38,
|
|
ROW_HEIGHT: 60,
|
|
ROW_FONT_SIZE: 18,
|
|
VERTICAL_PADDING: 16,
|
|
EDIT_ICON_TOP: 21,
|
|
},
|
|
};
|
|
const compactMode = props.compactMode || "DEFAULT";
|
|
const componentHeight = props.componentHeight - 10;
|
|
const tableSizes = TABLE_SIZES[compactMode];
|
|
|
|
let pageSize =
|
|
(componentHeight -
|
|
tableSizes.TABLE_HEADER_HEIGHT -
|
|
tableSizes.COLUMN_HEADER_HEIGHT) /
|
|
tableSizes.ROW_HEIGHT;
|
|
|
|
return pageSize % 1 > 0.3 && props.tableData.length > pageSize
|
|
? Math.ceil(pageSize)
|
|
: Math.floor(pageSize);
|
|
},
|
|
//
|
|
getProcessedTableData: (props, moment, _) => {
|
|
let data;
|
|
|
|
if (_.isArray(props.tableData)) {
|
|
/* Populate meta keys (__originalIndex__, __primaryKey__) and transient values */
|
|
data = props.tableData.map((row, index) => ({
|
|
...row,
|
|
__originalIndex__: index,
|
|
__primaryKey__: props.primaryColumnId
|
|
? row[props.primaryColumnId]
|
|
: undefined,
|
|
...props.transientTableData[index],
|
|
}));
|
|
} else {
|
|
data = [];
|
|
}
|
|
|
|
return data;
|
|
},
|
|
//
|
|
getOrderedTableColumns: (props, moment, _) => {
|
|
let columns = [];
|
|
let existingColumns = props.primaryColumns || {};
|
|
/*
|
|
* Assign index based on the columnOrder
|
|
*/
|
|
if (
|
|
_.isArray(props.columnOrder) &&
|
|
props.columnOrder.length > 0 &&
|
|
Object.keys(existingColumns).length > 0
|
|
) {
|
|
const newColumnsInOrder = {};
|
|
let index = 0;
|
|
|
|
_.uniq(props.columnOrder).forEach((columnId) => {
|
|
if (existingColumns[columnId]) {
|
|
newColumnsInOrder[columnId] = Object.assign(
|
|
{},
|
|
existingColumns[columnId],
|
|
{
|
|
index,
|
|
},
|
|
);
|
|
|
|
index++;
|
|
}
|
|
});
|
|
|
|
existingColumns = newColumnsInOrder;
|
|
}
|
|
|
|
const sortByColumn = props.sortOrder && props.sortOrder.column;
|
|
const isAscOrder = props.sortOrder && props.sortOrder.order === "asc";
|
|
/* set sorting flags and convert the existing columns into an array */
|
|
Object.values(existingColumns).forEach((column) => {
|
|
/* guard to not allow columns without id */
|
|
if (column.id) {
|
|
columns.push({
|
|
...column,
|
|
isAscOrder: column.id === sortByColumn ? isAscOrder : undefined,
|
|
});
|
|
}
|
|
});
|
|
|
|
return columns;
|
|
},
|
|
//
|
|
getFilteredTableData: (props, moment, _) => {
|
|
/* Make a shallow copy */
|
|
const primaryColumns = props.primaryColumns || {};
|
|
let processedTableData = [...props.processedTableData];
|
|
const derivedColumns = {};
|
|
|
|
Object.keys(primaryColumns).forEach((id) => {
|
|
if (primaryColumns[id] && primaryColumns[id].isDerived) {
|
|
derivedColumns[id] = primaryColumns[id];
|
|
}
|
|
});
|
|
|
|
if (!processedTableData || !processedTableData.length) {
|
|
return [];
|
|
}
|
|
|
|
/* extend processedTableData with values from
|
|
* - computedValues, in case of normal column
|
|
* - empty values, in case of derived column
|
|
*/
|
|
if (primaryColumns && _.isPlainObject(primaryColumns)) {
|
|
Object.entries(primaryColumns).forEach(([id, column]) => {
|
|
let computedValues = [];
|
|
|
|
if (column && column.computedValue) {
|
|
if (_.isString(column.computedValue)) {
|
|
try {
|
|
computedValues = JSON.parse(column.computedValue);
|
|
} catch (e) {
|
|
/* do nothing */
|
|
}
|
|
} else if (_.isArray(column.computedValue)) {
|
|
computedValues = column.computedValue;
|
|
}
|
|
}
|
|
|
|
/* for derived columns inject empty strings */
|
|
if (
|
|
computedValues.length === 0 &&
|
|
derivedColumns &&
|
|
derivedColumns[id]
|
|
) {
|
|
computedValues = Array(processedTableData.length).fill("");
|
|
}
|
|
|
|
computedValues.forEach((computedValue, index) => {
|
|
processedTableData[index] = {
|
|
...processedTableData[index],
|
|
[column.alias]: computedValue,
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
const columns = props.orderedTableColumns;
|
|
const sortByColumnId = props.sortOrder.column;
|
|
|
|
let sortedTableData;
|
|
/*
|
|
Check if there are select columns,
|
|
and if the columns are sorting by label instead of default value
|
|
*/
|
|
const selectColumnKeysWithSortByLabel = [];
|
|
Object.entries(primaryColumns).forEach(([id, column]) => {
|
|
const isColumnSortedByLabel =
|
|
column?.columnType === "select" &&
|
|
column?.sortBy === "label" &&
|
|
column?.selectOptions?.length;
|
|
if (isColumnSortedByLabel) {
|
|
selectColumnKeysWithSortByLabel.push(id);
|
|
}
|
|
});
|
|
|
|
/*
|
|
If there are select columns,
|
|
transform the specific columns data to show the label instead of the value for sorting
|
|
*/
|
|
let processedTableDataWithLabelInsteadOfValue;
|
|
if (selectColumnKeysWithSortByLabel.length) {
|
|
const transformedValueToLabelTableData = processedTableData.map((row) => {
|
|
const newRow = { ...row };
|
|
selectColumnKeysWithSortByLabel.forEach((key) => {
|
|
const value = row[key];
|
|
const selectOptions =
|
|
primaryColumns[key].selectOptions[row.__originalIndex__];
|
|
const option = selectOptions.find((option) => option.value === value);
|
|
|
|
if (option) {
|
|
newRow[key] = option.label;
|
|
}
|
|
});
|
|
|
|
return newRow;
|
|
});
|
|
processedTableDataWithLabelInsteadOfValue =
|
|
transformedValueToLabelTableData;
|
|
}
|
|
|
|
if (sortByColumnId) {
|
|
const sortBycolumn = columns.find(
|
|
(column) => column.id === sortByColumnId,
|
|
);
|
|
const sortByColumnOriginalId = sortBycolumn.alias;
|
|
|
|
const columnType =
|
|
sortBycolumn && sortBycolumn.columnType
|
|
? sortBycolumn.columnType
|
|
: "text";
|
|
|
|
let inputFormat = (() => {
|
|
switch (sortBycolumn.inputFormat) {
|
|
case "Epoch":
|
|
return "X";
|
|
case "Milliseconds":
|
|
return "x";
|
|
default:
|
|
return sortBycolumn.inputFormat;
|
|
}
|
|
})();
|
|
|
|
const isEmptyOrNil = (value) => {
|
|
return _.isNil(value) || value === "";
|
|
};
|
|
const isAscOrder = props.sortOrder.order === "asc";
|
|
const sortByOrder = (isAGreaterThanB) => {
|
|
if (isAGreaterThanB) {
|
|
return isAscOrder ? 1 : -1;
|
|
} else {
|
|
return isAscOrder ? -1 : 1;
|
|
}
|
|
};
|
|
|
|
const transformedTableDataForSorting =
|
|
selectColumnKeysWithSortByLabel.length
|
|
? processedTableDataWithLabelInsteadOfValue
|
|
: processedTableData;
|
|
|
|
sortedTableData = transformedTableDataForSorting.sort((a, b) => {
|
|
if (_.isPlainObject(a) && _.isPlainObject(b)) {
|
|
if (
|
|
isEmptyOrNil(a[sortByColumnOriginalId]) ||
|
|
isEmptyOrNil(b[sortByColumnOriginalId])
|
|
) {
|
|
/* push null, undefined and "" values to the bottom. */
|
|
return isEmptyOrNil(a[sortByColumnOriginalId]) ? 1 : -1;
|
|
} else {
|
|
switch (columnType) {
|
|
case "number":
|
|
case "currency":
|
|
return sortByOrder(
|
|
Number(a[sortByColumnOriginalId]) >
|
|
Number(b[sortByColumnOriginalId]),
|
|
);
|
|
case "date":
|
|
try {
|
|
return sortByOrder(
|
|
moment(a[sortByColumnOriginalId], inputFormat).isAfter(
|
|
moment(b[sortByColumnOriginalId], inputFormat),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
case "url":
|
|
const column = primaryColumns[sortByColumnOriginalId];
|
|
if (column && column.displayText) {
|
|
if (_.isString(column.displayText)) {
|
|
return sortByOrder(false);
|
|
} else if (_.isArray(column.displayText)) {
|
|
return sortByOrder(
|
|
column.displayText[a.__originalIndex__]
|
|
.toString()
|
|
.toLowerCase() >
|
|
column.displayText[b.__originalIndex__]
|
|
.toString()
|
|
.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
default:
|
|
return sortByOrder(
|
|
a[sortByColumnOriginalId].toString().toLowerCase() >
|
|
b[sortByColumnOriginalId].toString().toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
return isAscOrder ? 1 : 0;
|
|
}
|
|
});
|
|
|
|
if (selectColumnKeysWithSortByLabel.length) {
|
|
const transformedLabelToValueData = sortedTableData.map((row) => {
|
|
const newRow = { ...row };
|
|
selectColumnKeysWithSortByLabel.forEach((key) => {
|
|
const label = row[key];
|
|
const selectOptions =
|
|
primaryColumns[key].selectOptions[row.__originalIndex__];
|
|
const option = selectOptions.find(
|
|
(option) => option.label === label,
|
|
);
|
|
if (option) {
|
|
newRow[key] = option.value;
|
|
}
|
|
});
|
|
|
|
return newRow;
|
|
});
|
|
sortedTableData = transformedLabelToValueData;
|
|
}
|
|
} else {
|
|
sortedTableData = [...processedTableData];
|
|
}
|
|
|
|
const ConditionFunctions = {
|
|
isExactly: (a, b) => {
|
|
return a.toString() === b.toString();
|
|
},
|
|
empty: (a) => {
|
|
return _.isNil(a) || _.isEmpty(a.toString());
|
|
},
|
|
notEmpty: (a) => {
|
|
return !_.isNil(a) && !_.isEmpty(a.toString());
|
|
},
|
|
notEqualTo: (a, b) => {
|
|
return a.toString() !== b.toString();
|
|
},
|
|
/* Note: Duplicate of isExactly */
|
|
isEqualTo: (a, b) => {
|
|
return a.toString() === b.toString();
|
|
},
|
|
lessThan: (a, b) => {
|
|
return Number(a) < Number(b);
|
|
},
|
|
lessThanEqualTo: (a, b) => {
|
|
return Number(a) <= Number(b);
|
|
},
|
|
greaterThan: (a, b) => {
|
|
return Number(a) > Number(b);
|
|
},
|
|
greaterThanEqualTo: (a, b) => {
|
|
return Number(a) >= Number(b);
|
|
},
|
|
contains: (a, b) => {
|
|
try {
|
|
return a
|
|
.toString()
|
|
.toLowerCase()
|
|
.includes(b.toString().toLowerCase());
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
doesNotContain: (a, b) => {
|
|
try {
|
|
return !a
|
|
.toString()
|
|
.toLowerCase()
|
|
.includes(b.toString().toLowerCase());
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
startsWith: (a, b) => {
|
|
try {
|
|
return (
|
|
a.toString().toLowerCase().indexOf(b.toString().toLowerCase()) === 0
|
|
);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
endsWith: (a, b) => {
|
|
try {
|
|
const _a = a.toString().toLowerCase();
|
|
const _b = b.toString().toLowerCase();
|
|
return (
|
|
_a.lastIndexOf(_b) >= 0 &&
|
|
_a.length === _a.lastIndexOf(_b) + _b.length
|
|
);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
is: (a, b) => {
|
|
return moment(a).isSame(moment(b), "minute");
|
|
},
|
|
isNot: (a, b) => {
|
|
return !moment(a).isSame(moment(b), "minute");
|
|
},
|
|
isAfter: (a, b) => {
|
|
return moment(a).isAfter(moment(b), "minute");
|
|
},
|
|
isBefore: (a, b) => {
|
|
return moment(a).isBefore(moment(b), "minute");
|
|
},
|
|
isChecked: (a) => {
|
|
return a === true;
|
|
},
|
|
isUnChecked: (a) => {
|
|
return a === false;
|
|
},
|
|
};
|
|
let searchKey;
|
|
|
|
/* skipping search when client side search is turned off */
|
|
if (
|
|
props.searchText &&
|
|
(!props.onSearchTextChanged || props.enableClientSideSearch)
|
|
) {
|
|
searchKey = props.searchText.toLowerCase();
|
|
} else {
|
|
searchKey = "";
|
|
}
|
|
|
|
/*
|
|
* We need to omit hidden column values from being included
|
|
* in the search
|
|
*/
|
|
const hiddenColumns = Object.values(props.primaryColumns)
|
|
.filter((column) => !column.isVisible)
|
|
.map((column) => column.alias);
|
|
|
|
const finalTableData = sortedTableData.filter((row) => {
|
|
let isSearchKeyFound = true;
|
|
const columnWithDisplayText = Object.values(props.primaryColumns).filter(
|
|
(column) => column.columnType === "url" && column.displayText,
|
|
);
|
|
const displayedRow = {
|
|
...row,
|
|
...columnWithDisplayText.reduce((acc, column) => {
|
|
let displayText;
|
|
if (_.isArray(column.displayText)) {
|
|
displayText = column.displayText[row.__originalIndex__];
|
|
} else {
|
|
displayText = column.displayText;
|
|
}
|
|
acc[column.alias] = displayText;
|
|
return acc;
|
|
}, {}),
|
|
};
|
|
if (searchKey) {
|
|
isSearchKeyFound = Object.values(_.omit(displayedRow, hiddenColumns))
|
|
.join(", ")
|
|
.toLowerCase()
|
|
.includes(searchKey);
|
|
}
|
|
if (!isSearchKeyFound) {
|
|
return false;
|
|
}
|
|
|
|
/* when there is no filter defined or when server side filtering is enabled prevent client-side filtering */
|
|
if (
|
|
!props.filters ||
|
|
props.filters.length === 0 ||
|
|
props.enableServerSideFiltering
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const filterOperator =
|
|
props.filters.length >= 2 ? props.filters[1].operator : "OR";
|
|
let isSatisfyingFilters = filterOperator === "AND";
|
|
for (let i = 0; i < props.filters.length; i++) {
|
|
let filterResult = true;
|
|
try {
|
|
const conditionFunction =
|
|
ConditionFunctions[props.filters[i].condition];
|
|
if (conditionFunction) {
|
|
filterResult = conditionFunction(
|
|
displayedRow[props.filters[i].column],
|
|
props.filters[i].value,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
filterResult = false;
|
|
}
|
|
|
|
/* if one filter condition is not satisfied and filter operator is AND, bailout early */
|
|
if (!filterResult && filterOperator === "AND") {
|
|
isSatisfyingFilters = false;
|
|
break;
|
|
} else if (filterResult && filterOperator === "OR") {
|
|
/* if one filter condition is satisfied and filter operator is OR, bailout early */
|
|
isSatisfyingFilters = true;
|
|
break;
|
|
}
|
|
|
|
isSatisfyingFilters =
|
|
filterOperator === "AND"
|
|
? isSatisfyingFilters && filterResult
|
|
: isSatisfyingFilters || filterResult;
|
|
}
|
|
|
|
return isSatisfyingFilters;
|
|
});
|
|
return finalTableData;
|
|
},
|
|
//
|
|
getUpdatedRow: (props, moment, _) => {
|
|
let index = -1;
|
|
const parsedUpdatedRowIndex = parseInt(props.updatedRowIndex);
|
|
|
|
if (!_.isNaN(parsedUpdatedRowIndex)) {
|
|
index = parsedUpdatedRowIndex;
|
|
}
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
|
const primaryColumns = props.primaryColumns;
|
|
let updatedRow;
|
|
|
|
if (index > -1) {
|
|
const row = rows.find((row) => row.__originalIndex__ === index);
|
|
updatedRow = { ...row };
|
|
} else {
|
|
/*
|
|
* If updatedRowIndex is not a valid index, updatedRow should
|
|
* have proper row structure with empty string values
|
|
*/
|
|
updatedRow = {};
|
|
if (rows && rows[0]) {
|
|
Object.keys(rows[0]).forEach((key) => {
|
|
updatedRow[key] = "";
|
|
});
|
|
}
|
|
}
|
|
|
|
const nonDataColumnTypes = [
|
|
"editActions",
|
|
"button",
|
|
"iconButton",
|
|
"menuButton",
|
|
];
|
|
const nonDataColumnAliases = primaryColumns
|
|
? Object.values(primaryColumns)
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
.map((column) => column.alias)
|
|
: [];
|
|
|
|
const keysToBeOmitted = [
|
|
"__originalIndex__",
|
|
"__primaryKey__",
|
|
...nonDataColumnAliases,
|
|
];
|
|
return _.omit(updatedRow, keysToBeOmitted);
|
|
},
|
|
//
|
|
getUpdatedRows: (props, moment, _) => {
|
|
const primaryColumns = props.primaryColumns;
|
|
const nonDataColumnTypes = [
|
|
"editActions",
|
|
"button",
|
|
"iconButton",
|
|
"menuButton",
|
|
];
|
|
const nonDataColumnAliases = primaryColumns
|
|
? Object.values(primaryColumns)
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
.map((column) => column.alias)
|
|
: [];
|
|
const keysToBeOmitted = [
|
|
"__originalIndex__",
|
|
"__primaryKey__",
|
|
...nonDataColumnAliases,
|
|
];
|
|
/*
|
|
* case 1. If transientTableData is not empty, return aray of updated row.
|
|
* case 2. If transientTableData is empty, return empty array
|
|
*
|
|
* updated row structure
|
|
* {
|
|
* index: {{original index of the row}},
|
|
* {{primary_column}}: {{primary_column_value}} // only if primary has been set
|
|
* updatedFields: {
|
|
* {{updated_column_1}}: {{updated_column_1_value}}
|
|
* },
|
|
* allFields: {
|
|
* {{updated_column_1}}: {{updated_column_1_value}}
|
|
* {{rest of the fields from the row}}
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/* case 1 */
|
|
if (
|
|
props.transientTableData &&
|
|
!!Object.keys(props.transientTableData).length
|
|
) {
|
|
const updatedRows = [];
|
|
const tableData = props.processedTableData || props.tableData;
|
|
|
|
/* updatedRows is not sorted by index */
|
|
Object.entries(props.transientTableData)
|
|
.filter((entry) => {
|
|
return (
|
|
!_.isNil(entry[0]) && !!entry[0] && _.isFinite(Number(entry[0]))
|
|
);
|
|
})
|
|
.forEach((entry) => {
|
|
const key = entry[0];
|
|
const value = entry[1];
|
|
const row = tableData.find(
|
|
(row) => row.__originalIndex__ === Number(key),
|
|
);
|
|
|
|
updatedRows.push({
|
|
index: Number(key),
|
|
[props.primaryColumnId]: row[props.primaryColumnId],
|
|
updatedFields: value,
|
|
allFields: _.omit(row, keysToBeOmitted) || {},
|
|
});
|
|
});
|
|
|
|
return updatedRows;
|
|
} else {
|
|
/* case 2 */
|
|
return [];
|
|
}
|
|
},
|
|
//
|
|
getUpdatedRowIndices: (props, moment, _) => {
|
|
/* should return the keys of the transientTableData */
|
|
if (props.transientTableData) {
|
|
return Object.keys(props.transientTableData).map((index) =>
|
|
Number(index),
|
|
);
|
|
} else {
|
|
return [];
|
|
}
|
|
},
|
|
//
|
|
getPageOffset: (props, moment, _) => {
|
|
const pageSize =
|
|
props.serverSidePaginationEnabled && props.tableData
|
|
? props.tableData?.length
|
|
: props.pageSize;
|
|
|
|
if (
|
|
Number.isFinite(props.pageNo) &&
|
|
Number.isFinite(pageSize) &&
|
|
props.pageNo >= 0 &&
|
|
pageSize >= 0
|
|
) {
|
|
/* Math.max fixes the value of (pageNo - 1) to a minimum of 0 as negative values are not valid */
|
|
return Math.max(props.pageNo - 1, 0) * pageSize;
|
|
}
|
|
return 0;
|
|
},
|
|
//
|
|
getEditableCellValidity: (props, moment, _) => {
|
|
if (
|
|
(!props.editableCell?.column && !props.isAddRowInProgress) ||
|
|
!props.primaryColumns
|
|
) {
|
|
return {};
|
|
}
|
|
|
|
const createRegex = (regex) => {
|
|
if (!regex) {
|
|
return new RegExp("//");
|
|
}
|
|
|
|
/*
|
|
* break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags
|
|
* Example /test/i will be split into ["/test/gi", "/", "test", "gi"]
|
|
*/
|
|
const regexParts = regex.match(/(\/?)(.+)\\1([a-z]*)/i);
|
|
let parsedRegex;
|
|
|
|
if (!regexParts) {
|
|
parsedRegex = new RegExp(regex);
|
|
} else {
|
|
/*
|
|
* if we don't have a regex flags (gmisuy), convert provided string into regexp directly
|
|
*/
|
|
if (
|
|
regexParts[3] &&
|
|
!/^(?!.*?(.).*?\\1)[gmisuy]+$/.test(regexParts[3])
|
|
) {
|
|
parsedRegex = RegExp(regex);
|
|
} else {
|
|
/*
|
|
* if we have a regex flags, use it to form regexp
|
|
*/
|
|
parsedRegex = new RegExp(regexParts[2], regexParts[3]);
|
|
}
|
|
}
|
|
|
|
return parsedRegex;
|
|
};
|
|
|
|
let editableColumns = [];
|
|
const validatableColumns = ["text", "number", "currency"];
|
|
|
|
if (props.isAddRowInProgress) {
|
|
Object.values(props.primaryColumns)
|
|
.filter(
|
|
(column) =>
|
|
column.isEditable && validatableColumns.includes(column.columnType),
|
|
)
|
|
.forEach((column) => {
|
|
editableColumns.push([column, props.newRow[column.alias]]);
|
|
});
|
|
} else {
|
|
const editedColumn = Object.values(props.primaryColumns).find(
|
|
(column) => column.alias === props.editableCell?.column,
|
|
);
|
|
|
|
if (validatableColumns.includes(editedColumn.columnType)) {
|
|
editableColumns.push([editedColumn, props.editableCell?.value]);
|
|
}
|
|
}
|
|
|
|
const validationMap = {};
|
|
|
|
editableColumns.forEach((validationObj) => {
|
|
const editedColumn = validationObj[0];
|
|
const value = validationObj[1];
|
|
|
|
if (editedColumn && editedColumn.validation) {
|
|
const validation = editedColumn.validation;
|
|
|
|
/* General validations */
|
|
if (
|
|
!validation.isColumnEditableCellRequired &&
|
|
(value === "" || _.isNil(value))
|
|
) {
|
|
validationMap[editedColumn.alias] = true;
|
|
return;
|
|
} else if (
|
|
(!_.isNil(validation.isColumnEditableCellValid) &&
|
|
!validation.isColumnEditableCellValid) ||
|
|
(validation.regex && !createRegex(validation.regex).test(value)) ||
|
|
(validation.isColumnEditableCellRequired &&
|
|
(value === "" || _.isNil(value)))
|
|
) {
|
|
validationMap[editedColumn.alias] = false;
|
|
return;
|
|
}
|
|
|
|
/* Column type related validations */
|
|
switch (editedColumn.columnType) {
|
|
case "number":
|
|
case "currency":
|
|
if (
|
|
!_.isNil(validation.min) &&
|
|
validation.min !== "" &&
|
|
validation.min > value
|
|
) {
|
|
validationMap[editedColumn.alias] = false;
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!_.isNil(validation.max) &&
|
|
validation.max !== "" &&
|
|
validation.max < value
|
|
) {
|
|
validationMap[editedColumn.alias] = false;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
validationMap[editedColumn.alias] = true;
|
|
});
|
|
|
|
return validationMap;
|
|
},
|
|
//
|
|
getTableHeaders: (props, moment, _) => {
|
|
const columns = props.primaryColumns
|
|
? Object.values(props.primaryColumns)
|
|
: [];
|
|
|
|
return columns
|
|
.sort((a, b) => a.index - b.index)
|
|
.map((column) => ({
|
|
id: column?.id,
|
|
label: column?.label,
|
|
isVisible: column?.isVisible,
|
|
}));
|
|
},
|
|
//
|
|
};
|