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_tablev2_infinitescroll_enabled:
|
||||
"release_tablev2_infinitescroll_enabled",
|
||||
release_table_custom_sort_function_enabled:
|
||||
"release_table_custom_sort_function_enabled",
|
||||
} as const;
|
||||
|
||||
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_external_saas_plugins_enabled: false,
|
||||
release_tablev2_infinitescroll_enabled: false,
|
||||
release_table_custom_sort_function_enabled: false,
|
||||
};
|
||||
|
||||
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 ToolbarButtonListControl from "./ToolbarButtonListControl";
|
||||
import ArrayControl from "./ArrayControl";
|
||||
import TableCustomSortControl, {
|
||||
type TableCustomSortControlProps,
|
||||
} from "./TableCustomSortControl";
|
||||
|
||||
export const PropertyControls = {
|
||||
InputTextControl,
|
||||
|
|
@ -130,6 +133,7 @@ export const PropertyControls = {
|
|||
IconSelectControlV2,
|
||||
PrimaryColumnsControlWDS,
|
||||
ToolbarButtonListControl,
|
||||
TableCustomSortControl,
|
||||
};
|
||||
|
||||
export type PropertyControlPropsType =
|
||||
|
|
@ -158,7 +162,8 @@ export type PropertyControlPropsType =
|
|||
| WrappedCodeEditorControlProps
|
||||
| ZoneStepperControlProps
|
||||
| SectionSplitterControlProps
|
||||
| IconSelectControlV2Props;
|
||||
| IconSelectControlV2Props
|
||||
| TableCustomSortControlProps;
|
||||
|
||||
export const getPropertyControlTypes = (): { [key: string]: string } => {
|
||||
const _types: { [key: string]: string } = {};
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export interface TableWidgetProps
|
|||
onRowSelected?: string;
|
||||
onSearchTextChanged: string;
|
||||
onSort: string;
|
||||
customSortFunction?: string;
|
||||
selectedRowIndex?: number;
|
||||
selectedRowIndices: number[];
|
||||
serverSidePaginationEnabled?: boolean;
|
||||
|
|
@ -247,3 +248,6 @@ export const ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING =
|
|||
|
||||
export const INFINITE_SCROLL_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 {
|
||||
customIsLoading,
|
||||
customIsLoadingValue,
|
||||
customSortFunction: customSortFunctionData,
|
||||
delimiter,
|
||||
filteredTableData = [],
|
||||
isVisibleDownload,
|
||||
|
|
@ -1228,7 +1229,14 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
|
|||
} = this.props;
|
||||
|
||||
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 =
|
||||
isVisibleDownload ||
|
||||
isVisibleFilters ||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import { ValidationTypes } from "constants/WidgetValidation";
|
|||
import { EvaluationSubstitutionType } from "ee/entities/DataTree/types";
|
||||
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
|
||||
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 { composePropertyUpdateHook } from "widgets/WidgetUtils";
|
||||
import {
|
||||
|
|
@ -409,9 +412,30 @@ export default [
|
|||
hidden: (props: TableWidgetProps) => !props.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,
|
||||
},
|
||||
|
||||
{
|
||||
sectionName: "Adding a row",
|
||||
children: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user