feat: adds custom sort function feature flag for table widget (#39649)
## Description ### Custom Sort Function for Table Widget This PR introduces a new custom sort function feature for the Table Widget, allowing users to define their own sorting logic for table data. **Feature Flag Implementation** - Added a new feature flag `release_table_custom_sort_function_enabled` to control the availability of this feature - Set the default value to `false` to allow for controlled rollout **Table Widget Updates** - Added `customSortFunction` property to the TableWidgetV2 props interface - Updated property configuration to include the custom sort function control - Added helper text and placeholder to guide users on proper implementation - Implemented visibility condition based on the feature flag and sortable state **Control Updates** - Enhanced `TableCustomSortControl` to support the new custom sorting logic - Improved error handling in the sort function implementation - Updated the binding structure to provide access to original data with `original_` prefix - Modified the function signature to use `(tableData, column, order)` parameters **Testing** - Updated test cases to verify the extraction and computation of custom sort expressions - Implemented round-trip testing to ensure proper function parsing and generation - Added tests for edge cases like empty strings and non-matching formats This feature enables users to implement complex sorting logic beyond the default column-based sorting, such as multi-column sorting, custom collation, or business-specific ordering rules. Fixes #https://github.com/appsmithorg/appsmith-ee/issues/6503 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Table, @tag.Sanity, @tag.Datasource" ### 🔍 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/13986693183> > Commit: b64c2919c5d8eb1b82f6f8e6c76b98a60c2a6e22 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13986693183&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Table, @tag.Sanity, @tag.Datasource` > Spec: > <hr>Fri, 21 Mar 2025 08:35:48 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced custom table sorting, enabling users to define and apply their own sorting logic in table displays. - Added a new control interface for configuring custom sort functions, offering enhanced flexibility in data presentation. - Integrated an option to enable or disable the custom sort functionality based on feature flags. - Enhanced the table widget to support custom sorting function data, allowing for dynamic data presentation. - Added a new property configuration for custom sorting functions within the table widget. - **Tests** - Implemented comprehensive tests to verify custom sorting input processing and ensure accurate data computation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
db25d123da
commit
09d8a4deab
|
|
@ -53,6 +53,8 @@ export const FEATURE_FLAG = {
|
||||||
"release_external_saas_plugins_enabled",
|
"release_external_saas_plugins_enabled",
|
||||||
release_tablev2_infinitescroll_enabled:
|
release_tablev2_infinitescroll_enabled:
|
||||||
"release_tablev2_infinitescroll_enabled",
|
"release_tablev2_infinitescroll_enabled",
|
||||||
|
release_table_custom_sort_function_enabled:
|
||||||
|
"release_table_custom_sort_function_enabled",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FeatureFlag = keyof typeof FEATURE_FLAG;
|
export type FeatureFlag = keyof typeof FEATURE_FLAG;
|
||||||
|
|
@ -97,6 +99,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
||||||
release_ads_entity_item_enabled: false,
|
release_ads_entity_item_enabled: false,
|
||||||
release_external_saas_plugins_enabled: false,
|
release_external_saas_plugins_enabled: false,
|
||||||
release_tablev2_infinitescroll_enabled: false,
|
release_tablev2_infinitescroll_enabled: false,
|
||||||
|
release_table_custom_sort_function_enabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AB_TESTING_EVENT_KEYS = {
|
export const AB_TESTING_EVENT_KEYS = {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||||
|
import TableCustomSortControl from "./TableCustomSortControl";
|
||||||
|
|
||||||
|
const requiredParams = {
|
||||||
|
evaluatedValue: "",
|
||||||
|
widgetProperties: {
|
||||||
|
widgetName: "Table1",
|
||||||
|
primaryColumns: {
|
||||||
|
id: {
|
||||||
|
alias: "id",
|
||||||
|
originalId: "id",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
alias: "name",
|
||||||
|
originalId: "name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentPropertyName: "",
|
||||||
|
parentPropertyValue: "",
|
||||||
|
additionalDynamicData: {},
|
||||||
|
label: "Custom Sort",
|
||||||
|
propertyName: "customSort",
|
||||||
|
controlType: "TABLE_CUSTOM_SORT",
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
onPropertyChange: jest.fn(),
|
||||||
|
openNextPanel: jest.fn(),
|
||||||
|
deleteProperties: jest.fn(),
|
||||||
|
hideEvaluatedValue: false,
|
||||||
|
placeholderText: "Enter custom sort logic",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("TableCustomSortControl.getInputComputedValue", () => {
|
||||||
|
// Create an instance to use for generating test data
|
||||||
|
const testInstance = new TableCustomSortControl({
|
||||||
|
...requiredParams,
|
||||||
|
theme: EditorTheme.LIGHT,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract computation expression correctly from binding string with arrow function", () => {
|
||||||
|
const sortExpression = "tableData.sort((a, b) => a.id - b.id)";
|
||||||
|
|
||||||
|
const bindingString = testInstance.getComputedValue(
|
||||||
|
`{{${sortExpression}}}`,
|
||||||
|
"Table1",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = TableCustomSortControl.getInputComputedValue(bindingString);
|
||||||
|
|
||||||
|
expect(result).toBe(`{{${sortExpression}}}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract computation expression correctly from binding string with function body", () => {
|
||||||
|
const sortExpression = `{
|
||||||
|
return tableData.filter(row => row.original_status === "active");
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const bindingString = testInstance.getComputedValue(
|
||||||
|
`{{${sortExpression}}}`,
|
||||||
|
"Table1",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = TableCustomSortControl.getInputComputedValue(bindingString);
|
||||||
|
|
||||||
|
expect(result).toBe(`{{${sortExpression}}}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return original value when binding string doesn't match expected format", () => {
|
||||||
|
const nonMatchingString = "{{Table1.tableData[0].id}}";
|
||||||
|
const result =
|
||||||
|
TableCustomSortControl.getInputComputedValue(nonMatchingString);
|
||||||
|
|
||||||
|
expect(result).toBe(nonMatchingString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty string input", () => {
|
||||||
|
const emptyString = "";
|
||||||
|
const result = TableCustomSortControl.getInputComputedValue(emptyString);
|
||||||
|
|
||||||
|
expect(result).toBe(emptyString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support round-trip conversion (getComputedValue -> getInputComputedValue)", () => {
|
||||||
|
// Test with arrow function
|
||||||
|
const arrowExpression =
|
||||||
|
"tableData.sort((a, b) => a.name.localeCompare(b.name))";
|
||||||
|
const arrowBindingString = testInstance.getComputedValue(
|
||||||
|
arrowExpression,
|
||||||
|
"Table1",
|
||||||
|
);
|
||||||
|
const arrowResult =
|
||||||
|
TableCustomSortControl.getInputComputedValue(arrowBindingString);
|
||||||
|
|
||||||
|
expect(arrowResult).toBe(arrowExpression);
|
||||||
|
|
||||||
|
// Test with function body
|
||||||
|
const bodyExpression = `{
|
||||||
|
const direction = order === "asc" ? 1 : -1;
|
||||||
|
return tableData.sort((a, b) => direction * (a[column] - b[column]));
|
||||||
|
}`;
|
||||||
|
const bodyBindingString = testInstance.getComputedValue(
|
||||||
|
`{{${bodyExpression}}}`,
|
||||||
|
"Table1",
|
||||||
|
);
|
||||||
|
const bodyResult =
|
||||||
|
TableCustomSortControl.getInputComputedValue(bodyBindingString);
|
||||||
|
// We need to normalize the whitespace for this comparison
|
||||||
|
const normalizeWhitespace = (str: string) =>
|
||||||
|
str.replace(/\s+/g, " ").trim();
|
||||||
|
|
||||||
|
expect(normalizeWhitespace(bodyResult)).toBe(
|
||||||
|
normalizeWhitespace(`{{${bodyExpression}}}`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("TableCustomSortControl instance methods", () => {
|
||||||
|
const testInstance = new TableCustomSortControl({
|
||||||
|
...requiredParams,
|
||||||
|
theme: EditorTheme.LIGHT,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates correct binding with getComputedValue", () => {
|
||||||
|
const inputValue = "{{tableData.sort((a, b) => a.id - b.id)}}";
|
||||||
|
const result = testInstance.getComputedValue(inputValue, "Table1");
|
||||||
|
|
||||||
|
// Check that the result contains key parts of our expected binding
|
||||||
|
expect(result).toContain(
|
||||||
|
"const originalTableData = Table1.tableData || []",
|
||||||
|
);
|
||||||
|
expect(result).toContain(
|
||||||
|
"const filteredTableData = Table1.filteredTableData || []",
|
||||||
|
);
|
||||||
|
expect(result).toContain("const primaryColumnId = Table1.primaryColumnId");
|
||||||
|
expect(result).toContain(
|
||||||
|
"const getMergedTableData = (originalData, filteredData, primaryId)",
|
||||||
|
);
|
||||||
|
expect(result).toContain(
|
||||||
|
"const mergedTableData = primaryColumnId ? getMergedTableData(originalTableData, filteredTableData, primaryColumnId) : filteredTableData",
|
||||||
|
);
|
||||||
|
expect(result).toContain("tableData.sort((a, b) => a.id - b.id)");
|
||||||
|
expect(result).toContain(
|
||||||
|
"if (Array.isArray(sortedTableData) && primaryColumnId)",
|
||||||
|
);
|
||||||
|
expect(result).toContain('console.error("Error in table custom sort:", e)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles non-binding values correctly in getComputedValue", () => {
|
||||||
|
const plainValue = "plain text";
|
||||||
|
const result = testInstance.getComputedValue(plainValue, "Table1");
|
||||||
|
|
||||||
|
expect(result).toBe(plainValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty string in getComputedValue", () => {
|
||||||
|
const emptyValue = "";
|
||||||
|
const result = testInstance.getComputedValue(emptyValue, "Table1");
|
||||||
|
|
||||||
|
expect(result).toBe(emptyValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly handles original data in computed value", () => {
|
||||||
|
const inputValue =
|
||||||
|
"{{tableData.map(row => ({...row, age: row.__original__.age}))}}";
|
||||||
|
const result = testInstance.getComputedValue(inputValue, "Table1");
|
||||||
|
|
||||||
|
expect(result).toContain(
|
||||||
|
"const originalTableData = Table1.tableData || []",
|
||||||
|
);
|
||||||
|
expect(result).toContain(
|
||||||
|
"const filteredTableData = Table1.filteredTableData || []",
|
||||||
|
);
|
||||||
|
expect(result).toContain("const primaryColumnId = Table1.primaryColumnId");
|
||||||
|
expect(result).toContain(
|
||||||
|
"const getMergedTableData = (originalData, filteredData, primaryId)",
|
||||||
|
);
|
||||||
|
expect(result).toContain(
|
||||||
|
"const mergedTableData = primaryColumnId ? getMergedTableData(originalTableData, filteredTableData, primaryColumnId) : filteredTableData",
|
||||||
|
);
|
||||||
|
expect(result).toContain(
|
||||||
|
"tableData.map(row => ({...row, age: row.__original__.age}))",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("TableCustomSortControl.getControlType", () => {
|
||||||
|
it("returns the correct control type", () => {
|
||||||
|
expect(TableCustomSortControl.getControlType()).toBe("TABLE_CUSTOM_SORT");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
import React from "react";
|
||||||
|
import type { ControlProps } from "./BaseControl";
|
||||||
|
import BaseControl from "./BaseControl";
|
||||||
|
import { StyledDynamicInput } from "./StyledControls";
|
||||||
|
import type {
|
||||||
|
CodeEditorExpected,
|
||||||
|
EditorProps,
|
||||||
|
} from "components/editorComponents/CodeEditor";
|
||||||
|
import {
|
||||||
|
CodeEditorBorder,
|
||||||
|
EditorModes,
|
||||||
|
EditorSize,
|
||||||
|
EditorTheme,
|
||||||
|
TabBehaviour,
|
||||||
|
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||||
|
import type { ColumnProperties } from "widgets/TableWidgetV2/component/Constants";
|
||||||
|
import { isDynamicValue } from "utils/DynamicBindingUtils";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { isString } from "utils/helpers";
|
||||||
|
import { JSToString, stringToJS } from "./utils";
|
||||||
|
import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator";
|
||||||
|
import LazyCodeEditor from "components/editorComponents/LazyCodeEditor";
|
||||||
|
import { bindingHintHelper } from "components/editorComponents/CodeEditor/hintHelpers";
|
||||||
|
import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper";
|
||||||
|
import { CollapseContext } from "pages/Editor/PropertyPane/PropertySection";
|
||||||
|
|
||||||
|
const PromptMessage = styled.span`
|
||||||
|
line-height: 17px;
|
||||||
|
|
||||||
|
> .code-wrapper {
|
||||||
|
font-family: var(--ads-v2-font-family-code);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CurlyBraces = styled.span`
|
||||||
|
color: var(--ads-v2-color-fg);
|
||||||
|
background-color: var(--ads-v2-color-bg-muted);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 0 2px 0 0;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--ads-v2-font-weight-bold);
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface InputTextProp {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (event: React.ChangeEvent<HTMLTextAreaElement> | string) => void;
|
||||||
|
evaluatedValue?: unknown;
|
||||||
|
expected?: CodeEditorExpected;
|
||||||
|
placeholder?: string;
|
||||||
|
dataTreePath?: string;
|
||||||
|
additionalDynamicData: AdditionalDynamicDataTree;
|
||||||
|
theme: EditorTheme;
|
||||||
|
height?: string | number;
|
||||||
|
maxHeight?: string | number;
|
||||||
|
isEditorHidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputText(props: InputTextProp) {
|
||||||
|
const {
|
||||||
|
additionalDynamicData,
|
||||||
|
dataTreePath,
|
||||||
|
evaluatedValue,
|
||||||
|
expected,
|
||||||
|
height,
|
||||||
|
isEditorHidden,
|
||||||
|
maxHeight,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
theme,
|
||||||
|
value,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDynamicInput>
|
||||||
|
<LazyCodeEditor
|
||||||
|
AIAssisted
|
||||||
|
additionalDynamicData={additionalDynamicData}
|
||||||
|
border={CodeEditorBorder.ALL_SIDE}
|
||||||
|
dataTreePath={dataTreePath}
|
||||||
|
evaluatedValue={evaluatedValue}
|
||||||
|
expected={expected}
|
||||||
|
height={height as EditorProps["height"]}
|
||||||
|
hinting={[bindingHintHelper, slashCommandHintHelper]}
|
||||||
|
hoverInteraction
|
||||||
|
input={{
|
||||||
|
value: value,
|
||||||
|
onChange: onChange,
|
||||||
|
}}
|
||||||
|
isEditorHidden={isEditorHidden}
|
||||||
|
maxHeight={maxHeight as EditorProps["maxHeight"]}
|
||||||
|
mode={EditorModes.TEXT_WITH_BINDING}
|
||||||
|
placeholder={placeholder}
|
||||||
|
positionCursorInsideBinding
|
||||||
|
promptMessage={
|
||||||
|
<PromptMessage>
|
||||||
|
Access data using{" "}
|
||||||
|
<span className="code-wrapper">
|
||||||
|
<CurlyBraces>{"{{"}</CurlyBraces>
|
||||||
|
tableData, column, order
|
||||||
|
<CurlyBraces>{"}}"}</CurlyBraces>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
Original data is available inside{" "}
|
||||||
|
<span className="code-wrapper">
|
||||||
|
<CurlyBraces>__original__</CurlyBraces>
|
||||||
|
</span>
|
||||||
|
</PromptMessage>
|
||||||
|
}
|
||||||
|
size={EditorSize.EXTENDED}
|
||||||
|
tabBehaviour={TabBehaviour.INDENT}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</StyledDynamicInput>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableCustomSortControl extends BaseControl<TableCustomSortControlProps> {
|
||||||
|
static contextType = CollapseContext;
|
||||||
|
context!: React.ContextType<typeof CollapseContext>;
|
||||||
|
|
||||||
|
static getTableCustomSortBinding = (
|
||||||
|
tableName: string,
|
||||||
|
stringToEvaluate: string,
|
||||||
|
) => {
|
||||||
|
if (!stringToEvaluate) {
|
||||||
|
return stringToEvaluate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a self-executing function that implements custom table sorting logic.
|
||||||
|
*
|
||||||
|
* State Management:
|
||||||
|
* 1. Initializes with original table data, filtered table data, and primary column ID
|
||||||
|
* 2. If primary column ID is not set, it returns the filtered table data
|
||||||
|
* 3. Creates a mapping function to merge original and filtered data
|
||||||
|
* 4. Processes the data through several states:
|
||||||
|
* - Initial state: Raw table data and filtered data
|
||||||
|
* - Merged state: Combines original and filtered data with "original_" prefixed properties
|
||||||
|
* - Sorted state: Applies user-defined sorting logic from stringToEvaluate
|
||||||
|
* - Cleaned state: Removes temporary "original_" properties from the result
|
||||||
|
*
|
||||||
|
* Error handling is implemented to return filtered data if any step fails.
|
||||||
|
* Empty dataset handling returns empty results immediately.
|
||||||
|
* The function maintains data integrity by preserving the original structure.
|
||||||
|
*/
|
||||||
|
return `{{(() => {
|
||||||
|
if(!${tableName}.sortOrder.column || !${tableName}.sortOrder.order) {
|
||||||
|
return ${tableName}.filteredTableData;
|
||||||
|
}
|
||||||
|
const originalTableData = ${tableName}.tableData || [];
|
||||||
|
const filteredTableData = ${tableName}.filteredTableData || [];
|
||||||
|
const primaryColumnId = ${tableName}.primaryColumnId;
|
||||||
|
const getMergedTableData = (originalData, filteredData, primaryId) => {
|
||||||
|
const originalDataMap = {};
|
||||||
|
originalData.forEach((row) => {
|
||||||
|
originalDataMap[row[primaryId]] = row;
|
||||||
|
});
|
||||||
|
return filteredData.map(row => {
|
||||||
|
return {...row, __original__: originalDataMap[row[primaryId]] || {}};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if(filteredTableData.length === 0) {
|
||||||
|
return filteredTableData;
|
||||||
|
}
|
||||||
|
const mergedTableData = primaryColumnId ? getMergedTableData(originalTableData, filteredTableData, primaryColumnId) : filteredTableData;
|
||||||
|
const sortedTableData = ((tableData, column, order) => (${stringToEvaluate}))(mergedTableData, ${tableName}.sortOrder.column, ${tableName}.sortOrder.order);
|
||||||
|
if (Array.isArray(sortedTableData) && primaryColumnId) {
|
||||||
|
const cleanedData = sortedTableData.map(row => {
|
||||||
|
if (typeof row !== 'object' || row === null) return row;
|
||||||
|
const cleanRow = {...row};
|
||||||
|
delete cleanRow.__original__;
|
||||||
|
return cleanRow;
|
||||||
|
});
|
||||||
|
return cleanedData.length > 0 ? cleanedData : filteredTableData;
|
||||||
|
}
|
||||||
|
return filteredTableData;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error in table custom sort:", e);
|
||||||
|
return ${tableName}.filteredTableData || [];
|
||||||
|
}
|
||||||
|
})()}}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
controlConfig,
|
||||||
|
dataTreePath,
|
||||||
|
defaultValue,
|
||||||
|
expected,
|
||||||
|
label,
|
||||||
|
placeholderText,
|
||||||
|
propertyValue,
|
||||||
|
theme,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// subscribing to context to help re-render component on Property section open or close
|
||||||
|
const isOpen = this.context;
|
||||||
|
|
||||||
|
const value =
|
||||||
|
propertyValue && isDynamicValue(propertyValue)
|
||||||
|
? TableCustomSortControl.getInputComputedValue(propertyValue)
|
||||||
|
: propertyValue
|
||||||
|
? propertyValue
|
||||||
|
: defaultValue;
|
||||||
|
const evaluatedProperties = this.props.widgetProperties;
|
||||||
|
|
||||||
|
const columns: Record<string, ColumnProperties> =
|
||||||
|
evaluatedProperties.primaryColumns || {};
|
||||||
|
const currentRow: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
Object.values(columns).forEach((column) => {
|
||||||
|
currentRow[column.alias || column.originalId] = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load default value in evaluated value
|
||||||
|
if (value && !propertyValue) {
|
||||||
|
this.onTextChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputText
|
||||||
|
additionalDynamicData={{
|
||||||
|
tableData: {} as Record<string, unknown>,
|
||||||
|
column: {} as Record<string, unknown>,
|
||||||
|
order: {} as Record<string, unknown>,
|
||||||
|
}}
|
||||||
|
dataTreePath={dataTreePath}
|
||||||
|
expected={expected}
|
||||||
|
height={controlConfig?.height as EditorProps["height"]}
|
||||||
|
isEditorHidden={!isOpen}
|
||||||
|
label={label}
|
||||||
|
maxHeight={controlConfig?.maxHeight as EditorProps["maxHeight"]}
|
||||||
|
onChange={this.onTextChange}
|
||||||
|
placeholder={placeholderText}
|
||||||
|
theme={theme || EditorTheme.LIGHT}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInputComputedValue = (propertyValue: string) => {
|
||||||
|
// Update the function signature to match the new implementation
|
||||||
|
const FUNCTION_SIGNATURE = "((tableData, column, order) => {";
|
||||||
|
const ALTERNATIVE_SIGNATURE = "((tableData, column, order) => (";
|
||||||
|
|
||||||
|
if (
|
||||||
|
!propertyValue.includes(FUNCTION_SIGNATURE) &&
|
||||||
|
!propertyValue.includes(ALTERNATIVE_SIGNATURE)
|
||||||
|
) {
|
||||||
|
return propertyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let signatureIndex, computationStart;
|
||||||
|
|
||||||
|
if (propertyValue.includes(FUNCTION_SIGNATURE)) {
|
||||||
|
signatureIndex = propertyValue.indexOf(FUNCTION_SIGNATURE);
|
||||||
|
computationStart = signatureIndex + FUNCTION_SIGNATURE.length;
|
||||||
|
|
||||||
|
// Find the matching closing brace
|
||||||
|
let braceCount = 1;
|
||||||
|
let computationEnd = computationStart;
|
||||||
|
|
||||||
|
for (let i = computationStart; i < propertyValue.length; i++) {
|
||||||
|
if (propertyValue[i] === "{") braceCount++;
|
||||||
|
|
||||||
|
if (propertyValue[i] === "}") braceCount--;
|
||||||
|
|
||||||
|
if (braceCount === 0) {
|
||||||
|
computationEnd = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computationEnd === computationStart) return propertyValue;
|
||||||
|
|
||||||
|
// Extract the computation expression
|
||||||
|
const computationExpression = propertyValue.substring(
|
||||||
|
computationStart,
|
||||||
|
computationEnd,
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSToString(computationExpression);
|
||||||
|
} else {
|
||||||
|
signatureIndex = propertyValue.indexOf(ALTERNATIVE_SIGNATURE);
|
||||||
|
computationStart = signatureIndex + ALTERNATIVE_SIGNATURE.length;
|
||||||
|
const computationEnd = propertyValue.indexOf("))(");
|
||||||
|
|
||||||
|
if (computationEnd === -1) return propertyValue;
|
||||||
|
|
||||||
|
// Extract the computation expression
|
||||||
|
const computationExpression = propertyValue.substring(
|
||||||
|
computationStart,
|
||||||
|
computationEnd,
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSToString(computationExpression);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return propertyValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getComputedValue = (value: string, tableName: string) => {
|
||||||
|
if (
|
||||||
|
!isDynamicValue(value) &&
|
||||||
|
!this.props.additionalControlData?.isArrayValue
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringToEvaluate = stringToJS(value);
|
||||||
|
|
||||||
|
if (stringToEvaluate === "") {
|
||||||
|
return stringToEvaluate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TableCustomSortControl.getTableCustomSortBinding(
|
||||||
|
tableName,
|
||||||
|
stringToEvaluate,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onTextChange = (event: React.ChangeEvent<HTMLTextAreaElement> | string) => {
|
||||||
|
let value = "";
|
||||||
|
|
||||||
|
if (typeof event !== "string") {
|
||||||
|
value = event.target?.value;
|
||||||
|
} else {
|
||||||
|
value = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(value)) {
|
||||||
|
const output = this.getComputedValue(
|
||||||
|
value,
|
||||||
|
this.props.widgetProperties.widgetName,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateProperty(this.props.propertyName, output);
|
||||||
|
} else {
|
||||||
|
this.updateProperty(this.props.propertyName, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static getControlType() {
|
||||||
|
return "TABLE_CUSTOM_SORT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableCustomSortControlProps extends ControlProps {
|
||||||
|
defaultValue?: string;
|
||||||
|
placeholderText?: string;
|
||||||
|
controlConfig?: {
|
||||||
|
height?: string | number;
|
||||||
|
maxHeight?: string | number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableCustomSortControl;
|
||||||
|
|
@ -77,6 +77,9 @@ import IconSelectControlV2 from "./IconSelectControlV2";
|
||||||
import PrimaryColumnsControlWDS from "./PrimaryColumnsControlWDS";
|
import PrimaryColumnsControlWDS from "./PrimaryColumnsControlWDS";
|
||||||
import ToolbarButtonListControl from "./ToolbarButtonListControl";
|
import ToolbarButtonListControl from "./ToolbarButtonListControl";
|
||||||
import ArrayControl from "./ArrayControl";
|
import ArrayControl from "./ArrayControl";
|
||||||
|
import TableCustomSortControl, {
|
||||||
|
type TableCustomSortControlProps,
|
||||||
|
} from "./TableCustomSortControl";
|
||||||
|
|
||||||
export const PropertyControls = {
|
export const PropertyControls = {
|
||||||
InputTextControl,
|
InputTextControl,
|
||||||
|
|
@ -130,6 +133,7 @@ export const PropertyControls = {
|
||||||
IconSelectControlV2,
|
IconSelectControlV2,
|
||||||
PrimaryColumnsControlWDS,
|
PrimaryColumnsControlWDS,
|
||||||
ToolbarButtonListControl,
|
ToolbarButtonListControl,
|
||||||
|
TableCustomSortControl,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropertyControlPropsType =
|
export type PropertyControlPropsType =
|
||||||
|
|
@ -158,7 +162,8 @@ export type PropertyControlPropsType =
|
||||||
| WrappedCodeEditorControlProps
|
| WrappedCodeEditorControlProps
|
||||||
| ZoneStepperControlProps
|
| ZoneStepperControlProps
|
||||||
| SectionSplitterControlProps
|
| SectionSplitterControlProps
|
||||||
| IconSelectControlV2Props;
|
| IconSelectControlV2Props
|
||||||
|
| TableCustomSortControlProps;
|
||||||
|
|
||||||
export const getPropertyControlTypes = (): { [key: string]: string } => {
|
export const getPropertyControlTypes = (): { [key: string]: string } => {
|
||||||
const _types: { [key: string]: string } = {};
|
const _types: { [key: string]: string } = {};
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export interface TableWidgetProps
|
||||||
onRowSelected?: string;
|
onRowSelected?: string;
|
||||||
onSearchTextChanged: string;
|
onSearchTextChanged: string;
|
||||||
onSort: string;
|
onSort: string;
|
||||||
|
customSortFunction?: string;
|
||||||
selectedRowIndex?: number;
|
selectedRowIndex?: number;
|
||||||
selectedRowIndices: number[];
|
selectedRowIndices: number[];
|
||||||
serverSidePaginationEnabled?: boolean;
|
serverSidePaginationEnabled?: boolean;
|
||||||
|
|
@ -247,3 +248,6 @@ export const ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING =
|
||||||
|
|
||||||
export const INFINITE_SCROLL_ENABLED =
|
export const INFINITE_SCROLL_ENABLED =
|
||||||
FEATURE_FLAG["release_tablev2_infinitescroll_enabled"];
|
FEATURE_FLAG["release_tablev2_infinitescroll_enabled"];
|
||||||
|
|
||||||
|
export const CUSTOM_SORT_FUNCTION_ENABLED =
|
||||||
|
FEATURE_FLAG["release_table_custom_sort_function_enabled"];
|
||||||
|
|
|
||||||
|
|
@ -1216,6 +1216,7 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
|
||||||
const {
|
const {
|
||||||
customIsLoading,
|
customIsLoading,
|
||||||
customIsLoadingValue,
|
customIsLoadingValue,
|
||||||
|
customSortFunction: customSortFunctionData,
|
||||||
delimiter,
|
delimiter,
|
||||||
filteredTableData = [],
|
filteredTableData = [],
|
||||||
isVisibleDownload,
|
isVisibleDownload,
|
||||||
|
|
@ -1228,7 +1229,14 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const tableColumns = this.getTableColumns() || emptyArr;
|
const tableColumns = this.getTableColumns() || emptyArr;
|
||||||
const transformedData = this.transformData(filteredTableData, tableColumns);
|
let data = filteredTableData;
|
||||||
|
|
||||||
|
if (customSortFunctionData && Array.isArray(customSortFunctionData)) {
|
||||||
|
data = customSortFunctionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedData = this.transformData(data, tableColumns);
|
||||||
|
|
||||||
const isVisibleHeaderOptions =
|
const isVisibleHeaderOptions =
|
||||||
isVisibleDownload ||
|
isVisibleDownload ||
|
||||||
isVisibleFilters ||
|
isVisibleFilters ||
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import { ValidationTypes } from "constants/WidgetValidation";
|
||||||
import { EvaluationSubstitutionType } from "ee/entities/DataTree/types";
|
import { EvaluationSubstitutionType } from "ee/entities/DataTree/types";
|
||||||
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
|
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
|
||||||
import type { TableWidgetProps } from "widgets/TableWidgetV2/constants";
|
import type { TableWidgetProps } from "widgets/TableWidgetV2/constants";
|
||||||
import { ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING } from "../../constants";
|
import {
|
||||||
|
ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING,
|
||||||
|
CUSTOM_SORT_FUNCTION_ENABLED,
|
||||||
|
} from "../../constants";
|
||||||
import { InlineEditingSaveOptions } from "widgets/TableWidgetV2/constants";
|
import { InlineEditingSaveOptions } from "widgets/TableWidgetV2/constants";
|
||||||
import { composePropertyUpdateHook } from "widgets/WidgetUtils";
|
import { composePropertyUpdateHook } from "widgets/WidgetUtils";
|
||||||
import {
|
import {
|
||||||
|
|
@ -409,9 +412,30 @@ export default [
|
||||||
hidden: (props: TableWidgetProps) => !props.isSortable,
|
hidden: (props: TableWidgetProps) => !props.isSortable,
|
||||||
dependencies: ["isSortable"],
|
dependencies: ["isSortable"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
helperText:
|
||||||
|
"Client side only, custom sort function data(overrides default sorting)",
|
||||||
|
helpText:
|
||||||
|
"Function should expect three arguments: tableData, columnId, and order. Return the sorted tableData.",
|
||||||
|
propertyName: "customSortFunction",
|
||||||
|
label: "Custom sort function data",
|
||||||
|
controlType: "TABLE_CUSTOM_SORT",
|
||||||
|
placeholderText:
|
||||||
|
"{{(tableData, columnId, order) => { /* Return sorted table data */ }}}",
|
||||||
|
controlConfig: {
|
||||||
|
maxHeight: "400px",
|
||||||
|
height: "100px",
|
||||||
|
},
|
||||||
|
isTriggerProperty: false,
|
||||||
|
hidden: (props: TableWidgetProps) =>
|
||||||
|
!props.isSortable ||
|
||||||
|
!Widget.getFeatureFlag(CUSTOM_SORT_FUNCTION_ENABLED),
|
||||||
|
dependencies: ["isSortable"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
expandedByDefault: false,
|
expandedByDefault: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
sectionName: "Adding a row",
|
sectionName: "Adding a row",
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user