chore: Add Rename context menu (#37116)
This commit is contained in:
parent
a647668814
commit
ac9e101eaf
|
|
@ -1068,6 +1068,10 @@ const ExternalLinkIcon = importRemixIcon(
|
|||
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";
|
||||
|
||||
function PlayIconPNGWrapper() {
|
||||
|
|
@ -1363,6 +1367,7 @@ const ICON_LOOKUP = {
|
|||
"minimize-v3": MinimizeV3Icon,
|
||||
"maximize-v3": MaximizeV3Icon,
|
||||
"workflows-mono": WorkflowsMonochromeIcon,
|
||||
"input-cursor-move": InputCursorMoveIcon,
|
||||
billing: BillingIcon,
|
||||
binding: Binding,
|
||||
book: BookIcon,
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -145,15 +145,13 @@ describe("EditableName", () => {
|
|||
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(getByRole("tooltip").textContent).toEqual(validationError);
|
||||
|
||||
expect(exitEditing).toHaveBeenCalled();
|
||||
expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
|
||||
|
|
@ -169,7 +167,6 @@ describe("EditableName", () => {
|
|||
target: { value: invalidTitle },
|
||||
});
|
||||
|
||||
fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
|
||||
fireEvent.keyUp(inputElement, KEY_CONFIG.ESC);
|
||||
|
||||
expect(getByRole("tooltip")).toBeInTheDocument();
|
||||
|
|
@ -189,9 +186,8 @@ describe("EditableName", () => {
|
|||
target: { value: invalidTitle },
|
||||
});
|
||||
|
||||
fireEvent.keyUp(inputElement, KEY_CONFIG.ENTER);
|
||||
fireEvent.focusOut(inputElement);
|
||||
expect(getByRole("tooltip").textContent).toEqual("");
|
||||
expect(getByRole("tooltip").textContent).toEqual(validationError);
|
||||
expect(exitEditing).toHaveBeenCalled();
|
||||
expect(onNameSave).not.toHaveBeenCalledWith(invalidTitle);
|
||||
});
|
||||
|
|
@ -201,12 +197,12 @@ describe("EditableName", () => {
|
|||
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",
|
||||
);
|
||||
fireEvent.keyUp(input, KEY_CONFIG.ENTER);
|
||||
|
||||
expect(onNameSave).not.toHaveBeenCalledWith("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 { useEventCallback, useEventListener } from "usehooks-ts";
|
||||
import { usePrevious } from "@mantine/hooks";
|
||||
|
|
@ -6,10 +12,19 @@ import { useNameEditor } from "./useNameEditor";
|
|||
|
||||
interface EditableTextProps {
|
||||
name: string;
|
||||
/** isLoading true will show a spinner **/
|
||||
isLoading?: boolean;
|
||||
/** if a valid name is entered, the onNameSave
|
||||
* will be called with the new name */
|
||||
onNameSave: (name: string) => void;
|
||||
/** Used in conjunction with exit editing to control
|
||||
* this component input editable state */
|
||||
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;
|
||||
/** Icon is replaced by spinner when isLoading is shown */
|
||||
icon: React.ReactNode;
|
||||
inputTestId?: string;
|
||||
}
|
||||
|
|
@ -32,30 +47,61 @@ export const EditableName = ({
|
|||
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(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
const nameError = validateName(editableName);
|
||||
|
||||
if (nameError === null) {
|
||||
exitEditing();
|
||||
onNameSave(editableName);
|
||||
} else {
|
||||
setValidationError(nameError);
|
||||
}
|
||||
attemptSave();
|
||||
} else if (e.key === "Escape") {
|
||||
exitEditing();
|
||||
setEditableName(name);
|
||||
setValidationError(null);
|
||||
} else {
|
||||
setValidationError(null);
|
||||
exitWithoutSaving();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleTitleChange = useEventCallback(
|
||||
(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,
|
||||
style: { paddingTop: 0, paddingBottom: 0, left: -1, top: -1 },
|
||||
}),
|
||||
[handleKeyUp, handleTitleChange],
|
||||
[handleKeyUp, handleTitleChange, inputTestId],
|
||||
);
|
||||
|
||||
useEventListener(
|
||||
"focusout",
|
||||
function handleFocusOut() {
|
||||
if (isEditing) {
|
||||
const nameError = validateName(editableName);
|
||||
|
||||
exitEditing();
|
||||
|
||||
if (nameError === null) {
|
||||
onNameSave(editableName);
|
||||
} else {
|
||||
setEditableName(name);
|
||||
setValidationError(null);
|
||||
}
|
||||
attemptSave();
|
||||
}
|
||||
},
|
||||
inputRef,
|
||||
|
|
@ -120,9 +157,9 @@ export const EditableName = ({
|
|||
<Tooltip content={validationError} visible={Boolean(validationError)}>
|
||||
<Text
|
||||
inputProps={inputProps}
|
||||
inputRef={inputRef}
|
||||
isEditable={isEditing}
|
||||
kind="body-s"
|
||||
ref={inputRef}
|
||||
>
|
||||
{editableName}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +1,3 @@
|
|||
export { EditableName } from "./EditableName";
|
||||
export { RenameMenuItem } from "./RenameMenuItem";
|
||||
export { useIsRenaming } from "./useIsRenaming";
|
||||
|
|
|
|||
35
app/client/src/IDE/Components/EditableName/useIsRenaming.ts
Normal file
35
app/client/src/IDE/Components/EditableName/useIsRenaming.ts
Normal 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 };
|
||||
};
|
||||
|
|
@ -6,8 +6,8 @@ import {
|
|||
import { shallowEqual, useSelector } from "react-redux";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { getUsedActionNames } from "selectors/actionSelectors";
|
||||
import { useEventCallback } from "usehooks-ts";
|
||||
import { isNameValid, removeSpecialChars } from "utils/helpers";
|
||||
import { useCallback } from "react";
|
||||
|
||||
interface UseNameEditorProps {
|
||||
entityName: string;
|
||||
|
|
@ -25,15 +25,18 @@ export function useNameEditor(props: UseNameEditorProps) {
|
|||
shallowEqual,
|
||||
);
|
||||
|
||||
const validateName = useEventCallback((name: string): string | null => {
|
||||
if (!name || name.trim().length === 0) {
|
||||
return createMessage(ACTION_INVALID_NAME_ERROR);
|
||||
} else if (name !== entityName && !isNameValid(name, usedEntityNames)) {
|
||||
return createMessage(nameErrorMessage, name);
|
||||
}
|
||||
const validateName = useCallback(
|
||||
(name: string): string | null => {
|
||||
if (!name || name.trim().length === 0) {
|
||||
return createMessage(ACTION_INVALID_NAME_ERROR);
|
||||
} else if (name !== entityName && !isNameValid(name, usedEntityNames)) {
|
||||
return createMessage(nameErrorMessage, name);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
[entityName, nameErrorMessage, usedEntityNames],
|
||||
);
|
||||
|
||||
return {
|
||||
validateName,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,11 @@ export { ToolbarSettingsPopover } from "./Components/ToolbarSettingsPopover";
|
|||
* 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.
|
||||
*/
|
||||
export { EditableName } from "./Components/EditableName";
|
||||
export {
|
||||
EditableName,
|
||||
RenameMenuItem,
|
||||
useIsRenaming,
|
||||
} from "./Components/EditableName";
|
||||
|
||||
/* ====================================================
|
||||
**** Interfaces ****
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { usePluginActionContext } from "../PluginActionContext";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
|
|
@ -8,11 +8,10 @@ import type { ReduxAction } from "ee/constants/ReduxActionConstants";
|
|||
import { getSavingStatusForActionName } from "selectors/actionSelectors";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
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 { useBoolean } from "usehooks-ts";
|
||||
import { noop } from "lodash";
|
||||
import { EditableName } from "IDE";
|
||||
import { EditableName, useIsRenaming } from "IDE";
|
||||
|
||||
export interface SaveActionNameParams {
|
||||
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 = ({
|
||||
saveActionName,
|
||||
}: PluginActionNameEditorProps) => {
|
||||
const { action, plugin } = usePluginActionContext();
|
||||
|
||||
const isLoading = useSelector(
|
||||
(state) => getSavingStatusForActionName(state, action?.id || "").isSaving,
|
||||
(state) => getSavingStatusForActionName(state, action.id).isSaving,
|
||||
);
|
||||
|
||||
const {
|
||||
setFalse: exitEditMode,
|
||||
setTrue: enterEditMode,
|
||||
value: isEditing,
|
||||
} = useBoolean(false);
|
||||
const { enterEditMode, exitEditMode, isEditing } = useIsRenaming(action.id);
|
||||
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
|
|
@ -80,9 +69,11 @@ const PluginActionNameEditor = ({
|
|||
|
||||
const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleNameSave = useCallback(
|
||||
(name: string) => {
|
||||
saveActionName({ id: action.id, name });
|
||||
dispatch(saveActionName({ id: action.id, name }));
|
||||
},
|
||||
[action.id, saveActionName],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,3 +61,10 @@ export const setListViewActiveState = (payload: boolean) => {
|
|||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const setRenameEntity = (id: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SET_RENAME_ENTITY,
|
||||
payload: id,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -500,6 +500,7 @@ const IDEActionTypes = {
|
|||
CLOSE_QUERY_ACTION_TAB_SUCCESS: "CLOSE_QUERY_ACTION_TAB_SUCCESS",
|
||||
SET_IS_LIST_VIEW_ACTIVE: "SET_IS_LIST_VIEW_ACTIVE",
|
||||
OPEN_PLUGIN_ACTION_SETTINGS: "OPEN_PLUGIN_ACTION_SETTINGS",
|
||||
SET_RENAME_ENTITY: "SET_RENAME_ENTITY",
|
||||
};
|
||||
|
||||
const IDEActionErrorTypes = {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { ConvertToModuleCTA } from "../ConvertToModule";
|
|||
import { Move } from "./Move";
|
||||
import { Copy } from "./Copy";
|
||||
import { Delete } from "./Delete";
|
||||
import { Rename } from "./Rename";
|
||||
import { RenameMenuItem } from "IDE";
|
||||
|
||||
export const ToolbarMenu = () => {
|
||||
const { action } = usePluginActionContext();
|
||||
|
|
@ -31,11 +31,10 @@ export const ToolbarMenu = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Rename disabled={!isChangePermitted} />
|
||||
<RenameMenuItem disabled={!isChangePermitted} entityId={action.id} />
|
||||
<ConvertToModuleCTA />
|
||||
<Copy disabled={!isChangePermitted} />
|
||||
<Move disabled={!isChangePermitted} />
|
||||
<MenuSeparator />
|
||||
<Docs />
|
||||
<MenuSeparator />
|
||||
<Delete disabled={!isDeletePermitted} />
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const EditableTextWrapper = styled.div<{
|
|||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
& .${Classes.EDITABLE_TEXT} {
|
||||
background: ${(props) =>
|
||||
props.isEditing && !props.minimal
|
||||
|
|
@ -73,11 +74,13 @@ const EditableTextWrapper = styled.div<{
|
|||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& div.${Classes.EDITABLE_TEXT_INPUT} {
|
||||
text-transform: none;
|
||||
width: 100%;
|
||||
|
|
@ -100,6 +103,7 @@ const TextContainer = styled.div<{
|
|||
color: var(--ads-v2-color-fg-emphasis-plus);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&&&& .${Classes.EDITABLE_TEXT} {
|
||||
& .${Classes.EDITABLE_TEXT_CONTENT} {
|
||||
&:hover {
|
||||
|
|
@ -108,6 +112,7 @@ const TextContainer = styled.div<{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&&& .${Classes.EDITABLE_TEXT_CONTENT}:hover {
|
||||
${(props) =>
|
||||
props.underline
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { FileTab } from "IDE/Components/FileTab";
|
|||
import { type EntityItem } from "ee/entities/IDE/constants";
|
||||
import { useCurrentEditorState } from "../hooks";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { useBoolean, useEventCallback } from "usehooks-ts";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEventCallback } from "usehooks-ts";
|
||||
import { getIsSavingEntityName } from "ee/selectors/entitiesSelector";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
saveEntityName,
|
||||
} from "ee/entities/IDE/utils";
|
||||
import { noop } from "lodash";
|
||||
import { EditableName } from "IDE";
|
||||
import { EditableName, useIsRenaming } from "IDE";
|
||||
import { IconContainer } from "IDE/Components/FileTab/styles";
|
||||
|
||||
interface EditableTabProps {
|
||||
|
|
@ -37,11 +37,7 @@ export function EditableTab(props: EditableTabProps) {
|
|||
entity,
|
||||
});
|
||||
|
||||
const {
|
||||
setFalse: exitEditMode,
|
||||
setTrue: enterEditMode,
|
||||
value: isEditing,
|
||||
} = useBoolean(false);
|
||||
const { enterEditMode, exitEditMode, isEditing } = useIsRenaming(id);
|
||||
|
||||
const isLoading = useSelector((state) =>
|
||||
getIsSavingEntityName(state, { id, segment, entity }),
|
||||
|
|
@ -54,9 +50,11 @@ export function EditableTab(props: EditableTabProps) {
|
|||
|
||||
const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleNameSave = useCallback(
|
||||
(name: string) => {
|
||||
saveEntityName({ params: { id, name }, segment, entity });
|
||||
dispatch(saveEntityName({ params: { id, name }, segment, entity }));
|
||||
exitEditMode();
|
||||
},
|
||||
[entity, exitEditMode, id, segment],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
CONFIRM_CONTEXT_DELETE,
|
||||
CONTEXT_MOVE,
|
||||
createMessage,
|
||||
CONTEXT_RENAME,
|
||||
} from "ee/constants/messages";
|
||||
import { getPageListAsOptions } from "ee/selectors/entitiesSelector";
|
||||
import {
|
||||
|
|
@ -32,6 +33,7 @@ import {
|
|||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import type { JSCollection } from "entities/JSCollection";
|
||||
import { setRenameEntity } from "actions/ideActions";
|
||||
|
||||
interface AppJSEditorContextMenuProps {
|
||||
pageId: string;
|
||||
|
|
@ -56,6 +58,13 @@ export function AppJSEditorContextMenu({
|
|||
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(
|
||||
(actionId: string, actionName: string, pageId: string) => {
|
||||
dispatch(
|
||||
|
|
@ -96,6 +105,14 @@ export function AppJSEditorContextMenu({
|
|||
|
||||
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 = {
|
||||
icon: "duplicate" as IconName,
|
||||
value: "copy",
|
||||
|
|
@ -169,7 +186,7 @@ export function AppJSEditorContextMenu({
|
|||
className: "t--apiFormDeleteBtn error-menuitem",
|
||||
};
|
||||
|
||||
const options: ContextMenuOption[] = [];
|
||||
const options: ContextMenuOption[] = [renameOption];
|
||||
|
||||
if (isChangePermitted) {
|
||||
options.push(copyOption);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
|
|||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { Text as ADSText, Flex } from "@appsmith/ads";
|
||||
import styled from "styled-components";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { noop } from "lodash";
|
||||
import { useParams } from "react-router";
|
||||
import type { AppState } from "ee/reducers";
|
||||
|
|
@ -16,7 +15,7 @@ import {
|
|||
getPlugin,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { JSObjectNameEditor as OldJSObjectNameEditor } from "./old/JSObjectNameEditor";
|
||||
import { EditableName } from "IDE";
|
||||
import { EditableName, useIsRenaming } from "IDE";
|
||||
|
||||
export interface SaveActionNameParams {
|
||||
id: string;
|
||||
|
|
@ -86,11 +85,9 @@ export const JSObjectNameEditor = ({
|
|||
|
||||
const name = currentJSObjectConfig?.name || "";
|
||||
|
||||
const {
|
||||
setFalse: exitEditMode,
|
||||
setTrue: enterEditMode,
|
||||
value: isEditing,
|
||||
} = useBoolean(false);
|
||||
const { enterEditMode, exitEditMode, isEditing } = useIsRenaming(
|
||||
currentJSObjectConfig?.id || "",
|
||||
);
|
||||
|
||||
const handleDoubleClick = disabled ? noop : enterEditMode;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const initialState: IDEState = {
|
|||
tabs: {},
|
||||
isListViewActive: false,
|
||||
showCreateModal: false,
|
||||
renameEntity: "",
|
||||
ideCanvasSideBySideHover: {
|
||||
navigated: false,
|
||||
widgetTypes: [],
|
||||
|
|
@ -110,6 +111,14 @@ const ideReducer = createImmerReducer(initialState, {
|
|||
) => {
|
||||
state.isListViewActive = action.payload;
|
||||
},
|
||||
[ReduxActionTypes.SET_RENAME_ENTITY]: (
|
||||
state: IDEState,
|
||||
action: {
|
||||
payload: string;
|
||||
},
|
||||
) => {
|
||||
state.renameEntity = action.payload;
|
||||
},
|
||||
});
|
||||
|
||||
export interface IDEState {
|
||||
|
|
@ -117,6 +126,7 @@ export interface IDEState {
|
|||
isListViewActive: boolean;
|
||||
tabs: ParentEntityIDETabs;
|
||||
showCreateModal: boolean;
|
||||
renameEntity: string;
|
||||
ideCanvasSideBySideHover: IDECanvasSideBySideHover;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,3 +64,10 @@ export const getIdeCanvasSideBySideHoverState = (state: AppState) =>
|
|||
|
||||
export const getListViewActiveState = (state: AppState) =>
|
||||
state.ui.ide.isListViewActive;
|
||||
|
||||
export const getRenameEntity = (state: AppState) => state.ui.ide.renameEntity;
|
||||
|
||||
export const getIsRenaming = (id: string) =>
|
||||
createSelector(getRenameEntity, (entityId) => {
|
||||
return entityId === id;
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user