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"),
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
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("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { 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,
|
||||||
|
|
|
||||||
|
|
@ -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 ****
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -61,3 +61,10 @@ export const setListViewActiveState = (payload: boolean) => {
|
||||||
payload,
|
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",
|
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 = {
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user