diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml
index c4dd4001beb..cedac1f8481 100644
--- a/.github/workflows/github-release.yml
+++ b/.github/workflows/github-release.yml
@@ -247,10 +247,14 @@ jobs:
run: |
scripts/generate_info_json.sh
+ # As pg docker image is continuously updated for each scheduled cron on release, we are using the nightly tag while building the latest tag
- name: Place server artifacts-es
run: |
if [[ -f scripts/prepare_server_artifacts.sh ]]; then
- scripts/prepare_server_artifacts.sh
+ PG_TAG=nightly scripts/prepare_server_artifacts.sh
+ else
+ echo "No script found to prepare server artifacts"
+ exit 1
fi
- name: Login to DockerHub
diff --git a/.github/workflows/test-build-docker-image.yml b/.github/workflows/test-build-docker-image.yml
index f14c565e210..e3c21798e6a 100644
--- a/.github/workflows/test-build-docker-image.yml
+++ b/.github/workflows/test-build-docker-image.yml
@@ -356,8 +356,12 @@ jobs:
- name: Place server artifacts-es
run: |
+ run: |
if [[ -f scripts/prepare_server_artifacts.sh ]]; then
scripts/prepare_server_artifacts.sh
+ else
+ echo "No script found to prepare server artifacts"
+ exit 1
fi
- name: Set up Depot CLI
@@ -439,6 +443,9 @@ jobs:
run: |
if [[ -f scripts/prepare_server_artifacts.sh ]]; then
scripts/prepare_server_artifacts.sh
+ else
+ echo "No script found to prepare server artifacts"
+ exit 1
fi
- name: Set up Depot CLI
diff --git a/app/client/cypress/e2e/Regression/ClientSide/Git/GitAutocommit_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Git/GitAutocommit_spec.ts
index 9d38aa357dc..c55b51711ce 100644
--- a/app/client/cypress/e2e/Regression/ClientSide/Git/GitAutocommit_spec.ts
+++ b/app/client/cypress/e2e/Regression/ClientSide/Git/GitAutocommit_spec.ts
@@ -1,5 +1,4 @@
import ReconnectLocators from "../../../../locators/ReconnectLocators";
-import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
import {
agHelper,
gitSync,
@@ -7,7 +6,7 @@ import {
} from "../../../../support/Objects/ObjectsCore";
let wsName: string;
-let repoName: string = "TED-testrepo1";
+let repoName: string = "TED-autocommit-test-1";
describe(
"Git Autocommit",
@@ -15,8 +14,8 @@ describe(
tags: [
"@tag.Git",
"@tag.GitAutocommit",
- "@tag.excludeForAirgap",
"@tag.Sanity",
+ "@tag.TedMigration",
],
},
function () {
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/e2e/Regression/ServerSide/ApiTests/API_Edit_spec.js b/app/client/cypress/e2e/Regression/ServerSide/ApiTests/API_Edit_spec.js
index aa86b788a33..32f675a51fa 100644
--- a/app/client/cypress/e2e/Regression/ServerSide/ApiTests/API_Edit_spec.js
+++ b/app/client/cypress/e2e/Regression/ServerSide/ApiTests/API_Edit_spec.js
@@ -96,7 +96,7 @@ describe(
force: true,
})
.type(
- "https://www.facebook.com/users/{{Button2.text}}?key=test&val={{Button2.text}}",
+ "http://host.docker.internal:5001/{{Button2.text}}?key=test&val={{Button2.text}}",
{ force: true, parseSpecialCharSequences: false },
)
.wait(3000)
@@ -106,7 +106,7 @@ describe(
.type("{enter}", { parseSpecialCharSequences: true });
cy.validateEvaluatedValue(
- "https://www.facebook.com/users/Cancel?key=test&val=Cancel",
+ "http://host.docker.internal:5001/Cancel?key=test&val=Cancel",
);
});
},
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 e32390df546..ebe2600bee5 100644
--- a/app/client/cypress/support/Pages/Table.ts
+++ b/app/client/cypress/support/Pages/Table.ts
@@ -855,9 +855,44 @@ export class Table {
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,
+ };
+ }
+
public ExpandIfCollapsedSection(sectionName: string) {
- cy.get(`.t--property-pane-section-collapse-${sectionName}`).scrollIntoView().then(
- ($element) => {
+ cy.get(`.t--property-pane-section-collapse-${sectionName}`)
+ .scrollIntoView()
+ .then(($element) => {
cy.wrap($element)
.siblings(".bp3-collapse")
.then(($sibling) => {
@@ -867,7 +902,6 @@ export class Table {
$element.click();
}
});
- },
- );
+ });
}
}
diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/PluginActionForm.tsx b/app/client/src/PluginActionEditor/components/PluginActionForm/PluginActionForm.tsx
index db91b29b3a2..66ab320a304 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionForm/PluginActionForm.tsx
+++ b/app/client/src/PluginActionEditor/components/PluginActionForm/PluginActionForm.tsx
@@ -2,13 +2,18 @@ import React from "react";
import APIEditorForm from "./components/APIEditorForm";
import { Flex } from "@appsmith/ads";
import { useChangeActionCall } from "./hooks/useChangeActionCall";
+import { usePluginActionContext } from "../../PluginActionContext";
+import { UIComponentTypes } from "api/PluginApi";
const PluginActionForm = () => {
useChangeActionCall();
+ const { plugin } = usePluginActionContext();
return (
-
+ {plugin.uiComponent === UIComponentTypes.ApiEditorForm ? (
+
+ ) : null}
);
};
diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useGetFormActionValues.ts b/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useGetFormActionValues.ts
index 1078d18282d..da75a1482a2 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useGetFormActionValues.ts
+++ b/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useGetFormActionValues.ts
@@ -18,7 +18,7 @@ function useGetFormActionValues() {
// In an unlikely scenario where form is not initialised,
// return empty values to avoid form ui issues
- if (!isAPIAction(formValues)) {
+ if (!formValues || !isAPIAction(formValues)) {
return {
actionHeaders: [],
actionParams: [],
diff --git a/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx b/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx
new file mode 100644
index 00000000000..a51d6006dec
--- /dev/null
+++ b/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx
@@ -0,0 +1,81 @@
+import React from "react";
+import { useSelector } from "react-redux";
+import ActionNameEditor from "components/editorComponents/ActionNameEditor";
+import { usePluginActionContext } from "PluginActionEditor/PluginActionContext";
+import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
+import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
+import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
+import { PluginType } from "entities/Action";
+import type { ReduxAction } from "ee/constants/ReduxActionConstants";
+import styled from "styled-components";
+import { getSavingStatusForActionName } from "selectors/actionSelectors";
+import { getAssetUrl } from "ee/utils/airgapHelpers";
+import { ActionUrlIcon } from "pages/Editor/Explorer/ExplorerIcons";
+
+export interface SaveActionNameParams {
+ id: string;
+ name: string;
+}
+
+export interface PluginActionNameEditorProps {
+ saveActionName: (
+ params: SaveActionNameParams,
+ ) => ReduxAction;
+}
+
+const ActionNameEditorWrapper = styled.div`
+ & .ads-v2-box {
+ gap: var(--ads-v2-spaces-2);
+ }
+
+ && .t--action-name-edit-field {
+ font-size: 12px;
+
+ .bp3-editable-text-content {
+ height: unset !important;
+ line-height: unset !important;
+ }
+ }
+
+ & .t--plugin-icon-box {
+ height: 12px;
+ width: 12px;
+
+ img {
+ width: 12px;
+ height: auto;
+ }
+ }
+`;
+
+const PluginActionNameEditor = (props: PluginActionNameEditorProps) => {
+ const { action, plugin } = usePluginActionContext();
+
+ const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
+ const isChangePermitted = getHasManageActionPermission(
+ isFeatureEnabled,
+ action?.userPermissions,
+ );
+
+ const saveStatus = useSelector((state) =>
+ getSavingStatusForActionName(state, action?.id || ""),
+ );
+
+ const iconUrl = getAssetUrl(plugin?.iconLocation) || "";
+ const icon = ActionUrlIcon(iconUrl);
+
+ return (
+
+
+
+ );
+};
+
+export default PluginActionNameEditor;
diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx
index f20a395d4cb..9359c8130f3 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx
+++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx
@@ -57,7 +57,7 @@ function PluginActionResponse() {
expandedHeight={`${ActionExecutionResizerHeight}px`}
isCollapsed={!open}
onSelect={updateSelectedResponseTab}
- selectedTabKey={selectedTab || tabs[0].key}
+ selectedTabKey={selectedTab || tabs[0]?.key}
tabs={tabs}
/>
diff --git a/app/client/src/PluginActionEditor/index.ts b/app/client/src/PluginActionEditor/index.ts
index 20265c8bc5a..e8083e1b0f0 100644
--- a/app/client/src/PluginActionEditor/index.ts
+++ b/app/client/src/PluginActionEditor/index.ts
@@ -6,3 +6,8 @@ export {
export { default as PluginActionToolbar } from "./components/PluginActionToolbar";
export { default as PluginActionForm } from "./components/PluginActionForm";
export { default as PluginActionResponse } from "./components/PluginActionResponse";
+export type {
+ SaveActionNameParams,
+ PluginActionNameEditorProps,
+} from "./components/PluginActionNameEditor";
+export { default as PluginActionNameEditor } from "./components/PluginActionNameEditor";
diff --git a/app/client/src/components/editorComponents/ActionNameEditor.tsx b/app/client/src/components/editorComponents/ActionNameEditor.tsx
index 8467855f479..1d9bf01aa7c 100644
--- a/app/client/src/components/editorComponents/ActionNameEditor.tsx
+++ b/app/client/src/components/editorComponents/ActionNameEditor.tsx
@@ -1,19 +1,13 @@
import React, { memo } from "react";
-import { useSelector } from "react-redux";
-import { useParams } from "react-router-dom";
import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { removeSpecialChars } from "utils/helpers";
-import type { AppState } from "ee/reducers";
-import { saveActionName } from "actions/pluginActionActions";
import { Flex } from "@appsmith/ads";
-import { getActionByBaseId, getPlugin } from "ee/selectors/entitiesSelector";
import NameEditorComponent, {
IconBox,
- IconWrapper,
NameWrapper,
} from "components/utils/NameEditorComponent";
import {
@@ -21,14 +15,13 @@ import {
ACTION_NAME_PLACEHOLDER,
createMessage,
} from "ee/constants/messages";
-import { getAssetUrl } from "ee/utils/airgapHelpers";
-import { getSavingStatusForActionName } from "selectors/actionSelectors";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
+import type { SaveActionNameParams } from "PluginActionEditor";
+import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
+import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
+import type { Action } from "entities/Action";
+import type { ModuleInstance } from "ee/constants/ModuleInstanceConstants";
-interface SaveActionNameParams {
- id: string;
- name: string;
-}
interface ActionNameEditorProps {
/*
This prop checks if page is API Pane or Query Pane or Curl Pane
@@ -38,38 +31,34 @@ interface ActionNameEditorProps {
*/
enableFontStyling?: boolean;
disabled?: boolean;
- saveActionName?: (
+ saveActionName: (
params: SaveActionNameParams,
) => ReduxAction;
+ actionConfig?: Action | ModuleInstance;
+ icon?: JSX.Element;
+ saveStatus: { isSaving: boolean; error: boolean };
}
function ActionNameEditor(props: ActionNameEditorProps) {
- const params = useParams<{ baseApiId?: string; baseQueryId?: string }>();
-
- const currentActionConfig = useSelector((state: AppState) =>
- getActionByBaseId(state, params.baseApiId || params.baseQueryId || ""),
- );
-
- const currentPlugin = useSelector((state: AppState) =>
- getPlugin(state, currentActionConfig?.pluginId || ""),
- );
+ const {
+ actionConfig,
+ disabled = false,
+ enableFontStyling = false,
+ icon = "",
+ saveActionName,
+ saveStatus,
+ } = props;
- const saveStatus = useSelector((state) =>
- getSavingStatusForActionName(state, currentActionConfig?.id || ""),
+ const isActionRedesignEnabled = useFeatureFlag(
+ FEATURE_FLAG.release_actions_redesign_enabled,
);
return (
{({
@@ -85,28 +74,22 @@ function ActionNameEditor(props: ActionNameEditorProps) {
isNew: boolean;
saveStatus: { isSaving: boolean; error: boolean };
}) => (
-
+
- {currentPlugin && (
-
-
-
- )}
+ {icon && {icon}}
))}
diff --git a/app/client/src/components/utils/NameEditorComponent.tsx b/app/client/src/components/utils/NameEditorComponent.tsx
index b7aa2e03ce4..876a9c9d1fd 100644
--- a/app/client/src/components/utils/NameEditorComponent.tsx
+++ b/app/client/src/components/utils/NameEditorComponent.tsx
@@ -11,6 +11,8 @@ import {
} from "ee/constants/messages";
import styled from "styled-components";
import { Classes } from "@blueprintjs/core";
+import type { SaveActionNameParams } from "PluginActionEditor";
+import type { ReduxAction } from "ee/constants/ReduxActionConstants";
export const NameWrapper = styled.div<{ enableFontStyling?: boolean }>`
min-width: 50%;
@@ -71,9 +73,9 @@ interface NameEditorProps {
children: (params: any) => JSX.Element;
id?: string;
name?: string;
- // TODO: Fix this the next time the file is edited
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- dispatchAction: (a: any) => any;
+ onSaveName: (
+ params: SaveActionNameParams,
+ ) => ReduxAction;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
suffixErrorMessage?: (params?: any) => string;
@@ -90,10 +92,10 @@ interface NameEditorProps {
function NameEditor(props: NameEditorProps) {
const {
- dispatchAction,
id: entityId,
idUndefinedErrorMessage,
name: entityName,
+ onSaveName,
saveStatus,
suffixErrorMessage = ACTION_NAME_CONFLICT_ERROR,
} = props;
@@ -131,8 +133,8 @@ function NameEditor(props: NameEditorProps) {
const handleNameChange = useCallback(
(name: string) => {
- if (name !== entityName && !isInvalidNameForEntity(name)) {
- dispatch(dispatchAction({ id: entityId, name }));
+ if (name !== entityName && !isInvalidNameForEntity(name) && entityId) {
+ dispatch(onSaveName({ id: entityId, name }));
}
},
[dispatch, isInvalidNameForEntity, entityId, entityName],
diff --git a/app/client/src/pages/Editor/APIEditor/ApiEditorContext.tsx b/app/client/src/pages/Editor/APIEditor/ApiEditorContext.tsx
index a5d3aa5e235..1a2d9d51789 100644
--- a/app/client/src/pages/Editor/APIEditor/ApiEditorContext.tsx
+++ b/app/client/src/pages/Editor/APIEditor/ApiEditorContext.tsx
@@ -1,11 +1,7 @@
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import type { PaginationField } from "api/ActionAPI";
import React, { createContext, useMemo } from "react";
-
-interface SaveActionNameParams {
- id: string;
- name: string;
-}
+import type { SaveActionNameParams } from "PluginActionEditor";
interface ApiEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
@@ -15,7 +11,7 @@ interface ApiEditorContextContextProps {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingsConfig: any;
- saveActionName?: (
+ saveActionName: (
params: SaveActionNameParams,
) => ReduxAction;
closeEditorLink?: React.ReactNode;
diff --git a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx
index b542cdfbd04..979c4edf1cb 100644
--- a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx
+++ b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx
@@ -35,6 +35,9 @@ import {
InfoFields,
RequestTabs,
} from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm";
+import { getSavingStatusForActionName } from "selectors/actionSelectors";
+import { getAssetUrl } from "ee/utils/airgapHelpers";
+import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
const Form = styled.form`
position: relative;
@@ -245,6 +248,18 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
currentActionConfig?.userPermissions,
);
+ const currentPlugin = useSelector((state: AppState) =>
+ getPlugin(state, currentActionConfig?.pluginId || ""),
+ );
+
+ const saveStatus = useSelector((state) =>
+ getSavingStatusForActionName(state, currentActionConfig?.id || ""),
+ );
+
+ const iconUrl = getAssetUrl(currentPlugin?.iconLocation) || "";
+
+ const icon = ActionUrlIcon(iconUrl);
+
const plugin = useSelector((state: AppState) =>
getPlugin(state, pluginId ?? ""),
);
@@ -281,9 +296,12 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
diff --git a/app/client/src/pages/Editor/APIEditor/index.tsx b/app/client/src/pages/Editor/APIEditor/index.tsx
index 247c2cfd408..31328d25055 100644
--- a/app/client/src/pages/Editor/APIEditor/index.tsx
+++ b/app/client/src/pages/Editor/APIEditor/index.tsx
@@ -8,7 +8,11 @@ import {
getPluginSettingConfigs,
getPlugins,
} from "ee/selectors/entitiesSelector";
-import { deleteAction, runAction } from "actions/pluginActionActions";
+import {
+ deleteAction,
+ runAction,
+ saveActionName,
+} from "actions/pluginActionActions";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import Editor from "./Editor";
import BackToCanvas from "components/common/BackToCanvas";
@@ -151,15 +155,7 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
});
dispatch(runAction(action?.id ?? "", paginationField));
},
- [
- action?.id,
- apiName,
- pageName,
- getPageName,
- plugins,
- pluginId,
- datasourceId,
- ],
+ [action?.id, apiName, pageName, plugins, pluginId, datasourceId, dispatch],
);
const actionRightPaneBackLink = useMemo(() => {
@@ -173,13 +169,13 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
pageName,
});
dispatch(deleteAction({ id: action?.id ?? "", name: apiName }));
- }, [getPageName, pages, basePageId, apiName]);
+ }, [pages, basePageId, apiName, action?.id, dispatch, pageName]);
const notification = useMemo(() => {
if (!isConverting) return null;
return ;
- }, [action?.name, isConverting]);
+ }, [action?.name, isConverting, icon]);
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
@@ -196,6 +192,7 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
handleRunClick={handleRunClick}
moreActionsMenu={moreActionsMenu}
notification={notification}
+ saveActionName={saveActionName}
settingsConfig={settingsConfig}
>
diff --git a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx
index de937dd86a1..e0fb005bc00 100644
--- a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx
+++ b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx
@@ -16,6 +16,8 @@ import {
import { Tooltip } from "@appsmith/ads";
import { useSelector } from "react-redux";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
+import type { ReduxAction } from "ee/constants/ReduxActionConstants";
+import type { SaveActionNameParams } from "PluginActionEditor";
export const searchHighlightSpanClassName = "token";
export const searchTokenizationDelimiter = "!!";
@@ -84,7 +86,7 @@ export interface EntityNameProps {
name: string;
isEditing?: boolean;
onChange?: (name: string) => void;
- updateEntityName: (name: string) => void;
+ updateEntityName: (name: string) => ReduxAction;
entityId: string;
searchKeyword?: string;
className?: string;
@@ -164,10 +166,10 @@ export const EntityName = React.memo(
return (
diff --git a/app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx b/app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx
index f82860f134b..ff1a598bc1b 100644
--- a/app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx
+++ b/app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx
@@ -25,11 +25,8 @@ import NameEditorComponent, {
} from "components/utils/NameEditorComponent";
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
+import type { SaveActionNameParams } from "PluginActionEditor";
-export interface SaveActionNameParams {
- id: string;
- name: string;
-}
export interface JSObjectNameEditorProps {
/*
This prop checks if page is API Pane or Query Pane or Curl Pane
@@ -64,10 +61,10 @@ export function JSObjectNameEditor(props: JSObjectNameEditorProps) {
return (
{({
diff --git a/app/client/src/pages/Editor/QueryEditor/QueryEditorContext.tsx b/app/client/src/pages/Editor/QueryEditor/QueryEditorContext.tsx
index 03c46a99d0d..c5089fc6bdf 100644
--- a/app/client/src/pages/Editor/QueryEditor/QueryEditorContext.tsx
+++ b/app/client/src/pages/Editor/QueryEditor/QueryEditorContext.tsx
@@ -1,18 +1,14 @@
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
+import type { SaveActionNameParams } from "PluginActionEditor";
import React, { createContext, useMemo } from "react";
-interface SaveActionNameParams {
- id: string;
- name: string;
-}
-
interface QueryEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
onCreateDatasourceClick?: () => void;
onEntityNotFoundBackClick?: () => void;
changeQueryPage?: (baseQueryId: string) => void;
actionRightPaneBackLink?: React.ReactNode;
- saveActionName?: (
+ saveActionName: (
params: SaveActionNameParams,
) => ReduxAction;
closeEditorLink?: React.ReactNode;
diff --git a/app/client/src/pages/Editor/QueryEditor/QueryEditorHeader.tsx b/app/client/src/pages/Editor/QueryEditor/QueryEditorHeader.tsx
index af1d28c83a9..38f9a9b3d72 100644
--- a/app/client/src/pages/Editor/QueryEditor/QueryEditorHeader.tsx
+++ b/app/client/src/pages/Editor/QueryEditor/QueryEditorHeader.tsx
@@ -13,6 +13,7 @@ import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import { useSelector } from "react-redux";
import {
getActionByBaseId,
+ getPlugin,
getPluginNameFromId,
} from "ee/selectors/entitiesSelector";
import { QueryEditorContext } from "./QueryEditorContext";
@@ -21,6 +22,9 @@ import type { Datasource } from "entities/Datasource";
import type { AppState } from "ee/reducers";
import { SQL_DATASOURCES } from "constants/QueryEditorConstants";
import DatasourceSelector from "./DatasourceSelector";
+import { getSavingStatusForActionName } from "selectors/actionSelectors";
+import { getAssetUrl } from "ee/utils/airgapHelpers";
+import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
const NameWrapper = styled.div`
display: flex;
@@ -79,6 +83,18 @@ const QueryEditorHeader = (props: Props) => {
currentActionConfig?.userPermissions,
);
+ const currentPlugin = useSelector((state: AppState) =>
+ getPlugin(state, currentActionConfig?.pluginId || ""),
+ );
+
+ const saveStatus = useSelector((state) =>
+ getSavingStatusForActionName(state, currentActionConfig?.id || ""),
+ );
+
+ const iconUrl = getAssetUrl(currentPlugin?.iconLocation) || "";
+
+ const icon = ActionUrlIcon(iconUrl);
+
// get the current action's plugin name
const currentActionPluginName = useSelector((state: AppState) =>
getPluginNameFromId(state, currentActionConfig?.pluginId || ""),
@@ -106,8 +122,11 @@ const QueryEditorHeader = (props: Props) => {
diff --git a/app/client/src/pages/Editor/QueryEditor/index.tsx b/app/client/src/pages/Editor/QueryEditor/index.tsx
index f76cbffe1b5..03457a6228e 100644
--- a/app/client/src/pages/Editor/QueryEditor/index.tsx
+++ b/app/client/src/pages/Editor/QueryEditor/index.tsx
@@ -42,6 +42,7 @@ import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import { AppPluginActionEditor } from "../AppPluginActionEditor";
+import { saveActionName } from "actions/pluginActionActions";
type QueryEditorProps = RouteComponentProps;
@@ -126,6 +127,7 @@ function QueryEditor(props: QueryEditorProps) {
}, [
action?.id,
action?.name,
+ action?.pluginType,
isChangePermitted,
isDeletePermitted,
basePageId,
@@ -143,7 +145,7 @@ function QueryEditor(props: QueryEditorProps) {
changeQuery({ baseQueryId: baseQueryId, basePageId, applicationId }),
);
},
- [basePageId, applicationId],
+ [basePageId, applicationId, dispatch],
);
const onCreateDatasourceClick = useCallback(() => {
@@ -159,13 +161,7 @@ function QueryEditor(props: QueryEditorProps) {
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
entryPoint,
});
- }, [
- basePageId,
- history,
- integrationEditorURL,
- DatasourceCreateEntryPoints,
- AnalyticsUtil,
- ]);
+ }, [basePageId]);
// custom function to return user to integrations page if action is not found
const onEntityNotFoundBackClick = useCallback(
@@ -176,7 +172,7 @@ function QueryEditor(props: QueryEditorProps) {
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
),
- [basePageId, history, integrationEditorURL],
+ [basePageId],
);
const notification = useMemo(() => {
@@ -189,7 +185,7 @@ function QueryEditor(props: QueryEditorProps) {
withPadding
/>
);
- }, [action?.name, isConverting]);
+ }, [action?.name, isConverting, icon]);
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
@@ -207,6 +203,7 @@ function QueryEditor(props: QueryEditorProps) {
notification={notification}
onCreateDatasourceClick={onCreateDatasourceClick}
onEntityNotFoundBackClick={onEntityNotFoundBackClick}
+ saveActionName={saveActionName}
>
= {},
+) => {
+ const defaultProps: BaseInputComponentProps = {
+ value: "",
+ inputType: "TEXT",
+ inputHTMLType: "TEXT",
+ disabled: false,
+ isLoading: false,
+ compactMode: false,
+ isInvalid: false,
+ label: "Salary",
+ showError: false,
+ onValueChange: jest.fn(),
+ onFocusChange: jest.fn(),
+ widgetId: "test-widget",
+ rtl: true,
+ };
+
+ return render(
+
+
+ ,
+ );
+};
+
+describe("BaseInputComponent TestCases", () => {
+ test("1. Icon should be visible and aligned to the right when the input type is a number", () => {
+ const { container } = renderBaseInputComponent({
+ inputType: "NUMBER",
+ inputHTMLType: "NUMBER",
+ value: "123",
+ onStep: jest.fn(),
+ rtl: false,
+ shouldUseLocale: true,
+ iconName: "add",
+ iconAlign: "right",
+ });
+
+ const numericInputIcon = container.getElementsByClassName(
+ "bp3-icon bp3-icon-add",
+ )[0];
+
+ expect(numericInputIcon).toBeInTheDocument();
+ });
+});
diff --git a/app/client/src/widgets/BaseInputWidget/component/index.tsx b/app/client/src/widgets/BaseInputWidget/component/index.tsx
index d898b5b73e4..cbb7b21599a 100644
--- a/app/client/src/widgets/BaseInputWidget/component/index.tsx
+++ b/app/client/src/widgets/BaseInputWidget/component/index.tsx
@@ -565,6 +565,7 @@ class BaseInputComponent extends React.Component<
onKeyUp={this.onKeyUp}
onValueChange={this.onNumberChange}
placeholder={this.props.placeholder}
+ rightElement={this.getRightIcon()}
stepSize={this.props.stepSize}
value={this.props.value}
{...conditionalProps}
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;
diff --git a/app/client/src/workers/Evaluation/handlers/jsLibrary.ts b/app/client/src/workers/Evaluation/handlers/jsLibrary.ts
index bdf704f2bab..ce81c2d7bf5 100644
--- a/app/client/src/workers/Evaluation/handlers/jsLibrary.ts
+++ b/app/client/src/workers/Evaluation/handlers/jsLibrary.ts
@@ -128,39 +128,31 @@ export async function installLibrary(
// Find keys add that were installed to the global scope.
const keysAfterInstallation = Object.keys(self);
- const differentiatingKeys = difference(
+ let differentiatingKeys = difference(
keysAfterInstallation,
envKeysBeforeInstallation,
);
- if (
- differentiatingKeys.length > 0 &&
- differentiatingKeys.includes("default")
- ) {
- // Changing default export to library specific name
- const uniqueName = generateUniqueAccessor(
- url,
- takenAccessors,
- takenNamesMap,
- );
+ // Changing default export to library specific name, if default exported
+ const uniqueName = generateUniqueAccessor(
+ url,
+ takenAccessors,
+ takenNamesMap,
+ );
- // mapping default functionality to library name accessor
- self[uniqueName] = self["default"];
- // deleting the reference of default key from the self object
- delete self["default"];
- // mapping all the references of differentiating keys from the self object to the self[uniqueName] key object
- differentiatingKeys.map((key) => {
- if (key !== "default") {
- self[uniqueName][key] = self[key];
- // deleting the references from the self object
- delete self[key];
- }
- });
- // pushing the uniqueName to the accessor array
- accessors.push(uniqueName);
- } else {
- accessors.push(...differentiatingKeys);
- }
+ movetheDefaultExportedLibraryToAccessorKey(
+ differentiatingKeys,
+ uniqueName,
+ );
+
+ // Following the same process which was happening earlier
+ const keysAfterDefaultOperation = Object.keys(self);
+
+ differentiatingKeys = difference(
+ keysAfterDefaultOperation,
+ envKeysBeforeInstallation,
+ );
+ accessors.push(...differentiatingKeys);
/**
* Check the list of installed library to see if their values have changed.
@@ -308,7 +300,18 @@ export async function loadLibraries(
try {
self.importScripts(url);
const keysAfter = Object.keys(self);
- const defaultAccessors = difference(keysAfter, keysBefore);
+ let defaultAccessors = difference(keysAfter, keysBefore);
+
+ // Changing default export to library accessors name which was saved when it was installed, if default export present
+ movetheDefaultExportedLibraryToAccessorKey(
+ defaultAccessors,
+ accessors[0],
+ );
+
+ // Following the same process which was happening earlier
+ const keysAfterDefaultOperation = Object.keys(self);
+
+ defaultAccessors = difference(keysAfterDefaultOperation, keysBefore);
/**
* Installing 2 different version of lodash tries to add the same accessor on the self object. Let take version a & b for example.
@@ -447,3 +450,24 @@ export function flattenModule(module: Record) {
return libModule;
}
+
+// This function will update the self keys only when the diffAccessors has default included in it.
+function movetheDefaultExportedLibraryToAccessorKey(
+ diffAccessors: string[],
+ uniqAccessor: string,
+) {
+ if (diffAccessors.length > 0 && diffAccessors.includes("default")) {
+ // mapping default functionality to library name accessor
+ self[uniqAccessor] = self["default"];
+ // deleting the reference of default key from the self object
+ delete self["default"];
+ // mapping all the references of differentiating keys from the self object to the self[uniqAccessor] key object
+ diffAccessors.map((key) => {
+ if (key !== "default") {
+ self[uniqAccessor][key] = self[key];
+ // deleting the references from the self object
+ delete self[key];
+ }
+ });
+ }
+}
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java
index 7c52a719d02..960834f9064 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java
@@ -24,6 +24,7 @@ interface PackageName {
String APPSMITH_AI_PLUGIN = "appsmithai-plugin";
String DATABRICKS_PLUGIN = "databricks-plugin";
String AWS_LAMBDA_PLUGIN = "aws-lambda-plugin";
+ String MONGO_PLUGIN = "mongo-plugin";
}
public static final String DEFAULT_REST_DATASOURCE = "DEFAULT_REST_DATASOURCE";
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java
index a654857ec80..e3e96745784 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java
@@ -15,5 +15,9 @@ public enum FeatureFlagEnum {
release_embed_hide_share_settings_enabled,
rollout_datasource_test_rate_limit_enabled,
+ // Deprecated CE flags over here
+ release_git_autocommit_feature_enabled,
+ release_git_autocommit_eligibility_enabled,
+
// Add EE flags below this line, to avoid conflicts.
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/DatasourcePluginContext.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/DatasourcePluginContext.java
new file mode 100644
index 00000000000..b3385964e2d
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/DatasourcePluginContext.java
@@ -0,0 +1,20 @@
+package com.appsmith.server.domains;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@ToString
+public class DatasourcePluginContext {
+ private T connection;
+ private String pluginId;
+ private Instant creationTime;
+
+ public DatasourcePluginContext() {
+ creationTime = Instant.now();
+ }
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceContextServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceContextServiceCEImpl.java
index 3693d1348c4..adc1d2ea82b 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceContextServiceCEImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceContextServiceCEImpl.java
@@ -13,6 +13,7 @@
import com.appsmith.server.datasourcestorages.base.DatasourceStorageService;
import com.appsmith.server.domains.DatasourceContext;
import com.appsmith.server.domains.DatasourceContextIdentifier;
+import com.appsmith.server.domains.DatasourcePluginContext;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
@@ -20,6 +21,10 @@
import com.appsmith.server.plugins.base.PluginService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.solutions.DatasourcePermission;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@@ -29,8 +34,12 @@
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
import java.util.function.Function;
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+
@Slf4j
public class DatasourceContextServiceCEImpl implements DatasourceContextServiceCE {
@@ -38,6 +47,21 @@ public class DatasourceContextServiceCEImpl implements DatasourceContextServiceC
protected final Map>> datasourceContextMonoMap;
protected final Map datasourceContextSynchronizationMonitorMap;
protected final Map> datasourceContextMap;
+
+ /**
+ * This cache is used to store the datasource context for a limited time and a limited max number of connections and
+ * then destroy the least recently used connection. The cleanup process is triggered when the cache is accessed and
+ * either the time limit or the max connections are reached.
+ * The purpose of this is to prevent the large number of open dangling connections to the movies mockDB.
+ * The removalListener method is called when the connection is removed from the cache.
+ */
+ protected final Cache datasourcePluginContextMapLRUCache =
+ CacheBuilder.newBuilder()
+ .removalListener(createRemovalListener())
+ .expireAfterAccess(2, TimeUnit.HOURS)
+ .maximumSize(300) // caches most recently used 300 mock connections per pod
+ .build();
+
private final DatasourceService datasourceService;
private final DatasourceStorageService datasourceStorageService;
private final PluginService pluginService;
@@ -67,6 +91,50 @@ public DatasourceContextServiceCEImpl(
this.datasourcePermission = datasourcePermission;
}
+ private RemovalListener createRemovalListener() {
+ return (RemovalNotification removalNotification) -> {
+ handleRemoval(removalNotification);
+ };
+ }
+
+ private Object getConnectionFromDatasourceContextMap(DatasourceContextIdentifier datasourceContextIdentifier) {
+ return this.datasourceContextMap.containsKey(datasourceContextIdentifier)
+ && this.datasourceContextMap.get(datasourceContextIdentifier) != null
+ ? this.datasourceContextMap.get(datasourceContextIdentifier).getConnection()
+ : null;
+ }
+
+ private void handleRemoval(
+ RemovalNotification removalNotification) {
+ final DatasourceContextIdentifier datasourceContextIdentifier = removalNotification.getKey();
+ final DatasourcePluginContext datasourcePluginContext = removalNotification.getValue();
+
+ log.debug(
+ "Removing Datasource Context from cache and closing the open connection for DatasourceId: {} and environmentId: {}",
+ datasourceContextIdentifier.getDatasourceId(),
+ datasourceContextIdentifier.getEnvironmentId());
+ log.info("LRU Cache Size after eviction: {}", datasourcePluginContextMapLRUCache.size());
+
+ // Close connection and remove entry from both cache maps
+ final Object connection = getConnectionFromDatasourceContextMap(datasourceContextIdentifier);
+
+ Mono pluginMono =
+ pluginService.findById(datasourcePluginContext.getPluginId()).cache();
+ if (connection != null) {
+ pluginExecutorHelper
+ .getPluginExecutor(pluginMono)
+ .flatMap(pluginExecutor -> Mono.fromRunnable(() -> pluginExecutor.datasourceDestroy(connection)))
+ .onErrorResume(e -> {
+ log.error("Error destroying stale datasource connection", e);
+ return Mono.empty();
+ })
+ .subscribe(); // Trigger the execution
+ }
+ // Remove the entries from both maps
+ datasourceContextMonoMap.remove(datasourceContextIdentifier);
+ datasourceContextMap.remove(datasourceContextIdentifier);
+ }
+
/**
* This method defines a critical section that can be executed only by one thread at a time per datasource id - i
* .e. if two threads want to create datasource for different datasource ids then they would not be synchronized.
@@ -115,6 +183,11 @@ public Mono> getCachedDatasourceContextMono(
}
datasourceContextMonoMap.remove(datasourceContextIdentifier);
datasourceContextMap.remove(datasourceContextIdentifier);
+ log.info(
+ "Invalidating the LRU cache entry for datasource id {}, environment id {} as the connection is stale or in error state",
+ datasourceContextIdentifier.getDatasourceId(),
+ datasourceContextIdentifier.getEnvironmentId());
+ datasourcePluginContextMapLRUCache.invalidate(datasourceContextIdentifier);
}
/*
@@ -129,17 +202,13 @@ public Mono> getCachedDatasourceContextMono(
+ ": Cached resource context mono exists for datasource id {}, environment id {}. Returning the same.",
datasourceContextIdentifier.getDatasourceId(),
datasourceContextIdentifier.getEnvironmentId());
+ // Accessing the LRU cache to update the last accessed time
+ datasourcePluginContextMapLRUCache.getIfPresent(datasourceContextIdentifier);
return datasourceContextMonoMap.get(datasourceContextIdentifier);
}
/* Create a fresh datasource context */
DatasourceContext