diff --git a/src/IDE/Components/EditableName/EditableName.test.tsx b/src/IDE/Components/EditableName/EditableName.test.tsx
new file mode 100644
index 000000000000..77a4f756555b
--- /dev/null
+++ b/src/IDE/Components/EditableName/EditableName.test.tsx
@@ -0,0 +1,212 @@
+import React from "react";
+import { EditableName } from "./EditableName";
+import { render } from "test/testUtils";
+import "@testing-library/jest-dom";
+import { Icon } from "@appsmith/ads";
+import { fireEvent } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+describe("EditableName", () => {
+ const mockOnNameSave = jest.fn();
+ const mockOnExitEditing = jest.fn();
+
+ const name = "test_name";
+ const TabIcon = () => ;
+ const KEY_CONFIG = {
+ ENTER: { key: "Enter", keyCode: 13 },
+ ESC: { key: "Esc", keyCode: 27 },
+ };
+
+ const setup = ({ isEditing = false, isLoading = false }) => {
+ // Define the props
+ const props = {
+ name,
+ icon: ,
+ isEditing,
+ onNameSave: mockOnNameSave,
+ exitEditing: mockOnExitEditing,
+ isLoading,
+ };
+
+ // Render the component
+ const utils = render();
+
+ return {
+ ...props,
+ ...utils,
+ };
+ };
+
+ test("renders component", () => {
+ const utils = setup({});
+ const editableNameElement = utils.getByText(utils.name);
+
+ expect(editableNameElement).toBeInTheDocument();
+ expect(editableNameElement.textContent).toBe(name);
+ });
+
+ test("renders input when editing", () => {
+ const utils = setup({ isEditing: true });
+
+ const editableNameElement = utils.queryByText(utils.name);
+
+ expect(editableNameElement).not.toBeInTheDocument();
+
+ const inputElement = utils.getByRole("textbox");
+
+ expect(inputElement).toBeInTheDocument();
+ });
+
+ describe("valid input actions", () => {
+ test("submit event", async () => {
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+
+ // hit enter
+ const enterTitle = "enter_title";
+
+ fireEvent.change(getByRole("textbox"), {
+ target: { value: enterTitle },
+ });
+ expect(getByRole("textbox")).toHaveValue(enterTitle);
+
+ fireEvent.keyUp(getByRole("textbox"), KEY_CONFIG.ENTER);
+
+ expect(onNameSave).toHaveBeenCalledWith(enterTitle);
+ expect(exitEditing).toHaveBeenCalled();
+ });
+
+ test("outside click event", async () => {
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+
+ const clickOutsideTitle = "click_outside_title";
+
+ fireEvent.change(getByRole("textbox"), {
+ target: { value: clickOutsideTitle },
+ });
+
+ await userEvent.click(document.body);
+
+ expect(onNameSave).toHaveBeenCalledWith(clickOutsideTitle);
+ expect(exitEditing).toHaveBeenCalled();
+ });
+
+ test("esc key event", async () => {
+ const escapeTitle = "escape_title";
+
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+
+ fireEvent.change(getByRole("textbox"), {
+ target: { value: escapeTitle },
+ });
+
+ fireEvent.keyUp(getByRole("textbox"), KEY_CONFIG.ESC);
+
+ expect(exitEditing).toHaveBeenCalled();
+ expect(onNameSave).not.toHaveBeenCalledWith(escapeTitle);
+ });
+
+ test("focus out event", async () => {
+ const focusOutTitle = "focus_out_title";
+
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+
+ const inputElement = getByRole("textbox");
+
+ fireEvent.change(inputElement, {
+ target: { value: focusOutTitle },
+ });
+
+ fireEvent.keyUp(inputElement, KEY_CONFIG.ESC);
+ expect(exitEditing).toHaveBeenCalled();
+ expect(onNameSave).not.toHaveBeenCalledWith(focusOutTitle);
+ });
+ });
+
+ describe("invalid input actions", () => {
+ const invalidTitle = "else";
+ const validationError =
+ "else is already being used or is a restricted keyword.";
+
+ test("click outside", async () => {
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+ const inputElement = getByRole("textbox");
+
+ fireEvent.change(inputElement, {
+ target: { value: invalidTitle },
+ });
+
+ fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
+
+ expect(getByRole("tooltip")).toBeInTheDocument();
+
+ expect(getByRole("tooltip").textContent).toEqual(validationError);
+
+ await userEvent.click(document.body);
+
+ expect(getByRole("tooltip").textContent).toEqual("");
+
+ expect(exitEditing).toHaveBeenCalled();
+ expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
+ });
+
+ test("esc key", async () => {
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+ const inputElement = getByRole("textbox");
+
+ fireEvent.change(inputElement, {
+ target: { value: invalidTitle },
+ });
+
+ fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
+ fireEvent.keyUp(inputElement, KEY_CONFIG.ESC);
+
+ expect(getByRole("tooltip")).toBeInTheDocument();
+
+ expect(getByRole("tooltip").textContent).toEqual("");
+ expect(exitEditing).toHaveBeenCalled();
+ expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
+ });
+
+ test("focus out event", async () => {
+ const { exitEditing, getByRole, onNameSave } = setup({
+ isEditing: true,
+ });
+ const inputElement = getByRole("textbox");
+
+ fireEvent.change(inputElement, {
+ target: { value: invalidTitle },
+ });
+
+ fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
+ fireEvent.focusOut(inputElement);
+ expect(getByRole("tooltip").textContent).toEqual("");
+ expect(exitEditing).toHaveBeenCalled();
+ expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
+ });
+
+ test("prevents saving empty name", () => {
+ const { getByRole, onNameSave } = setup({ isEditing: true });
+ const input = getByRole("textbox");
+
+ fireEvent.change(input, { target: { value: "" } });
+ fireEvent.keyUp(input, KEY_CONFIG.ENTER);
+
+ expect(onNameSave).not.toHaveBeenCalledWith("");
+ expect(getByRole("tooltip")).toHaveTextContent(
+ "Please enter a valid name",
+ );
+ });
+ });
+});
diff --git a/src/IDE/Components/EditableName/EditableName.tsx b/src/IDE/Components/EditableName/EditableName.tsx
new file mode 100644
index 000000000000..074c7ca6a829
--- /dev/null
+++ b/src/IDE/Components/EditableName/EditableName.tsx
@@ -0,0 +1,132 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { Spinner, Text, Tooltip } from "@appsmith/ads";
+import { useEventCallback, useEventListener } from "usehooks-ts";
+import { usePrevious } from "@mantine/hooks";
+import { useNameEditor } from "./useNameEditor";
+
+interface EditableTextProps {
+ name: string;
+ isLoading?: boolean;
+ onNameSave: (name: string) => void;
+ isEditing: boolean;
+ exitEditing: () => void;
+ icon: React.ReactNode;
+ inputTestId?: string;
+}
+
+export const EditableName = ({
+ exitEditing,
+ icon,
+ inputTestId,
+ isEditing,
+ isLoading = false,
+ name,
+ onNameSave,
+}: EditableTextProps) => {
+ const previousName = usePrevious(name);
+ const [editableName, setEditableName] = useState(name);
+ const [validationError, setValidationError] = useState(null);
+ const inputRef = useRef(null);
+
+ const { normalizeName, validateName } = useNameEditor({
+ entityName: name,
+ });
+
+ const handleKeyUp = useEventCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ const nameError = validateName(editableName);
+
+ if (nameError === null) {
+ exitEditing();
+ onNameSave(editableName);
+ } else {
+ setValidationError(nameError);
+ }
+ } else if (e.key === "Escape") {
+ exitEditing();
+ setEditableName(name);
+ setValidationError(null);
+ } else {
+ setValidationError(null);
+ }
+ },
+ );
+
+ const handleTitleChange = useEventCallback(
+ (e: React.ChangeEvent) => {
+ setEditableName(normalizeName(e.target.value));
+ },
+ );
+
+ const inputProps = useMemo(
+ () => ({
+ ["data-testid"]: inputTestId,
+ onKeyUp: handleKeyUp,
+ onChange: handleTitleChange,
+ autoFocus: true,
+ style: { paddingTop: 0, paddingBottom: 0, left: -1, top: -1 },
+ }),
+ [handleKeyUp, handleTitleChange],
+ );
+
+ useEventListener(
+ "focusout",
+ function handleFocusOut() {
+ if (isEditing) {
+ const nameError = validateName(editableName);
+
+ exitEditing();
+
+ if (nameError === null) {
+ onNameSave(editableName);
+ } else {
+ setEditableName(name);
+ setValidationError(null);
+ }
+ }
+ },
+ inputRef,
+ );
+
+ useEffect(
+ function syncEditableTitle() {
+ if (!isEditing && previousName !== name) {
+ setEditableName(name);
+ }
+ },
+ [name, previousName, isEditing],
+ );
+
+ // TODO: This is a temporary fix to focus the input after context retention applies focus to its target
+ // this is a nasty hack to re-focus the input after context retention applies focus to its target
+ // this will be addressed in a future task, likely by a focus retention modification
+ useEffect(
+ function recaptureFocusInEventOfFocusRetention() {
+ const input = inputRef.current;
+
+ if (isEditing && input) {
+ setTimeout(() => {
+ input.focus();
+ }, 200);
+ }
+ },
+ [isEditing],
+ );
+
+ return (
+ <>
+ {isLoading ? : icon}
+
+
+ {editableName}
+
+
+ >
+ );
+};
diff --git a/src/IDE/Components/EditableName/index.ts b/src/IDE/Components/EditableName/index.ts
new file mode 100644
index 000000000000..875359b04b0d
--- /dev/null
+++ b/src/IDE/Components/EditableName/index.ts
@@ -0,0 +1 @@
+export { EditableName } from "./EditableName";
diff --git a/src/utils/hooks/useNameEditor.ts b/src/IDE/Components/EditableName/useNameEditor.ts
similarity index 57%
rename from src/utils/hooks/useNameEditor.ts
rename to src/IDE/Components/EditableName/useNameEditor.ts
index 4dbc0fe6e7f9..40e7bf1d18d0 100644
--- a/src/utils/hooks/useNameEditor.ts
+++ b/src/IDE/Components/EditableName/useNameEditor.ts
@@ -1,27 +1,16 @@
-import { useSelector, useDispatch, shallowEqual } from "react-redux";
-import { isNameValid, removeSpecialChars } from "utils/helpers";
-import type { AppState } from "ee/reducers";
-
-import { getUsedActionNames } from "selectors/actionSelectors";
import {
ACTION_INVALID_NAME_ERROR,
ACTION_NAME_CONFLICT_ERROR,
createMessage,
} from "ee/constants/messages";
-import type { ReduxAction } from "ee/constants/ReduxActionConstants";
+import { shallowEqual, useSelector } from "react-redux";
+import type { AppState } from "ee/reducers";
+import { getUsedActionNames } from "selectors/actionSelectors";
import { useEventCallback } from "usehooks-ts";
-
-interface NameSaveActionParams {
- name: string;
- id: string;
-}
+import { isNameValid, removeSpecialChars } from "utils/helpers";
interface UseNameEditorProps {
- entityId: string;
entityName: string;
- nameSaveAction: (
- params: NameSaveActionParams,
- ) => ReduxAction;
nameErrorMessage?: (name: string) => string;
}
@@ -29,16 +18,7 @@ interface UseNameEditorProps {
* Provides a unified way to validate and save entity names.
*/
export function useNameEditor(props: UseNameEditorProps) {
- const dispatch = useDispatch();
- const {
- entityId,
- entityName,
- nameErrorMessage = ACTION_NAME_CONFLICT_ERROR,
- nameSaveAction,
- } = props;
-
- const isNew =
- new URLSearchParams(window.location.search).get("editName") === "true";
+ const { entityName, nameErrorMessage = ACTION_NAME_CONFLICT_ERROR } = props;
const usedEntityNames = useSelector(
(state: AppState) => getUsedActionNames(state, ""),
@@ -55,16 +35,8 @@ export function useNameEditor(props: UseNameEditorProps) {
return null;
});
- const handleNameSave = useEventCallback((name: string) => {
- if (name !== entityName && validateName(name) === null) {
- dispatch(nameSaveAction({ id: entityId, name }));
- }
- });
-
return {
- isNew,
validateName,
- handleNameSave,
normalizeName: removeSpecialChars,
};
}
diff --git a/src/IDE/Components/FileTab/FileTab.test.tsx b/src/IDE/Components/FileTab/FileTab.test.tsx
index dc1cd307c776..6650b2161add 100644
--- a/src/IDE/Components/FileTab/FileTab.test.tsx
+++ b/src/IDE/Components/FileTab/FileTab.test.tsx
@@ -2,8 +2,7 @@
/* eslint-disable react-perf/jsx-no-jsx-as-prop */
import "@testing-library/jest-dom";
import React from "react";
-import { render, fireEvent, within } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
+import { render, fireEvent } from "@testing-library/react";
import { Icon } from "@appsmith/ads";
import { FileTab } from "./FileTab";
@@ -12,44 +11,29 @@ import { DATA_TEST_ID } from "./constants";
describe("FileTab", () => {
const mockOnClick = jest.fn();
const mockOnClose = jest.fn();
+ const mockOnDoubleClick = jest.fn();
const TITLE = "test_file";
const TabIcon = () => ;
- const KEY_CONFIG = {
- ENTER: { key: "Enter", keyCode: 13 },
- ESC: { key: "Esc", keyCode: 27 },
- };
- const setup = (
- mockEditorConfig: {
- onTitleSave: () => void;
- titleTransformer: (title: string) => string;
- validateTitle: (title: string) => string | null;
- } = {
- onTitleSave: jest.fn(),
- titleTransformer: jest.fn((title) => title),
- validateTitle: jest.fn(() => null),
- },
- isLoading = false,
- ) => {
+ const setup = () => {
const utils = render(
}
isActive
- isChangePermitted
- isLoading={isLoading}
onClick={mockOnClick}
onClose={mockOnClose}
+ onDoubleClick={mockOnDoubleClick}
title={TITLE}
- />,
+ >
+
+ {TITLE}
+ ,
);
const tabElement = utils.getByText(TITLE);
return {
tabElement,
...utils,
- ...mockEditorConfig,
};
};
@@ -65,157 +49,16 @@ describe("FileTab", () => {
expect(mockOnClose).toHaveBeenCalled();
});
- test("renders component in loading state", () => {
- const { getByTestId, tabElement } = setup(undefined, true);
-
- fireEvent.click(tabElement);
- expect(mockOnClick).toHaveBeenCalled();
-
- const spinner = getByTestId(DATA_TEST_ID.SPINNER);
-
- fireEvent.click(spinner);
-
- const closeButton = getByTestId(DATA_TEST_ID.CLOSE_BUTTON);
-
- fireEvent.click(closeButton);
- expect(mockOnClose).toHaveBeenCalled();
- });
-
- test("enters edit mode on double click", () => {
+ test("double click event is fired", () => {
const { getByTestId, tabElement } = setup();
fireEvent.doubleClick(tabElement);
- within(tabElement).getByTestId(DATA_TEST_ID.INPUT);
+
+ expect(mockOnDoubleClick).toHaveBeenCalled();
const closeButton = getByTestId(DATA_TEST_ID.CLOSE_BUTTON);
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
-
- test("valid title actions", async () => {
- const {
- getByTestId,
- getByText,
- onTitleSave,
- queryByText,
- tabElement,
- titleTransformer,
- validateTitle,
- } = setup();
-
- // hit enter
- const enterTitle = "enter_title";
-
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: enterTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(enterTitle);
-
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ENTER);
- expect(titleTransformer).toHaveBeenCalledWith(enterTitle);
- expect(validateTitle).toHaveBeenCalledWith(enterTitle);
- expect(onTitleSave).toHaveBeenCalledWith(enterTitle);
- expect(getByText(enterTitle)).toBeInTheDocument();
-
- // click outside
- const clickOutsideTitle = "click_outside_title";
-
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: clickOutsideTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(clickOutsideTitle);
-
- await userEvent.click(document.body);
- expect(titleTransformer).toHaveBeenCalledWith(clickOutsideTitle);
- expect(validateTitle).toHaveBeenCalledWith(clickOutsideTitle);
- expect(onTitleSave).toHaveBeenCalledWith(clickOutsideTitle);
- expect(getByText(clickOutsideTitle)).toBeInTheDocument();
-
- // hit esc
- const escapeTitle = "escape_title";
-
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: escapeTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(escapeTitle);
-
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ESC);
- expect(queryByText(escapeTitle)).not.toBeInTheDocument();
- expect(getByText(TITLE)).toBeInTheDocument();
-
- // focus out event
- const focusOutTitle = "focus_out_title";
-
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: focusOutTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(focusOutTitle);
-
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ESC);
- expect(queryByText(focusOutTitle)).not.toBeInTheDocument();
- expect(getByText(TITLE)).toBeInTheDocument();
- });
-
- test("invalid title actions", async () => {
- const validationError = "Invalid title";
- const invalidTitle = "else";
-
- const {
- getByTestId,
- getByText,
- queryByText,
- tabElement,
- titleTransformer,
- validateTitle,
- } = setup({
- onTitleSave: jest.fn(),
- titleTransformer: jest.fn((title) => title),
- validateTitle: jest.fn(() => validationError),
- });
-
- // click outside
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: invalidTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(invalidTitle);
-
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ENTER);
- expect(titleTransformer).toHaveBeenCalledWith(invalidTitle);
- expect(validateTitle).toHaveBeenCalledWith(invalidTitle);
- expect(getByText(validationError)).toBeInTheDocument();
-
- await userEvent.click(document.body);
- expect(queryByText(validationError)).not.toBeInTheDocument();
- expect(getByText(TITLE)).toBeInTheDocument();
-
- // escape
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: invalidTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(invalidTitle);
-
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ENTER);
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ESC);
- expect(queryByText(validationError)).not.toBeInTheDocument();
- expect(getByText(TITLE)).toBeInTheDocument();
-
- // focus out event
- fireEvent.doubleClick(tabElement);
- fireEvent.change(getByTestId(DATA_TEST_ID.INPUT), {
- target: { value: invalidTitle },
- });
- expect(titleTransformer).toHaveBeenCalledWith(invalidTitle);
-
- fireEvent.keyUp(getByTestId(DATA_TEST_ID.INPUT), KEY_CONFIG.ENTER);
- fireEvent.focusOut(getByTestId(DATA_TEST_ID.INPUT));
- expect(queryByText(validationError)).not.toBeInTheDocument();
- expect(getByText(TITLE)).toBeInTheDocument();
- });
});
diff --git a/src/IDE/Components/FileTab/FileTab.tsx b/src/IDE/Components/FileTab/FileTab.tsx
index 8198e27050d0..193b5323b5bd 100644
--- a/src/IDE/Components/FileTab/FileTab.tsx
+++ b/src/IDE/Components/FileTab/FileTab.tsx
@@ -1,183 +1,38 @@
-import React, { useEffect, useMemo, useRef, useState } from "react";
+import React from "react";
import clsx from "classnames";
-import { noop } from "lodash";
-import { Icon, Spinner, Tooltip } from "@appsmith/ads";
+import { Icon } from "@appsmith/ads";
import { sanitizeString } from "utils/URLUtils";
-import { useBoolean, useEventCallback, useEventListener } from "usehooks-ts";
-import { usePrevious } from "@mantine/hooks";
import * as Styled from "./styles";
import { DATA_TEST_ID } from "./constants";
export interface FileTabProps {
isActive: boolean;
- isChangePermitted?: boolean;
- isLoading?: boolean;
title: string;
onClick: () => void;
onClose: (e: React.MouseEvent) => void;
- icon?: React.ReactNode;
- editorConfig?: {
- /** Triggered on enter or click outside */
- onTitleSave: (name: string) => void;
- /** Used to normalize title (remove white spaces etc.) */
- titleTransformer: (name: string) => string;
- /** Validates title and returns an error message or null */
- validateTitle: (name: string) => string | null;
- };
+ children: React.ReactNode;
+ onDoubleClick?: () => void;
}
export const FileTab = ({
- editorConfig,
- icon,
+ children,
isActive,
- isChangePermitted = false,
- isLoading = false,
onClick,
onClose,
+ onDoubleClick,
title,
}: FileTabProps) => {
- const {
- setFalse: exitEditMode,
- setTrue: enterEditMode,
- value: isEditing,
- } = useBoolean(false);
-
- const previousTitle = usePrevious(title);
- const [editableTitle, setEditableTitle] = useState(title);
- const currentTitle =
- isEditing || isLoading || title !== editableTitle ? editableTitle : title;
- const [validationError, setValidationError] = useState(null);
- const inputRef = useRef(null);
-
- const handleKeyUp = useEventCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- if (editorConfig) {
- const { onTitleSave, validateTitle } = editorConfig;
- const nameError = validateTitle(editableTitle);
-
- if (nameError === null) {
- exitEditMode();
- onTitleSave(editableTitle);
- } else {
- setValidationError(nameError);
- }
- }
- } else if (e.key === "Escape") {
- exitEditMode();
- setEditableTitle(title);
- setValidationError(null);
- } else {
- setValidationError(null);
- }
- },
- );
-
- const handleTitleChange = useEventCallback(
- (e: React.ChangeEvent) => {
- setEditableTitle(
- editorConfig
- ? editorConfig.titleTransformer(e.target.value)
- : e.target.value,
- );
- },
- );
-
- const handleEnterEditMode = useEventCallback(() => {
- setEditableTitle(title);
- enterEditMode();
- });
-
- const handleDoubleClick =
- editorConfig && isChangePermitted ? handleEnterEditMode : noop;
-
- const inputProps = useMemo(
- () => ({
- ["data-testid"]: DATA_TEST_ID.INPUT,
- onKeyUp: handleKeyUp,
- onChange: handleTitleChange,
- autoFocus: true,
- style: {
- paddingTop: 0,
- paddingBottom: 0,
- left: -1,
- top: -1,
- },
- }),
- [handleKeyUp, handleTitleChange],
- );
-
- useEventListener(
- "focusout",
- function handleFocusOut() {
- if (isEditing && editorConfig) {
- const { onTitleSave, validateTitle } = editorConfig;
- const nameError = validateTitle(editableTitle);
-
- exitEditMode();
-
- if (nameError === null) {
- onTitleSave(editableTitle);
- } else {
- setEditableTitle(title);
- setValidationError(null);
- }
- }
- },
- inputRef,
- );
-
- useEffect(
- function syncEditableTitle() {
- if (!isEditing && previousTitle !== title) {
- setEditableTitle(title);
- }
- },
- [title, previousTitle, isEditing],
- );
-
- // TODO: This is a temporary fix to focus the input after context retention applies focus to its target
- // this is a nasty hack to re-focus the input after context retention applies focus to its target
- // this will be addressed in a future task, likely by a focus retention modification
- useEffect(
- function recaptureFocusInEventOfFocusRetention() {
- const input = inputRef.current;
-
- if (isEditing && input) {
- setTimeout(() => {
- input.focus();
- }, 200);
- }
- },
- [isEditing],
- );
-
return (
- {icon && !isLoading ? (
- {icon}
- ) : null}
- {isLoading && }
-
-
-
- {currentTitle}
-
-
-
+ {children}
{
+const PluginActionNameEditor = ({
+ saveActionName,
+}: PluginActionNameEditorProps) => {
const { action, plugin } = usePluginActionContext();
- const title = action.name;
- const previousTitle = usePrevious(title);
- const [editableTitle, setEditableTitle] = useState(title);
- const [validationError, setValidationError] = useState(null);
- const inputRef = useRef(null);
const isLoading = useSelector(
(state) => getSavingStatusForActionName(state, action?.id || "").isSaving,
);
- const { handleNameSave, normalizeName, validateName } = useNameEditor({
- entityId: action.id,
- entityName: title,
- nameSaveAction: props.saveActionName,
- });
-
const {
setFalse: exitEditMode,
setTrue: enterEditMode,
@@ -85,117 +75,28 @@ const PluginActionNameEditor = (props: PluginActionNameEditorProps) => {
isFeatureEnabled,
action?.userPermissions,
);
-
- const currentTitle =
- isEditing || isLoading || title !== editableTitle ? editableTitle : title;
const iconUrl = getAssetUrl(plugin?.iconLocation) || "";
const icon = ActionUrlIcon(iconUrl);
- const handleKeyUp = useEventCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- const nameError = validateName(editableTitle);
-
- if (nameError === null) {
- exitEditMode();
- handleNameSave(editableTitle);
- } else {
- setValidationError(nameError);
- }
- } else if (e.key === "Escape") {
- exitEditMode();
- setEditableTitle(title);
- setValidationError(null);
- } else {
- setValidationError(null);
- }
- },
- );
-
- const handleTitleChange = useEventCallback(
- (e: React.ChangeEvent) => {
- setEditableTitle(normalizeName(e.target.value));
- },
- );
-
- const handleEnterEditMode = useEventCallback(() => {
- setEditableTitle(title);
- enterEditMode();
- });
-
- const handleDoubleClick = isChangePermitted ? handleEnterEditMode : noop;
-
- const inputProps = useMemo(
- () => ({
- onKeyUp: handleKeyUp,
- onChange: handleTitleChange,
- autoFocus: true,
- style: {
- paddingTop: 0,
- paddingBottom: 0,
- left: -1,
- top: -1,
- },
- }),
- [handleKeyUp, handleTitleChange],
- );
-
- useEventListener(
- "focusout",
- function handleFocusOut() {
- if (isEditing) {
- const nameError = validateName(editableTitle);
-
- exitEditMode();
-
- if (nameError === null) {
- handleNameSave(editableTitle);
- } else {
- setEditableTitle(title);
- setValidationError(null);
- }
- }
- },
- inputRef,
- );
+ const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
- useEffect(
- function syncEditableTitle() {
- if (!isEditing && previousTitle !== title) {
- setEditableTitle(title);
- }
+ const handleNameSave = useCallback(
+ (name: string) => {
+ saveActionName({ id: action.id, name });
},
- [title, previousTitle, isEditing],
- );
-
- useEffect(
- function recaptureFocusInEventOfFocusRetention() {
- const input = inputRef.current;
-
- if (isEditing && input) {
- setTimeout(() => {
- input.focus();
- }, 200);
- }
- },
- [isEditing],
+ [action.id, saveActionName],
);
return (
- {icon && !isLoading ? {icon} : null}
- {isLoading && }
-
-
-
- {currentTitle}
-
-
+ {icon}}
+ isEditing={isEditing}
+ isLoading={isLoading}
+ name={action.name}
+ onNameSave={handleNameSave}
+ />
);
};
diff --git a/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Copy.tsx b/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Copy.tsx
index 7ade07c38535..2847f3481bf9 100644
--- a/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Copy.tsx
+++ b/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Copy.tsx
@@ -7,7 +7,11 @@ import { MenuSub, MenuSubContent, MenuSubTrigger } from "@appsmith/ads";
import { CONTEXT_COPY, createMessage } from "ee/constants/messages";
import { PageMenuItem } from "./PageMenuItem";
-export const Copy = () => {
+interface Props {
+ disabled?: boolean;
+}
+
+export const Copy = ({ disabled }: Props) => {
const menuPages = useSelector(getPageList);
const { action } = usePluginActionContext();
const dispatch = useDispatch();
@@ -26,7 +30,7 @@ export const Copy = () => {
return (
-
+
{createMessage(CONTEXT_COPY)}
diff --git a/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Delete.tsx b/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Delete.tsx
index 498726f28163..41c0243856b2 100644
--- a/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Delete.tsx
+++ b/src/ce/pages/Editor/AppPluginActionEditor/components/ToolbarMenu/Delete.tsx
@@ -7,7 +7,11 @@ import {
} from "ee/constants/messages";
import { MenuItem } from "@appsmith/ads";
-export const Delete = () => {
+interface Props {
+ disabled?: boolean;
+}
+
+export const Delete = ({ disabled }: Props) => {
const { handleDeleteClick } = useHandleDeleteClick();
const [confirmDelete, setConfirmDelete] = useState(false);
@@ -26,6 +30,7 @@ export const Delete = () => {
return (