chore: Add Rename context menu (#37116)

This commit is contained in:
Hetu Nandu 2024-10-31 17:25:04 +05:30 committed by GitHub
parent a647668814
commit ac9e101eaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 234 additions and 83 deletions

View File

@ -1068,6 +1068,10 @@ const ExternalLinkIcon = importRemixIcon(
async () => import("remixicon-react/ExternalLinkLineIcon"), async () => import("remixicon-react/ExternalLinkLineIcon"),
); );
const InputCursorMoveIcon = importSvg(
async () => import("../__assets__/icons/ads/input-cursor-move.svg"),
);
import PlayIconPNG from "../__assets__/icons/control/play-icon.png"; import PlayIconPNG from "../__assets__/icons/control/play-icon.png";
function PlayIconPNGWrapper() { function PlayIconPNGWrapper() {
@ -1363,6 +1367,7 @@ const ICON_LOOKUP = {
"minimize-v3": MinimizeV3Icon, "minimize-v3": MinimizeV3Icon,
"maximize-v3": MaximizeV3Icon, "maximize-v3": MaximizeV3Icon,
"workflows-mono": WorkflowsMonochromeIcon, "workflows-mono": WorkflowsMonochromeIcon,
"input-cursor-move": InputCursorMoveIcon,
billing: BillingIcon, billing: BillingIcon,
binding: Binding, binding: Binding,
book: BookIcon, book: BookIcon,

View File

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g id="input-cursor-move" clip-path="url(#clip0_3192_40313)">
<path id="Vector"
d="M5.33329 14V12.6667H7.33329V3.33333H5.33329V2H10.6666V3.33333H8.66663V12.6667H10.6666V14H5.33329ZM12.0333 4.7L15.3333 8L12.0333 11.3L11.0906 10.3573L13.448 8L11.0906 5.64267L12.0333 4.7ZM3.96663 4.7L4.90929 5.64267L2.55196 8L4.90929 10.3573L3.96663 11.3L0.666626 8L3.96663 4.7V4.7Z"
fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@ -145,15 +145,13 @@ describe("EditableName", () => {
target: { value: invalidTitle }, target: { value: invalidTitle },
}); });
fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
expect(getByRole("tooltip")).toBeInTheDocument(); expect(getByRole("tooltip")).toBeInTheDocument();
expect(getByRole("tooltip").textContent).toEqual(validationError); expect(getByRole("tooltip").textContent).toEqual(validationError);
await userEvent.click(document.body); await userEvent.click(document.body);
expect(getByRole("tooltip").textContent).toEqual(""); expect(getByRole("tooltip").textContent).toEqual(validationError);
expect(exitEditing).toHaveBeenCalled(); expect(exitEditing).toHaveBeenCalled();
expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle); expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
@ -169,7 +167,6 @@ describe("EditableName", () => {
target: { value: invalidTitle }, target: { value: invalidTitle },
}); });
fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
fireEvent.keyUp(inputElement, KEY_CONFIG.ESC); fireEvent.keyUp(inputElement, KEY_CONFIG.ESC);
expect(getByRole("tooltip")).toBeInTheDocument(); expect(getByRole("tooltip")).toBeInTheDocument();
@ -189,9 +186,8 @@ describe("EditableName", () => {
target: { value: invalidTitle }, target: { value: invalidTitle },
}); });
fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
fireEvent.focusOut(inputElement); fireEvent.focusOut(inputElement);
expect(getByRole("tooltip").textContent).toEqual(""); expect(getByRole("tooltip").textContent).toEqual(validationError);
expect(exitEditing).toHaveBeenCalled(); expect(exitEditing).toHaveBeenCalled();
expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle); expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
}); });
@ -201,12 +197,12 @@ describe("EditableName", () => {
const input = getByRole("textbox"); const input = getByRole("textbox");
fireEvent.change(input, { target: { value: "" } }); fireEvent.change(input, { target: { value: "" } });
fireEvent.keyUp(input, KEY_CONFIG.ENTER);
expect(onNameSave).not.toHaveBeenCalledWith("");
expect(getByRole("tooltip")).toHaveTextContent( expect(getByRole("tooltip")).toHaveTextContent(
"Please enter a valid name", "Please enter a valid name",
); );
fireEvent.keyUp(input, KEY_CONFIG.ENTER);
expect(onNameSave).not.toHaveBeenCalledWith("");
}); });
}); });
}); });

View File

@ -1,4 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Spinner, Text, Tooltip } from "@appsmith/ads"; import { Spinner, Text, Tooltip } from "@appsmith/ads";
import { useEventCallback, useEventListener } from "usehooks-ts"; import { useEventCallback, useEventListener } from "usehooks-ts";
import { usePrevious } from "@mantine/hooks"; import { usePrevious } from "@mantine/hooks";
@ -6,10 +12,19 @@ import { useNameEditor } from "./useNameEditor";
interface EditableTextProps { interface EditableTextProps {
name: string; name: string;
/** isLoading true will show a spinner **/
isLoading?: boolean; isLoading?: boolean;
/** if a valid name is entered, the onNameSave
* will be called with the new name */
onNameSave: (name: string) => void; onNameSave: (name: string) => void;
/** Used in conjunction with exit editing to control
* this component input editable state */
isEditing: boolean; isEditing: boolean;
/** Used in conjunction with exit editing to control this component
* input editable state This function will be called when the
* user is trying to exit the editing mode **/
exitEditing: () => void; exitEditing: () => void;
/** Icon is replaced by spinner when isLoading is shown */
icon: React.ReactNode; icon: React.ReactNode;
inputTestId?: string; inputTestId?: string;
} }
@ -32,30 +47,61 @@ export const EditableName = ({
entityName: name, entityName: name,
}); });
const exitWithoutSaving = useCallback(() => {
exitEditing();
setEditableName(name);
setValidationError(null);
}, [exitEditing, name]);
const validate = useCallback(
(name: string) => {
const nameError = validateName(name);
if (nameError === null) {
setValidationError(null);
} else {
setValidationError(nameError);
}
return nameError;
},
[validateName],
);
const attemptSave = useCallback(() => {
const nameError = validate(editableName);
if (editableName === name) {
exitWithoutSaving();
} else if (nameError === null) {
exitEditing();
onNameSave(editableName);
}
}, [
editableName,
exitEditing,
exitWithoutSaving,
name,
onNameSave,
validate,
]);
const handleKeyUp = useEventCallback( const handleKeyUp = useEventCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") { if (e.key === "Enter") {
const nameError = validateName(editableName); attemptSave();
if (nameError === null) {
exitEditing();
onNameSave(editableName);
} else {
setValidationError(nameError);
}
} else if (e.key === "Escape") { } else if (e.key === "Escape") {
exitEditing(); exitWithoutSaving();
setEditableName(name);
setValidationError(null);
} else {
setValidationError(null);
} }
}, },
); );
const handleTitleChange = useEventCallback( const handleTitleChange = useEventCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableName(normalizeName(e.target.value)); const value = normalizeName(e.target.value);
setEditableName(value);
validate(value);
}, },
); );
@ -67,23 +113,14 @@ export const EditableName = ({
autoFocus: true, autoFocus: true,
style: { paddingTop: 0, paddingBottom: 0, left: -1, top: -1 }, style: { paddingTop: 0, paddingBottom: 0, left: -1, top: -1 },
}), }),
[handleKeyUp, handleTitleChange], [handleKeyUp, handleTitleChange, inputTestId],
); );
useEventListener( useEventListener(
"focusout", "focusout",
function handleFocusOut() { function handleFocusOut() {
if (isEditing) { if (isEditing) {
const nameError = validateName(editableName); attemptSave();
exitEditing();
if (nameError === null) {
onNameSave(editableName);
} else {
setEditableName(name);
setValidationError(null);
}
} }
}, },
inputRef, inputRef,
@ -120,9 +157,9 @@ export const EditableName = ({
<Tooltip content={validationError} visible={Boolean(validationError)}> <Tooltip content={validationError} visible={Boolean(validationError)}>
<Text <Text
inputProps={inputProps} inputProps={inputProps}
inputRef={inputRef}
isEditable={isEditing} isEditable={isEditing}
kind="body-s" kind="body-s"
ref={inputRef}
> >
{editableName} {editableName}
</Text> </Text>

View File

@ -0,0 +1,30 @@
import React, { useCallback } from "react";
import { MenuItem } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import { setRenameEntity } from "actions/ideActions";
interface Props {
disabled?: boolean;
entityId: string;
}
export const RenameMenuItem = ({ disabled, entityId }: Props) => {
const dispatch = useDispatch();
const setRename = useCallback(() => {
// We add a delay to avoid having the focus stuck in the menu trigger
setTimeout(() => {
dispatch(setRenameEntity(entityId));
}, 100);
}, [dispatch, entityId]);
return (
<MenuItem
disabled={disabled}
onSelect={setRename}
startIcon="input-cursor-move"
>
Rename
</MenuItem>
);
};

View File

@ -1 +1,3 @@
export { EditableName } from "./EditableName"; export { EditableName } from "./EditableName";
export { RenameMenuItem } from "./RenameMenuItem";
export { useIsRenaming } from "./useIsRenaming";

View File

@ -0,0 +1,35 @@
import { useDispatch, useSelector } from "react-redux";
import { getIsRenaming } from "selectors/ideSelectors";
import { useCallback, useEffect, useState } from "react";
import { setRenameEntity } from "actions/ideActions";
export const useIsRenaming = (id: string) => {
const dispatch = useDispatch();
const [isEditing, setIsEditing] = useState(false);
const isEditingViaExternal = useSelector(getIsRenaming(id));
useEffect(
function onExternalEditEvent() {
if (isEditingViaExternal) {
setIsEditing(true);
}
return () => {
setIsEditing(false);
};
},
[isEditingViaExternal],
);
const enterEditMode = useCallback(() => {
setIsEditing(true);
}, []);
const exitEditMode = useCallback(() => {
dispatch(setRenameEntity(""));
setIsEditing(false);
}, [dispatch]);
return { isEditing, enterEditMode, exitEditMode };
};

View File

@ -6,8 +6,8 @@ import {
import { shallowEqual, useSelector } from "react-redux"; import { shallowEqual, useSelector } from "react-redux";
import type { AppState } from "ee/reducers"; import type { AppState } from "ee/reducers";
import { getUsedActionNames } from "selectors/actionSelectors"; import { getUsedActionNames } from "selectors/actionSelectors";
import { useEventCallback } from "usehooks-ts";
import { isNameValid, removeSpecialChars } from "utils/helpers"; import { isNameValid, removeSpecialChars } from "utils/helpers";
import { useCallback } from "react";
interface UseNameEditorProps { interface UseNameEditorProps {
entityName: string; entityName: string;
@ -25,15 +25,18 @@ export function useNameEditor(props: UseNameEditorProps) {
shallowEqual, shallowEqual,
); );
const validateName = useEventCallback((name: string): string | null => { const validateName = useCallback(
if (!name || name.trim().length === 0) { (name: string): string | null => {
return createMessage(ACTION_INVALID_NAME_ERROR); if (!name || name.trim().length === 0) {
} else if (name !== entityName && !isNameValid(name, usedEntityNames)) { return createMessage(ACTION_INVALID_NAME_ERROR);
return createMessage(nameErrorMessage, name); } else if (name !== entityName && !isNameValid(name, usedEntityNames)) {
} return createMessage(nameErrorMessage, name);
}
return null; return null;
}); },
[entityName, nameErrorMessage, usedEntityNames],
);
return { return {
validateName, validateName,

View File

@ -69,7 +69,11 @@ export { ToolbarSettingsPopover } from "./Components/ToolbarSettingsPopover";
* EditableName is a component that allows the user to edit the name of an entity * EditableName is a component that allows the user to edit the name of an entity
* It is used in the IDE for renaming pages, actions, queries, etc. * It is used in the IDE for renaming pages, actions, queries, etc.
*/ */
export { EditableName } from "./Components/EditableName"; export {
EditableName,
RenameMenuItem,
useIsRenaming,
} from "./Components/EditableName";
/* ==================================================== /* ====================================================
**** Interfaces **** **** Interfaces ****

View File

@ -1,5 +1,5 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { usePluginActionContext } from "../PluginActionContext"; import { usePluginActionContext } from "../PluginActionContext";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
@ -8,11 +8,10 @@ import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import { getSavingStatusForActionName } from "selectors/actionSelectors"; import { getSavingStatusForActionName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers"; import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ActionUrlIcon } from "pages/Editor/Explorer/ExplorerIcons"; import { ActionUrlIcon } from "pages/Editor/Explorer/ExplorerIcons";
import { Text as ADSText, Flex } from "@appsmith/ads"; import { Flex } from "@appsmith/ads";
import styled from "styled-components"; import styled from "styled-components";
import { useBoolean } from "usehooks-ts";
import { noop } from "lodash"; import { noop } from "lodash";
import { EditableName } from "IDE"; import { EditableName, useIsRenaming } from "IDE";
export interface SaveActionNameParams { export interface SaveActionNameParams {
id: string; id: string;
@ -49,26 +48,16 @@ export const IconContainer = styled.div`
} }
`; `;
export const Text = styled(ADSText)`
min-width: 3ch;
padding: 0 var(--ads-v2-spaces-1);
font-weight: 500;
`;
const PluginActionNameEditor = ({ const PluginActionNameEditor = ({
saveActionName, saveActionName,
}: PluginActionNameEditorProps) => { }: PluginActionNameEditorProps) => {
const { action, plugin } = usePluginActionContext(); const { action, plugin } = usePluginActionContext();
const isLoading = useSelector( const isLoading = useSelector(
(state) => getSavingStatusForActionName(state, action?.id || "").isSaving, (state) => getSavingStatusForActionName(state, action.id).isSaving,
); );
const { const { enterEditMode, exitEditMode, isEditing } = useIsRenaming(action.id);
setFalse: exitEditMode,
setTrue: enterEditMode,
value: isEditing,
} = useBoolean(false);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission( const isChangePermitted = getHasManageActionPermission(
@ -80,9 +69,11 @@ const PluginActionNameEditor = ({
const handleDoubleClick = isChangePermitted ? enterEditMode : noop; const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
const dispatch = useDispatch();
const handleNameSave = useCallback( const handleNameSave = useCallback(
(name: string) => { (name: string) => {
saveActionName({ id: action.id, name }); dispatch(saveActionName({ id: action.id, name }));
}, },
[action.id, saveActionName], [action.id, saveActionName],
); );

View File

@ -61,3 +61,10 @@ export const setListViewActiveState = (payload: boolean) => {
payload, payload,
}; };
}; };
export const setRenameEntity = (id: string) => {
return {
type: ReduxActionTypes.SET_RENAME_ENTITY,
payload: id,
};
};

View File

@ -500,6 +500,7 @@ const IDEActionTypes = {
CLOSE_QUERY_ACTION_TAB_SUCCESS: "CLOSE_QUERY_ACTION_TAB_SUCCESS", CLOSE_QUERY_ACTION_TAB_SUCCESS: "CLOSE_QUERY_ACTION_TAB_SUCCESS",
SET_IS_LIST_VIEW_ACTIVE: "SET_IS_LIST_VIEW_ACTIVE", SET_IS_LIST_VIEW_ACTIVE: "SET_IS_LIST_VIEW_ACTIVE",
OPEN_PLUGIN_ACTION_SETTINGS: "OPEN_PLUGIN_ACTION_SETTINGS", OPEN_PLUGIN_ACTION_SETTINGS: "OPEN_PLUGIN_ACTION_SETTINGS",
SET_RENAME_ENTITY: "SET_RENAME_ENTITY",
}; };
const IDEActionErrorTypes = { const IDEActionErrorTypes = {

View File

@ -14,7 +14,7 @@ import { ConvertToModuleCTA } from "../ConvertToModule";
import { Move } from "./Move"; import { Move } from "./Move";
import { Copy } from "./Copy"; import { Copy } from "./Copy";
import { Delete } from "./Delete"; import { Delete } from "./Delete";
import { Rename } from "./Rename"; import { RenameMenuItem } from "IDE";
export const ToolbarMenu = () => { export const ToolbarMenu = () => {
const { action } = usePluginActionContext(); const { action } = usePluginActionContext();
@ -31,11 +31,10 @@ export const ToolbarMenu = () => {
return ( return (
<> <>
<Rename disabled={!isChangePermitted} /> <RenameMenuItem disabled={!isChangePermitted} entityId={action.id} />
<ConvertToModuleCTA /> <ConvertToModuleCTA />
<Copy disabled={!isChangePermitted} /> <Copy disabled={!isChangePermitted} />
<Move disabled={!isChangePermitted} /> <Move disabled={!isChangePermitted} />
<MenuSeparator />
<Docs /> <Docs />
<MenuSeparator /> <MenuSeparator />
<Delete disabled={!isDeletePermitted} /> <Delete disabled={!isDeletePermitted} />

View File

@ -61,6 +61,7 @@ const EditableTextWrapper = styled.div<{
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
width: 100%; width: 100%;
& .${Classes.EDITABLE_TEXT} { & .${Classes.EDITABLE_TEXT} {
background: ${(props) => background: ${(props) =>
props.isEditing && !props.minimal props.isEditing && !props.minimal
@ -73,11 +74,13 @@ const EditableTextWrapper = styled.div<{
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
&:before, &:before,
&:after { &:after {
display: none; display: none;
} }
} }
& div.${Classes.EDITABLE_TEXT_INPUT} { & div.${Classes.EDITABLE_TEXT_INPUT} {
text-transform: none; text-transform: none;
width: 100%; width: 100%;
@ -100,6 +103,7 @@ const TextContainer = styled.div<{
color: var(--ads-v2-color-fg-emphasis-plus); color: var(--ads-v2-color-fg-emphasis-plus);
display: flex; display: flex;
align-items: center; align-items: center;
&&&& .${Classes.EDITABLE_TEXT} { &&&& .${Classes.EDITABLE_TEXT} {
& .${Classes.EDITABLE_TEXT_CONTENT} { & .${Classes.EDITABLE_TEXT_CONTENT} {
&:hover { &:hover {
@ -108,6 +112,7 @@ const TextContainer = styled.div<{
} }
} }
} }
&&& .${Classes.EDITABLE_TEXT_CONTENT}:hover { &&& .${Classes.EDITABLE_TEXT_CONTENT}:hover {
${(props) => ${(props) =>
props.underline props.underline

View File

@ -4,8 +4,8 @@ import { FileTab } from "IDE/Components/FileTab";
import { type EntityItem } from "ee/entities/IDE/constants"; import { type EntityItem } from "ee/entities/IDE/constants";
import { useCurrentEditorState } from "../hooks"; import { useCurrentEditorState } from "../hooks";
import { useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useBoolean, useEventCallback } from "usehooks-ts"; import { useEventCallback } from "usehooks-ts";
import { getIsSavingEntityName } from "ee/selectors/entitiesSelector"; import { getIsSavingEntityName } from "ee/selectors/entitiesSelector";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
@ -14,7 +14,7 @@ import {
saveEntityName, saveEntityName,
} from "ee/entities/IDE/utils"; } from "ee/entities/IDE/utils";
import { noop } from "lodash"; import { noop } from "lodash";
import { EditableName } from "IDE"; import { EditableName, useIsRenaming } from "IDE";
import { IconContainer } from "IDE/Components/FileTab/styles"; import { IconContainer } from "IDE/Components/FileTab/styles";
interface EditableTabProps { interface EditableTabProps {
@ -37,11 +37,7 @@ export function EditableTab(props: EditableTabProps) {
entity, entity,
}); });
const { const { enterEditMode, exitEditMode, isEditing } = useIsRenaming(id);
setFalse: exitEditMode,
setTrue: enterEditMode,
value: isEditing,
} = useBoolean(false);
const isLoading = useSelector((state) => const isLoading = useSelector((state) =>
getIsSavingEntityName(state, { id, segment, entity }), getIsSavingEntityName(state, { id, segment, entity }),
@ -54,9 +50,11 @@ export function EditableTab(props: EditableTabProps) {
const handleDoubleClick = isChangePermitted ? enterEditMode : noop; const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
const dispatch = useDispatch();
const handleNameSave = useCallback( const handleNameSave = useCallback(
(name: string) => { (name: string) => {
saveEntityName({ params: { id, name }, segment, entity }); dispatch(saveEntityName({ params: { id, name }, segment, entity }));
exitEditMode(); exitEditMode();
}, },
[entity, exitEditMode, id, segment], [entity, exitEditMode, id, segment],

View File

@ -12,6 +12,7 @@ import {
CONFIRM_CONTEXT_DELETE, CONFIRM_CONTEXT_DELETE,
CONTEXT_MOVE, CONTEXT_MOVE,
createMessage, createMessage,
CONTEXT_RENAME,
} from "ee/constants/messages"; } from "ee/constants/messages";
import { getPageListAsOptions } from "ee/selectors/entitiesSelector"; import { getPageListAsOptions } from "ee/selectors/entitiesSelector";
import { import {
@ -32,6 +33,7 @@ import {
import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import type { JSCollection } from "entities/JSCollection"; import type { JSCollection } from "entities/JSCollection";
import { setRenameEntity } from "actions/ideActions";
interface AppJSEditorContextMenuProps { interface AppJSEditorContextMenuProps {
pageId: string; pageId: string;
@ -56,6 +58,13 @@ export function AppJSEditorContextMenu({
jsCollection?.userPermissions || [], jsCollection?.userPermissions || [],
); );
const renameJS = useCallback(() => {
// We add a delay to avoid having the focus stuck in the menu trigger
setTimeout(() => {
dispatch(setRenameEntity(jsCollection.id));
}, 100);
}, []);
const copyJSCollectionToPage = useCallback( const copyJSCollectionToPage = useCallback(
(actionId: string, actionName: string, pageId: string) => { (actionId: string, actionName: string, pageId: string) => {
dispatch( dispatch(
@ -96,6 +105,14 @@ export function AppJSEditorContextMenu({
const menuPages = useSelector(getPageListAsOptions, equal); const menuPages = useSelector(getPageListAsOptions, equal);
const renameOption = {
icon: "input-cursor-move" as IconName,
value: "rename",
onSelect: renameJS,
label: createMessage(CONTEXT_RENAME),
disabled: !isChangePermitted,
};
const copyOption = { const copyOption = {
icon: "duplicate" as IconName, icon: "duplicate" as IconName,
value: "copy", value: "copy",
@ -169,7 +186,7 @@ export function AppJSEditorContextMenu({
className: "t--apiFormDeleteBtn error-menuitem", className: "t--apiFormDeleteBtn error-menuitem",
}; };
const options: ContextMenuOption[] = []; const options: ContextMenuOption[] = [renameOption];
if (isChangePermitted) { if (isChangePermitted) {
options.push(copyOption); options.push(copyOption);

View File

@ -7,7 +7,6 @@ import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers"; import { getAssetUrl } from "ee/utils/airgapHelpers";
import { Text as ADSText, Flex } from "@appsmith/ads"; import { Text as ADSText, Flex } from "@appsmith/ads";
import styled from "styled-components"; import styled from "styled-components";
import { useBoolean } from "usehooks-ts";
import { noop } from "lodash"; import { noop } from "lodash";
import { useParams } from "react-router"; import { useParams } from "react-router";
import type { AppState } from "ee/reducers"; import type { AppState } from "ee/reducers";
@ -16,7 +15,7 @@ import {
getPlugin, getPlugin,
} from "ee/selectors/entitiesSelector"; } from "ee/selectors/entitiesSelector";
import { JSObjectNameEditor as OldJSObjectNameEditor } from "./old/JSObjectNameEditor"; import { JSObjectNameEditor as OldJSObjectNameEditor } from "./old/JSObjectNameEditor";
import { EditableName } from "IDE"; import { EditableName, useIsRenaming } from "IDE";
export interface SaveActionNameParams { export interface SaveActionNameParams {
id: string; id: string;
@ -86,11 +85,9 @@ export const JSObjectNameEditor = ({
const name = currentJSObjectConfig?.name || ""; const name = currentJSObjectConfig?.name || "";
const { const { enterEditMode, exitEditMode, isEditing } = useIsRenaming(
setFalse: exitEditMode, currentJSObjectConfig?.id || "",
setTrue: enterEditMode, );
value: isEditing,
} = useBoolean(false);
const handleDoubleClick = disabled ? noop : enterEditMode; const handleDoubleClick = disabled ? noop : enterEditMode;

View File

@ -15,6 +15,7 @@ const initialState: IDEState = {
tabs: {}, tabs: {},
isListViewActive: false, isListViewActive: false,
showCreateModal: false, showCreateModal: false,
renameEntity: "",
ideCanvasSideBySideHover: { ideCanvasSideBySideHover: {
navigated: false, navigated: false,
widgetTypes: [], widgetTypes: [],
@ -110,6 +111,14 @@ const ideReducer = createImmerReducer(initialState, {
) => { ) => {
state.isListViewActive = action.payload; state.isListViewActive = action.payload;
}, },
[ReduxActionTypes.SET_RENAME_ENTITY]: (
state: IDEState,
action: {
payload: string;
},
) => {
state.renameEntity = action.payload;
},
}); });
export interface IDEState { export interface IDEState {
@ -117,6 +126,7 @@ export interface IDEState {
isListViewActive: boolean; isListViewActive: boolean;
tabs: ParentEntityIDETabs; tabs: ParentEntityIDETabs;
showCreateModal: boolean; showCreateModal: boolean;
renameEntity: string;
ideCanvasSideBySideHover: IDECanvasSideBySideHover; ideCanvasSideBySideHover: IDECanvasSideBySideHover;
} }

View File

@ -64,3 +64,10 @@ export const getIdeCanvasSideBySideHoverState = (state: AppState) =>
export const getListViewActiveState = (state: AppState) => export const getListViewActiveState = (state: AppState) =>
state.ui.ide.isListViewActive; state.ui.ide.isListViewActive;
export const getRenameEntity = (state: AppState) => state.ui.ide.renameEntity;
export const getIsRenaming = (id: string) =>
createSelector(getRenameEntity, (entityId) => {
return entityId === id;
});