diff --git a/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/Widgets/TableV2/columnTypes/select_spec.js b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/Widgets/TableV2/columnTypes/select_spec.js index de3634341f..5973408267 100644 --- a/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/Widgets/TableV2/columnTypes/select_spec.js +++ b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/Widgets/TableV2/columnTypes/select_spec.js @@ -3,10 +3,12 @@ const widgetsPage = require("../../../../../../locators/Widgets.json"); import { ObjectsRegistry } from "../../../../../../support/Objects/Registry"; let dataSources = ObjectsRegistry.DataSources; +const propertyPane = ObjectsRegistry.PropertyPane; +const agHelper = ObjectsRegistry.AggregateHelper; describe("Table widget - Select column type functionality", () => { before(() => { - cy.dragAndDropToCanvas("tablewidgetv2", { x: 150, y: 300 }); + cy.dragAndDropToCanvas("tablewidgetv2", { x: 350, y: 500 }); }); it("1. should check that select column is available in the column dropdown options", () => { @@ -197,7 +199,137 @@ describe("Table widget - Select column type functionality", () => { cy.get(".menu-item-text").contains("#1").should("not.exist"); }); - it("8. should check that server side filering is working", () => { + it("8. should check that 'same select option in new row' property is working", () => { + propertyPane.NavigateBackToPropertyPane(); + + const checkSameOptionsInNewRowWhileEditing = () => { + propertyPane.ToggleOnOrOff("Allow adding a row", "On"); + + propertyPane.OpenTableColumnSettings("step"); + + cy.get(".t--property-control-sameoptionsinnewrow label") + .last() + .should("have.class", "checked"); + + // Check if newrowoption is invisible when sameoptionsinnewrow is true + cy.get(".t--property-control-newrowoptions").should("not.exist"); + + cy.updateCodeInput( + ".t--property-control-options", + ` + {{[{ + "label": "male", + "value": "male" + }, + { + "label": "female", + "value": "female" + } + ]}} + `, + ); + + cy.editTableSelectCell(0, 0); + + // Check if the options appear in the table + cy.get(".menu-item-text").contains("male").should("exist"); + cy.get(".menu-item-text").contains("female").should("exist"); + cy.get("[data-colindex=1][data-rowindex=0]").click({ force: true }); + }; + + const checkSameOptionsWhileAddingNewRow = () => { + // Check if the same options appears while adding a new row. + cy.get(".t--add-new-row").scrollIntoView().should("be.visible"); + cy.get(".t--add-new-row").click({ force: true }); + cy.wait(500); + + // Check if new row is visible + cy.get(".tableWrap .new-row").should("be.visible"); + + // Click on the first cell of the table that hasa select dropdown + cy.get( + "[data-colindex=0][data-rowindex=0] [data-testid='selectbutton.btn.main']", + ).click({ force: true }); + + cy.get(".menu-item-text").contains("male").should("exist"); + cy.get(".menu-item-text").contains("female").should("exist"); + + cy.get(".t--discard-new-row").click({ force: true }); + }; + + checkSameOptionsInNewRowWhileEditing(); + + cy.wait(500); + + checkSameOptionsWhileAddingNewRow(); + }); + + it("9. should check that 'new row select options' is working", () => { + const checkNewRowOptions = () => { + // New row select options should be visible when "Same options in new row" is turned off + propertyPane.ToggleOnOrOff("Same options in new row", "Off"); + cy.get(".t--property-control-newrowoptions").should("exist"); + + // New row select options should appear in table + cy.updateCodeInput( + ".t--property-control-newrowoptions", + ` + [{"label": "abc", "value": "abc"}] + `, + ); + cy.get(".t--add-new-row").scrollIntoView().should("be.visible"); + cy.get(".t--add-new-row").click({ force: true }); + cy.get(".tableWrap .new-row").should("exist"); + cy.get( + "[data-colindex=0][data-rowindex=0] [data-testid='selectbutton.btn.main']", + ).click({ force: true }); + + cy.get(".menu-item-text").contains("abc").should("exist"); + }; + + const isCurrentRowAccessDisabled = () => { + // New row select options should not have access to the currentRow + cy.updateCodeInput( + ".t--property-control-newrowoptions", + "{{currentRow}}", + ); + agHelper.VerifyEvaluatedErrorMessage("currentRow is not defined"); + }; + + const checkDynamicBindingSupport = () => { + //New row selection options should support dynamic values + cy.updateCodeInput( + ".t--property-control-newrowoptions", + ` + {{[{"label": "abc1", "value": "abc1"}]}} + `, + ); + + cy.get( + "[data-colindex=0][data-rowindex=0] [data-testid='selectbutton.btn.main']", + ).click({ force: true }); + + cy.get(".menu-item-text").contains("abc1").should("exist"); + }; + + const checkNoOptionState = () => { + // Check that no select options are present when new row options are cleared: + cy.updateCodeInput(".t--property-control-newrowoptions", ``); // Clear the field + cy.get( + "[data-colindex=0][data-rowindex=0] [data-testid='selectbutton.btn.main']", + ).click({ force: true }); + cy.get(".menu-item-text").contains("No Results Found").should("exist"); + + cy.get(".t--discard-new-row").click({ force: true }); + }; + + checkNewRowOptions(); + isCurrentRowAccessDisabled(); + checkDynamicBindingSupport(); + checkNoOptionState(); + }); + + it("10. should check that server side filering is working", () => { dataSources.CreateDataSource("Postgres"); dataSources.CreateQueryAfterDSSaved( "SELECT * FROM public.astronauts {{this.params.filterText ? `WHERE name LIKE '%${this.params.filterText}%'` : ''}} LIMIT 10;", diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 9dac0987bd..f9071012b4 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -70,7 +70,7 @@ export const layoutConfigurations: LayoutConfigurations = { FLUID: { minWidth: -1, maxWidth: -1 }, }; -export const LATEST_PAGE_VERSION = 77; +export const LATEST_PAGE_VERSION = 78; export const GridDefaults = { DEFAULT_CELL_SIZE: 1, diff --git a/app/client/src/utils/DSLMigration.test.ts b/app/client/src/utils/DSLMigration.test.ts index 6287c23035..23c41b9902 100644 --- a/app/client/src/utils/DSLMigration.test.ts +++ b/app/client/src/utils/DSLMigration.test.ts @@ -745,6 +745,15 @@ const migrations: Migration[] = [ ], version: 76, }, + { + functionLookup: [ + { + moduleObj: tableMigrations, + functionName: "migrateTableSelectOptionAttributesForNewRow", + }, + ], + version: 77, + }, ]; const mockFnObj: Record = {}; diff --git a/app/client/src/utils/DSLMigrations.ts b/app/client/src/utils/DSLMigrations.ts index 802b964c4d..ceb7773cfa 100644 --- a/app/client/src/utils/DSLMigrations.ts +++ b/app/client/src/utils/DSLMigrations.ts @@ -26,6 +26,7 @@ import { migrateMenuButtonDynamicItemsInsideTableWidget, migrateTableWidgetV2SelectOption, migrateColumnFreezeAttributes, + migrateTableSelectOptionAttributesForNewRow, } from "./migrations/TableWidget"; import { migrateTextStyleFromTextWidget, @@ -1168,6 +1169,11 @@ export const transformDSL = (currentDSL: DSLWidget, newPage = false) => { if (currentDSL.version === 76) { currentDSL = migrateColumnFreezeAttributes(currentDSL); + currentDSL.version = 77; + } + + if (currentDSL.version === 77) { + currentDSL = migrateTableSelectOptionAttributesForNewRow(currentDSL); currentDSL.version = LATEST_PAGE_VERSION; } diff --git a/app/client/src/utils/migrations/TableWidget.ts b/app/client/src/utils/migrations/TableWidget.ts index 8016fca6cf..add3eb33eb 100644 --- a/app/client/src/utils/migrations/TableWidget.ts +++ b/app/client/src/utils/migrations/TableWidget.ts @@ -22,7 +22,10 @@ import { getSubstringBetweenTwoWords } from "utils/helpers"; import { traverseDSLAndMigrate } from "utils/WidgetMigrationUtils"; import { isDynamicValue } from "utils/DynamicBindingUtils"; import { stringToJS } from "components/editorComponents/ActionCreator/utils"; -import { StickyType } from "widgets/TableWidgetV2/component/Constants"; +import { + type ColumnProperties as ColumnPropertiesV2, + StickyType, +} from "widgets/TableWidgetV2/component/Constants"; export const isSortableMigration = (currentDSL: DSLWidget) => { currentDSL.children = currentDSL.children?.map((child: WidgetProps) => { @@ -713,3 +716,25 @@ export const migrateColumnFreezeAttributes = (currentDSL: DSLWidget) => { } }); }; + +export const migrateTableSelectOptionAttributesForNewRow = ( + currentDSL: DSLWidget, +) => { + return traverseDSLAndMigrate(currentDSL, (widget: WidgetProps) => { + if (widget.type === "TABLE_WIDGET_V2") { + const primaryColumns = widget?.primaryColumns as ColumnPropertiesV2; + + // Set default value for allowSameOptionsInNewRow + if (primaryColumns) { + Object.values(primaryColumns).forEach((column) => { + if ( + column.hasOwnProperty("columnType") && + column.columnType === "select" + ) { + column.allowSameOptionsInNewRow = true; + } + }); + } + } + }); +}; diff --git a/app/client/src/widgets/TableWidgetV2/component/Constants.ts b/app/client/src/widgets/TableWidgetV2/component/Constants.ts index 78cfbf6219..280bcadeac 100644 --- a/app/client/src/widgets/TableWidgetV2/component/Constants.ts +++ b/app/client/src/widgets/TableWidgetV2/component/Constants.ts @@ -330,6 +330,8 @@ export interface ColumnProperties DateColumnProperties, ColumnEditabilityProperties, EditActionColumnProperties { + allowSameOptionsInNewRow?: boolean; + newRowSelectOptions?: DropdownOption[]; buttonLabel?: string; menuButtonLabel?: string; buttonColor?: string; diff --git a/app/client/src/widgets/TableWidgetV2/index.ts b/app/client/src/widgets/TableWidgetV2/index.ts index a248229cd0..296a53a806 100644 --- a/app/client/src/widgets/TableWidgetV2/index.ts +++ b/app/client/src/widgets/TableWidgetV2/index.ts @@ -71,6 +71,7 @@ export const CONFIG = { id: "step", originalId: "step", alias: "step", + allowSameOptionsInNewRow: true, horizontalAlignment: "LEFT", verticalAlignment: "CENTER", columnType: "text", @@ -92,6 +93,7 @@ export const CONFIG = { id: "task", originalId: "task", alias: "task", + allowSameOptionsInNewRow: true, horizontalAlignment: "LEFT", verticalAlignment: "CENTER", columnType: "text", @@ -113,6 +115,7 @@ export const CONFIG = { id: "status", originalId: "status", alias: "status", + allowSameOptionsInNewRow: true, horizontalAlignment: "LEFT", verticalAlignment: "CENTER", columnType: "text", @@ -134,6 +137,7 @@ export const CONFIG = { id: "action", originalId: "action", alias: "action", + allowSameOptionsInNewRow: true, horizontalAlignment: "LEFT", verticalAlignment: "CENTER", columnType: "button", diff --git a/app/client/src/widgets/TableWidgetV2/widget/index.tsx b/app/client/src/widgets/TableWidgetV2/widget/index.tsx index 61be5bc9df..355dc4b828 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/index.tsx @@ -1434,11 +1434,13 @@ class TableWidgetV2 extends BaseWidget { originalIndex = row[ORIGINAL_INDEX_KEY] ?? rowIndex; } + const isNewRow = this.props.isAddRowInProgress && rowIndex === 0; + /* * cellProperties order or size does not change when filter/sorting/grouping is applied * on the data thus original index is needed to identify the column's cell property. */ - const cellProperties = getCellProperties(column, originalIndex); + const cellProperties = getCellProperties(column, originalIndex, isNewRow); let isSelected = false; if (this.props.transientTableData) { @@ -1460,8 +1462,6 @@ class TableWidgetV2 extends BaseWidget { column.isEditable && isColumnTypeEditable(column.columnType); const alias = props.cell.column.columnProperties.alias; - const isNewRow = this.props.isAddRowInProgress && rowIndex === 0; - const isCellEditable = isColumnEditable && cellProperties.isCellEditable; const isCellEditMode = diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Select.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Select.ts index 8178193c6e..a74cfc7752 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Select.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Select.ts @@ -1,7 +1,9 @@ import { ValidationTypes } from "constants/WidgetValidation"; +import { get } from "lodash"; import type { TableWidgetProps } from "widgets/TableWidgetV2/constants"; import { ColumnTypes } from "widgets/TableWidgetV2/constants"; import { + getBasePropertyPath, hideByColumnType, selectColumnOptionsValidation, } from "../../propertyUtils"; @@ -35,6 +37,64 @@ export default { return hideByColumnType(props, propertyPath, [ColumnTypes.SELECT]); }, }, + { + propertyName: "allowSameOptionsInNewRow", + defaultValue: true, + helpText: + "Toggle to display same choices for new row and editing existing row in column", + label: "Same options in new row", + controlType: "SWITCH", + isBindProperty: true, + isJSConvertible: true, + isTriggerProperty: false, + hidden: (props: TableWidgetProps) => { + return !props.allowAddNewRow; + }, + dependencies: ["primaryColumns", "allowAddNewRow"], + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "newRowSelectOptions", + helpText: + "Options exclusively displayed in the column for new row addition", + label: "New row options", + controlType: "INPUT_TEXT", + isJSConvertible: false, + isBindProperty: true, + validation: { + type: ValidationTypes.FUNCTION, + params: { + expected: { + type: 'Array<{ "label": string | number, "value": string | number}>', + example: '[{"label": "abc", "value": "abc"}]', + }, + fnString: selectColumnOptionsValidation.toString(), + }, + }, + isTriggerProperty: false, + dependencies: ["primaryColumns", "allowAddNewRow"], + hidden: (props: TableWidgetProps, propertyPath: string) => { + const baseProperty = getBasePropertyPath(propertyPath); + + if (baseProperty) { + const columnType = get(props, `${baseProperty}.columnType`, ""); + const allowSameOptionsInNewRow = get( + props, + `${baseProperty}.allowSameOptionsInNewRow`, + ); + + if ( + columnType === ColumnTypes.SELECT && + props.allowAddNewRow && + !allowSameOptionsInNewRow + ) { + return false; + } else { + return true; + } + } + }, + }, { propertyName: "placeholderText", helpText: "Sets a Placeholder Text", diff --git a/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts b/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts index a21ce0a69a..111561969a 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts @@ -10,6 +10,7 @@ import { getDerivedColumns, getHeaderClassNameOnDragDirection, getOriginalRowIndex, + getSelectOptions, getSelectRowIndex, getSelectRowIndices, getSourceDataAndCaluclateKeysForEventAutoComplete, @@ -2553,3 +2554,120 @@ describe("getHeaderClassNameOnDragDirection", () => { ); }); }); + +describe("getSelectOptions", () => { + it("Should return select options when user is not adding a new row", () => { + const columnProperties = { + allowSameOptionsInNewRow: true, + selectOptions: [ + { + label: "male", + value: "male", + }, + { + label: "female", + value: "female", + }, + ], + }; + expect( + getSelectOptions(false, 0, columnProperties as ColumnProperties), + ).toEqual([ + { + label: "male", + value: "male", + }, + { + label: "female", + value: "female", + }, + ]); + + // Check when select options are inside dynamic binding + const columnPropertiesDynamicSelectOptions = { + allowSameOptionsInNewRow: true, + selectOptions: [ + [ + { + label: "abc", + value: "abc", + }, + ], + [ + { + label: "efg", + value: "efg", + }, + ], + [ + { + label: "xyz", + value: "xyz", + }, + ], + ], + }; + expect( + getSelectOptions( + false, + 0, + columnPropertiesDynamicSelectOptions as ColumnProperties, + ), + ).toEqual([ + { + label: "abc", + value: "abc", + }, + ]); + }); + + it("Should return select options while adding a new row and when 'Same options in new row' option is turned on", () => { + const columnProperties = { + allowSameOptionsInNewRow: true, + selectOptions: [ + { + label: "male", + value: "male", + }, + { + label: "female", + value: "female", + }, + ], + }; + + expect( + getSelectOptions(true, -1, columnProperties as ColumnProperties), + ).toEqual([ + { + label: "male", + value: "male", + }, + { + label: "female", + value: "female", + }, + ]); + }); + + it("Should return new row options", () => { + const columnProperties = { + allowSameOptionsInNewRow: false, + newRowSelectOptions: [ + { + label: "abc", + value: "abc", + }, + ], + }; + + expect( + getSelectOptions(true, -1, columnProperties as ColumnProperties), + ).toEqual([ + { + label: "abc", + value: "abc", + }, + ]); + }); +}); diff --git a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts index b56db8a3f1..8c382102ca 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts @@ -188,6 +188,7 @@ export function getDefaultColumnProperties( ): ColumnProperties { const columnProps = { allowCellWrapping: false, + allowSameOptionsInNewRow: true, index: index, width: DEFAULT_COLUMN_WIDTH, originalId: id, @@ -293,6 +294,7 @@ export const getArrayPropertyValue = (value: unknown, index: number) => { export const getCellProperties = ( columnProperties: ColumnProperties, rowIndex: number, + isAddRowInProgress = false, ) => { if (columnProperties) { return { @@ -493,9 +495,10 @@ export const getCellProperties = ( true, ), shortcuts: getBooleanPropertyValue(columnProperties.shortcuts, rowIndex), - selectOptions: getArrayPropertyValue( - columnProperties.selectOptions, + selectOptions: getSelectOptions( + isAddRowInProgress, rowIndex, + columnProperties, ), timePrecision: getPropertyValue( columnProperties.timePrecision, @@ -1102,3 +1105,23 @@ export const getDragHandlers = ( onDrop, }; }; + +export const getSelectOptions = ( + isNewRow: boolean, + rowIndex: number, + columnProperties: ColumnProperties, +) => { + if (isNewRow) { + if ( + columnProperties.allowSameOptionsInNewRow && + columnProperties?.selectOptions + ) { + // Use select options from the first row + return getArrayPropertyValue(columnProperties.selectOptions, 0); + } else { + return columnProperties.newRowSelectOptions; + } + } else { + return getArrayPropertyValue(columnProperties.selectOptions, rowIndex); + } +};