diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/Date_column_types_validation_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/Date_column_types_validation_spec.ts new file mode 100644 index 00000000000..0ce8bc321a3 --- /dev/null +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/Date_column_types_validation_spec.ts @@ -0,0 +1,149 @@ +import { tableDateColumnTypes } from "../../../../../fixtures/tableDateColumnTypes"; +import { + agHelper, + entityExplorer, + propPane, + table, +} from "../../../../../support/Objects/ObjectsCore"; + +import EditorNavigation, { + EntityType, +} from "../../../../../support/Pages/EditorNavigation"; + +describe( + "Table widget date column type validation", + { tags: ["@tag.Widget", "@tag.Table"] }, + () => { + before(() => { + entityExplorer.DragNDropWidget("tablewidgetv2", 350, 500); + EditorNavigation.SelectEntityByName("Table1", EntityType.Widget); + propPane.ToggleJSMode("Table data", true); + propPane.UpdatePropertyFieldValue("Table data", tableDateColumnTypes); + table.EditColumn("unixs", "v2"); + }); + + beforeEach(() => { + propPane.NavigateBackToPropertyPane(false); + }); + + const setEditableDateFormats = (format: string) => { + // Update date format property + propPane.ToggleJSMode("Date format", true); + propPane.UpdatePropertyFieldValue("Date format", format); + + // Update display format property + propPane.ToggleJSMode("Display format", true); + propPane.UpdatePropertyFieldValue("Display format", "YYYY-MM-DD"); + + // Toggle editable + propPane.TogglePropertyState("Editable", "On"); + }; + + const clickAndValidateDateCell = (row: number, column: number) => { + // Click unix cell edit + table.ClickOnEditIcon(row, column); + + // Click on specific date within + agHelper.GetNClick( + `${table._dateInputPopover} [aria-label='${table.getFormattedTomorrowDates().verboseFormat}']`, + ); + + // Check that date is set in column + table + .ReadTableRowColumnData(row, column, "v2") + .then((val) => + expect(val).to.equal(table.getFormattedTomorrowDates().isoFormat), + ); + }; + + it("1. should allow inline editing of Unix Timestamp in seconds (unix/s)", () => { + table.ChangeColumnType("unixs", "Date"); + setEditableDateFormats("Epoch"); + clickAndValidateDateCell(0, 0); + }); + + it("2. should allow inline editing of Unix Timestamp in milliseconds (unix/ms)", () => { + table.ChangeColumnType("unixms", "Date"); + setEditableDateFormats("Milliseconds"); + clickAndValidateDateCell(0, 1); + }); + + it("3. should allow inline editing of date in YYYY-MM-DD format", () => { + table.EditColumn("yyyymmdd", "v2"); + setEditableDateFormats("YYYY-MM-DD"); + clickAndValidateDateCell(0, 2); + }); + + it("4. should allow inline editing of date in YYYY-MM-DD HH:mm format", () => { + table.EditColumn("yyyymmddhhmm", "v2"); + setEditableDateFormats("YYYY-MM-DD HH:mm"); + clickAndValidateDateCell(0, 3); + }); + + it("5. should allow inline editing of date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss)", () => { + table.EditColumn("iso8601", "v2"); + setEditableDateFormats("YYYY-MM-DDTHH:mm:ss"); + clickAndValidateDateCell(0, 4); + }); + + it("6. should allow inline editing of date in YYYY-MM-DD HH:mm format", () => { + table.EditColumn("yyyymmddTHHmmss", "v2"); + setEditableDateFormats("YYYY-MM-DD HH:mm"); + clickAndValidateDateCell(0, 5); + }); + + it("7. should allow inline editing of date in 'do MMM yyyy' format", () => { + table.ChangeColumnType("yyyymmddhhmmss", "Date"); + setEditableDateFormats("YYYY-MM-DDTHH:mm:ss"); + clickAndValidateDateCell(0, 6); + }); + + it("8. should allow inline editing of date in DD/MM/YYYY format", () => { + table.ChangeColumnType("doMMMyyyy", "Date"); + setEditableDateFormats("Do MMM YYYY"); + clickAndValidateDateCell(0, 7); + }); + + it("9. should allow inline editing of date in DD/MM/YYYY HH:mm format", () => { + table.EditColumn("ddmmyyyy", "v2"); + setEditableDateFormats("DD/MM/YYYY"); + clickAndValidateDateCell(0, 8); + }); + + it("10. should allow inline editing of date in LLL (Month Day, Year Time) format", () => { + table.EditColumn("ddmmyyyyhhmm", "v2"); + setEditableDateFormats("DD/MM/YYYY HH:mm"); + clickAndValidateDateCell(0, 9); + }); + + it("11. should allow inline editing of date in LL (Month Day, Year) format", () => { + table.EditColumn("lll", "v2"); + setEditableDateFormats("LLL"); + clickAndValidateDateCell(0, 10); + }); + + it("12. should allow inline editing of date in 'D MMMM, YYYY' format", () => { + table.EditColumn("ll", "v2"); + setEditableDateFormats("LL"); + clickAndValidateDateCell(0, 11); + }); + + it("13. should allow inline editing of date in 'h:mm A D MMMM, YYYY' format", () => { + table.EditColumn("dmmmmyyyy", "v2"); + setEditableDateFormats("D MMMM, YYYY"); + clickAndValidateDateCell(0, 12); + }); + + it("14. should allow inline editing of date in MM-DD-YYYY format", () => { + table.EditColumn("hmmAdmmmmyyyy", "v2"); + setEditableDateFormats("H:mm A D MMMM, YYYY"); + clickAndValidateDateCell(0, 13); + }); + + it("15. should allow inline editing of date in DD-MM-YYYY format", () => { + table.EditColumn("mm1dd1yyyy", "v2"); + setEditableDateFormats("MM-DD-YYYY"); + clickAndValidateDateCell(0, 14); + }); + }, +); diff --git a/app/client/cypress/fixtures/tableDateColumnTypes.ts b/app/client/cypress/fixtures/tableDateColumnTypes.ts new file mode 100644 index 00000000000..614b36b5e11 --- /dev/null +++ b/app/client/cypress/fixtures/tableDateColumnTypes.ts @@ -0,0 +1,26 @@ +export const tableDateColumnTypes = ` +{{ + [ + { + "unixs": 1727212200, + "unixms": 1727212200000, + "yyyymmdd": "2024-09-25", + "yyyymmddhhmm": "2024-09-25 14:30", + iso8601: "2024-09-25T14:30:00.000Z", + "yyyymmddTHHmmss": "2024-09-25T14:30:00", + "yyyymmddhhmmss": "2024-09-25 02:30:00", + "doMMMyyyy": "25th Sep 2024", + "ddmmyyyy": "25/09/2024", + "ddmmyyyyhhmm": "25/09/2024 14:30", + lll: "September 25, 2024 2:30 PM", + ll: "September 25, 2024", + "dmmmmyyyy": "25 September, 2024", + "hmmAdmmmmyyyy": "2:30 PM 25 September, 2024", + "mm1dd1yyyy": "09-25-2024", + "dd1mm1yyyy": "25-09-2024", + "ddimmiyy": "25/09/24", + "mmddyy": "09/25/24", + }, + ] +}} +`; diff --git a/app/client/cypress/support/Pages/Table.ts b/app/client/cypress/support/Pages/Table.ts index 2f90efb052c..5ca57eeb5b4 100644 --- a/app/client/cypress/support/Pages/Table.ts +++ b/app/client/cypress/support/Pages/Table.ts @@ -848,4 +848,38 @@ export class Table { this.agHelper.GetHoverNClick(selector, 1, true); verify && cy.get(selector).eq(1).should("be.disabled"); } + + /** + * Helper function to get formatted date strings for tomorrow's date. + * + * @returns {Object} An object containing: + * - verbose format (e.g., "Sat Sep 21 2024") + * - ISO date format (e.g., "2024-09-21") + */ + public getFormattedTomorrowDates() { + // Create a new Date object for today + const tomorrow = new Date(); + + // Set the date to tomorrow by adding 1 to today's date + tomorrow.setDate(tomorrow.getDate() + 1); + + // Format tomorrow's date in verbose form (e.g., "Sat Sep 21 2024") + const verboseFormat = tomorrow + .toLocaleDateString("en-US", { + weekday: "short", + year: "numeric", + month: "short", + day: "2-digit", + }) + .replace(/,/g, ""); // Remove commas from the formatted string + + // Format tomorrow's date in ISO form (e.g., "2024-09-21") + const isoFormat = tomorrow.toISOString().split("T")[0]; // Extract the date part only + + // Return both formatted date strings as an object + return { + verboseFormat, + isoFormat, + }; + } } diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/DateCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/DateCell.tsx index 17f5c679bf4..8e34d928d86 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/DateCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/DateCell.tsx @@ -9,7 +9,11 @@ import DateComponent from "widgets/DatePickerWidget2/component"; import { TimePrecision } from "widgets/DatePickerWidget2/constants"; import type { RenderDefaultPropsType } from "./PlainTextCell"; import styled from "styled-components"; -import { EditableCellActions } from "widgets/TableWidgetV2/constants"; +import { + DateInputFormat, + EditableCellActions, + MomentDateInputFormat, +} from "widgets/TableWidgetV2/constants"; import { ISO_DATE_FORMAT } from "constants/WidgetValidation"; import moment from "moment"; import { BasicCell } from "./BasicCell"; @@ -196,6 +200,19 @@ export const DateCell = (props: DateComponentProps) => { const [isValid, setIsValid] = useState(true); const [showRequiredError, setShowRequiredError] = useState(false); const contentRef = useRef(null); + + const convertInputFormatToMomentFormat = (inputFormat: string) => { + let momentAdjustedInputFormat = inputFormat; + + if (inputFormat === DateInputFormat.MILLISECONDS) { + momentAdjustedInputFormat = MomentDateInputFormat.MILLISECONDS; + } else if (inputFormat === DateInputFormat.EPOCH) { + momentAdjustedInputFormat = MomentDateInputFormat.SECONDS; + } + + return momentAdjustedInputFormat; + }; + const isCellCompletelyValid = useMemo( () => isEditableCellValid && isValid, [isEditableCellValid, isValid], @@ -218,8 +235,15 @@ export const DateCell = (props: DateComponentProps) => { }, [value, props.outputFormat]); const onDateSelected = (date: string) => { + const momentAdjustedInputFormat = + convertInputFormatToMomentFormat(inputFormat); + + const formattedDate = date + ? moment(date).format(momentAdjustedInputFormat) + : ""; + if (isNewRow) { - updateNewRowValues(alias, date, date); + updateNewRowValues(alias, date, formattedDate); return; } @@ -235,8 +259,6 @@ export const DateCell = (props: DateComponentProps) => { setShowRequiredError(false); setHasFocus(false); - const formattedDate = date ? moment(date).format(inputFormat) : ""; - onDateSave(rowIndex, alias, formattedDate, onDateSelectedString); }; diff --git a/app/client/src/widgets/TableWidgetV2/constants.ts b/app/client/src/widgets/TableWidgetV2/constants.ts index 0e066c83f9a..8fb0e8605aa 100644 --- a/app/client/src/widgets/TableWidgetV2/constants.ts +++ b/app/client/src/widgets/TableWidgetV2/constants.ts @@ -219,6 +219,11 @@ export enum DateInputFormat { MILLISECONDS = "Milliseconds", } +export enum MomentDateInputFormat { + MILLISECONDS = "x", + SECONDS = "X", +} + export const defaultEditableCell: EditableCell = { column: "", index: -1, diff --git a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/TransformDataPureFn.test.ts b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/TransformDataPureFn.test.ts new file mode 100644 index 00000000000..d95d92df657 --- /dev/null +++ b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/TransformDataPureFn.test.ts @@ -0,0 +1,54 @@ +import type { ReactTableColumnProps } from "widgets/TableWidgetV2/component/Constants"; +import { + columns, + columnsNonDate, + expectedDataNonDate, + tableDataNonDate, +} from "./fixtures"; +import { transformDataPureFn } from "./transformDataPureFn"; + +describe("transformDataPureFn", () => { + it("should handle invalid date values", () => { + const invalidTableData = [ + { + epoch: "invalid_epoch", + milliseconds: "invalid_milliseconds", + iso_8601: "invalid_iso_8601", + yyyy_mm_dd: "invalid_date", + lll: "invalid_date", + }, + ]; + + const expectedInvalidData = [ + { + epoch: "Invalid date", + milliseconds: "Invalid date", + iso_8601: "8601-01-01", + yyyy_mm_dd: "Invalid date", + lll: "Invalid date", + }, + ]; + + const result = transformDataPureFn( + invalidTableData, + columns as ReactTableColumnProps[], + ); + + expect(result).toEqual(expectedInvalidData); + }); + + it("should return an empty array when tableData is empty", () => { + const result = transformDataPureFn([], columns as ReactTableColumnProps[]); + + expect(result).toEqual([]); + }); + + it("should not transform non-date data", () => { + const result = transformDataPureFn( + tableDataNonDate, + columnsNonDate as ReactTableColumnProps[], + ); + + expect(result).toEqual(expectedDataNonDate); + }); +}); diff --git a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/fixtures.ts b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/fixtures.ts new file mode 100644 index 00000000000..75355f2f4b9 --- /dev/null +++ b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/fixtures.ts @@ -0,0 +1,145 @@ +import { ColumnTypes, DateInputFormat } from "widgets/TableWidgetV2/constants"; + +// Mock columns data +export const columns = [ + { + alias: "epoch", + metaProperties: { + type: ColumnTypes.DATE, + inputFormat: DateInputFormat.EPOCH, + format: "YYYY-MM-DD", + }, + }, + { + alias: "milliseconds", + metaProperties: { + type: ColumnTypes.DATE, + inputFormat: DateInputFormat.MILLISECONDS, + format: "YYYY-MM-DD", + }, + }, + { + alias: "iso_8601", + metaProperties: { + type: ColumnTypes.DATE, + inputFormat: "YYYY-MM-DDTHH:mm:ss.SSSZ", + format: "YYYY-MM-DD", + }, + }, + { + alias: "yyyy_mm_dd", + metaProperties: { + type: ColumnTypes.DATE, + inputFormat: "YYYY-MM-DD", + format: "YYYY-MM-DD", + }, + }, + { + alias: "lll", + metaProperties: { + type: ColumnTypes.DATE, + inputFormat: "LLL", + format: "YYYY-MM-DD", + }, + }, +]; + +// Mock table data +export const tableData = [ + { + epoch: 1727132400, + milliseconds: 1727132400000, + iso_8601: "2024-09-24T00:00:00.000+01:00", + yyyy_mm_dd: "2024-09-24", + lll: "September 25, 2024 12:00 AM", + }, + { + epoch: 1726980974, + milliseconds: 1726980974328, + iso_8601: "2024-09-23T09:01:53.350627", + yyyy_mm_dd: "2024-09-23", + lll: "Sep 23, 2024 09:01", + }, +]; + +// Expected transformed data +export const expectedData = [ + { + epoch: "2024-09-24", // Converted from epoch to date + milliseconds: "2024-09-24", // Converted from milliseconds to date + iso_8601: "2024-09-24", // ISO 8601 to date + yyyy_mm_dd: "2024-09-24", // No transformation needed + lll: "2024-09-25", // LLL format to date + }, + { + epoch: "2024-09-22", // Converted from epoch to date + milliseconds: "2024-09-22", // Converted from milliseconds to date + iso_8601: "2024-09-23", // ISO 8601 to date + yyyy_mm_dd: "2024-09-23", // No transformation needed + lll: "2024-09-23", // LLL format to date + }, +]; + +// Mock columns for non-date data +export const columnsNonDate = [ + { + id: "role", + alias: "role", + metaProperties: { + type: ColumnTypes.NUMBER, + format: "", + inputFormat: "", + decimals: 0, + }, + }, + { + id: "id", + alias: "id", + metaProperties: { + type: ColumnTypes.NUMBER, + format: "", + inputFormat: "", + decimals: 0, + }, + }, + { + id: "name", + alias: "name", + metaProperties: { + type: ColumnTypes.TEXT, + format: "", + inputFormat: "", + decimals: 0, + }, + }, +]; + +// Mock table data for non-date transformation +export const tableDataNonDate = [ + { + role: 1, + id: 1, + name: "Alice Johnson", + __originalIndex__: 0, + }, + { + role: 2, + id: 2, + name: "Bob Smith", + __originalIndex__: 1, + }, +]; + +// Expected transformed data for non-date columns +export const expectedDataNonDate = [ + { + role: 1, + id: 1, + name: "Alice Johnson", + }, + { + role: 2, + id: 2, + name: "Bob Smith", + }, +]; diff --git a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx index 4432568c620..1454694ce8b 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx @@ -1,7 +1,7 @@ import log from "loglevel"; import type { MomentInput } from "moment"; import moment from "moment"; -import _, { isNumber, isNil, isArray } from "lodash"; +import _, { isNil, isArray } from "lodash"; import type { EditableCell } from "../../constants"; import { ColumnTypes, DateInputFormat } from "../../constants"; import type { ReactTableColumnProps } from "../../component/Constants"; @@ -10,7 +10,6 @@ import shallowEqual from "shallowequal"; export type tableData = Array>; -//TODO: (Vamsi) need to unit test this function export const transformDataPureFn = ( tableData: Array>, columns: ReactTableColumnProps[], @@ -45,8 +44,6 @@ export const transformDataPureFn = ( ) { inputFormat = type; moment(value as MomentInput, inputFormat); - } else if (!isNumber(value)) { - isValidDate = false; } } catch (e) { isValidDate = false;