diff --git a/app/client/src/IDE/Components/FileTab/FileTab.test.tsx b/app/client/src/IDE/Components/FileTab/FileTab.test.tsx
index 38ff125fe0f7..dc1cd307c776 100644
--- a/app/client/src/IDE/Components/FileTab/FileTab.test.tsx
+++ b/app/client/src/IDE/Components/FileTab/FileTab.test.tsx
@@ -37,6 +37,7 @@ describe("FileTab", () => {
editorConfig={mockEditorConfig}
icon={}
isActive
+ isChangePermitted
isLoading={isLoading}
onClick={mockOnClick}
onClose={mockOnClose}
diff --git a/app/client/src/IDE/Components/FileTab/FileTab.tsx b/app/client/src/IDE/Components/FileTab/FileTab.tsx
index 5de086408157..8198e27050d0 100644
--- a/app/client/src/IDE/Components/FileTab/FileTab.tsx
+++ b/app/client/src/IDE/Components/FileTab/FileTab.tsx
@@ -13,6 +13,7 @@ import { DATA_TEST_ID } from "./constants";
export interface FileTabProps {
isActive: boolean;
+ isChangePermitted?: boolean;
isLoading?: boolean;
title: string;
onClick: () => void;
@@ -32,6 +33,7 @@ export const FileTab = ({
editorConfig,
icon,
isActive,
+ isChangePermitted = false,
isLoading = false,
onClick,
onClose,
@@ -89,7 +91,8 @@ export const FileTab = ({
enterEditMode();
});
- const handleDoubleClick = editorConfig ? handleEnterEditMode : noop;
+ const handleDoubleClick =
+ editorConfig && isChangePermitted ? handleEnterEditMode : noop;
const inputProps = useMemo(
() => ({
diff --git a/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx b/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx
index 55f32a5e4cd1..7958facc607b 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx
+++ b/app/client/src/PluginActionEditor/components/PluginActionNameEditor.tsx
@@ -1,16 +1,19 @@
-import React from "react";
+import React, { useEffect, useMemo, useRef, useState } from "react";
import { useSelector } from "react-redux";
-import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import { usePluginActionContext } from "../PluginActionContext";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
-import { PluginType } from "entities/Action";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
-import styled from "styled-components";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ActionUrlIcon } from "pages/Editor/Explorer/ExplorerIcons";
+import { Spinner, Text as ADSText, Tooltip, Flex } from "@appsmith/ads";
+import { usePrevious } from "@mantine/hooks";
+import styled from "styled-components";
+import { useNameEditor } from "utils/hooks/useNameEditor";
+import { useBoolean, useEventCallback, useEventListener } from "usehooks-ts";
+import { noop } from "lodash";
export interface SaveActionNameParams {
id: string;
@@ -23,58 +26,177 @@ export interface PluginActionNameEditorProps {
) => ReduxAction;
}
-const ActionNameEditorWrapper = styled.div`
- & .ads-v2-box {
- gap: var(--ads-v2-spaces-2);
- }
-
- && .t--action-name-edit-field {
- font-size: 12px;
-
- .bp3-editable-text-content {
- height: unset !important;
- line-height: unset !important;
- }
- }
+export const NameWrapper = styled(Flex)`
+ height: 100%;
+ position: relative;
+ font-size: 12px;
+ color: var(--ads-v2-colors-text-default);
+ cursor: pointer;
+ gap: var(--ads-v2-spaces-2);
+ align-items: center;
+ justify-content: center;
+ padding: var(--ads-v2-spaces-3);
+`;
- & .t--plugin-icon-box {
- height: 12px;
+export const IconContainer = styled.div`
+ height: 12px;
+ width: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ img {
width: 12px;
-
- img {
- width: 12px;
- height: auto;
- }
}
`;
+export const Text = styled(ADSText)`
+ min-width: 3ch;
+ padding: 0 var(--ads-v2-spaces-1);
+ font-weight: 500;
+`;
+
const PluginActionNameEditor = (props: 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,
+ value: isEditing,
+ } = useBoolean(false);
+
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
- const saveStatus = useSelector((state) =>
- getSavingStatusForActionName(state, action?.id || ""),
- );
-
+ 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,
+ );
+
+ useEffect(
+ function syncEditableTitle() {
+ if (!isEditing && previousTitle !== title) {
+ setEditableTitle(title);
+ }
+ },
+ [title, previousTitle, isEditing],
+ );
+
+ useEffect(
+ function recaptureFocusInEventOfFocusRetention() {
+ const input = inputRef.current;
+
+ if (isEditing && input) {
+ setTimeout(() => {
+ input.focus();
+ }, 200);
+ }
+ },
+ [isEditing],
+ );
+
return (
-
-
-
+
+ {icon && !isLoading ? {icon} : null}
+ {isLoading && }
+
+
+
+ {currentTitle}
+
+
+
);
};
diff --git a/app/client/src/ce/entities/IDE/constants.ts b/app/client/src/ce/entities/IDE/constants.ts
index 3f1425dce203..0b19c02f6255 100644
--- a/app/client/src/ce/entities/IDE/constants.ts
+++ b/app/client/src/ce/entities/IDE/constants.ts
@@ -122,6 +122,7 @@ export interface EntityItem {
key: string;
icon?: ReactNode;
group?: string;
+ userPermissions?: string[];
}
export type UseRoutes = Array<{
diff --git a/app/client/src/ce/entities/IDE/utils.ts b/app/client/src/ce/entities/IDE/utils.ts
index 9beb96cdf2c2..d119cac82369 100644
--- a/app/client/src/ce/entities/IDE/utils.ts
+++ b/app/client/src/ce/entities/IDE/utils.ts
@@ -7,6 +7,19 @@ import {
BUILDER_PATH,
BUILDER_PATH_DEPRECATED,
} from "ee/constants/routes/appRoutes";
+import { saveActionName } from "actions/pluginActionActions";
+import { saveJSObjectName } from "actions/jsActionActions";
+import { EditorEntityTab, type EntityItem } from "ee/entities/IDE/constants";
+import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
+
+export interface SaveEntityName {
+ params: {
+ name: string;
+ id: string;
+ };
+ segment: EditorEntityTab;
+ entity?: EntityItem;
+}
export const EDITOR_PATHS = [
BUILDER_CUSTOM_PATH,
@@ -35,3 +48,28 @@ export function getIDETypeByUrl(path: string): IDEType {
export function getBaseUrlsForIDEType(type: IDEType): string[] {
return IDEBasePaths[type];
}
+
+export const saveEntityName = ({ params, segment }: SaveEntityName) => {
+ let saveNameAction = saveActionName(params);
+
+ if (EditorEntityTab.JS === segment) {
+ saveNameAction = saveJSObjectName(params);
+ }
+
+ return saveNameAction;
+};
+
+export interface EditableTabPermissions {
+ isFeatureEnabled: boolean;
+ entity?: EntityItem;
+}
+
+export const getEditableTabPermissions = ({
+ entity,
+ isFeatureEnabled,
+}: EditableTabPermissions) => {
+ return getHasManageActionPermission(
+ isFeatureEnabled,
+ entity?.userPermissions || [],
+ );
+};
diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts
index 4f50d471cb0b..03262d1b6489 100644
--- a/app/client/src/ce/selectors/entitiesSelector.ts
+++ b/app/client/src/ce/selectors/entitiesSelector.ts
@@ -59,12 +59,16 @@ import {
import { MAX_DATASOURCE_SUGGESTIONS } from "constants/DatasourceEditorConstants";
import type { CreateNewActionKeyInterface } from "ee/entities/Engine/actionHelpers";
import { getNextEntityName } from "utils/AppsmithUtils";
-import type { EntityItem } from "ee/entities/IDE/constants";
+import { EditorEntityTab, type EntityItem } from "ee/entities/IDE/constants";
import {
ActionUrlIcon,
JsFileIconV2,
} from "pages/Editor/Explorer/ExplorerIcons";
import { getAssetUrl } from "ee/utils/airgapHelpers";
+import {
+ getIsSavingForApiName,
+ getIsSavingForJSObjectName,
+} from "selectors/ui";
export enum GROUP_TYPES {
API = "APIs",
@@ -1650,6 +1654,7 @@ export const getQuerySegmentItems = createSelector(
key: action.config.baseId,
type: action.config.pluginType,
group,
+ userPermissions: action.config.userPermissions,
};
});
@@ -1664,6 +1669,7 @@ export const getJSSegmentItems = createSelector(
title: js.config.name,
key: js.config.baseId,
type: PluginType.JS,
+ userPermissions: js.config.userPermissions,
}));
return items;
@@ -1691,3 +1697,22 @@ export const getDatasourceUsageCountForApp = createSelector(
return actionDsMap;
},
);
+
+export interface IsSavingEntityNameParams {
+ id: string;
+ segment: EditorEntityTab;
+ entity?: EntityItem;
+}
+
+export const getIsSavingEntityName = (
+ state: AppState,
+ { id, segment }: IsSavingEntityNameParams,
+) => {
+ let isSavingEntityName = getIsSavingForApiName(state, id);
+
+ if (EditorEntityTab.JS === segment) {
+ isSavingEntityName = getIsSavingForJSObjectName(state, id);
+ }
+
+ return isSavingEntityName;
+};
diff --git a/app/client/src/components/editorComponents/ActionNameEditor.tsx b/app/client/src/components/editorComponents/ActionNameEditor.tsx
index 1d9bf01aa7cb..7db94c26666d 100644
--- a/app/client/src/components/editorComponents/ActionNameEditor.tsx
+++ b/app/client/src/components/editorComponents/ActionNameEditor.tsx
@@ -17,8 +17,6 @@ import {
} from "ee/constants/messages";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import type { SaveActionNameParams } from "PluginActionEditor";
-import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
-import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import type { Action } from "entities/Action";
import type { ModuleInstance } from "ee/constants/ModuleInstanceConstants";
@@ -49,10 +47,6 @@ function ActionNameEditor(props: ActionNameEditorProps) {
saveStatus,
} = props;
- const isActionRedesignEnabled = useFeatureFlag(
- FEATURE_FLAG.release_actions_redesign_enabled,
- );
-
return (
{
id: string;
onClose: (id: string) => void;
+ entity?: EntityItem;
}
export function EditableTab(props: EditableTabProps) {
- const { icon, id, isActive, onClick, onClose, title } = props;
+ const { entity, icon, id, isActive, onClick, onClose, title } = props;
const { segment } = useCurrentEditorState();
+ const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
+ const isChangePermitted = getEditableTabPermissions({
+ isFeatureEnabled,
+ entity,
+ });
+
const { handleNameSave, normalizeName, validateName } = useNameEditor({
entityId: id,
entityName: title,
- nameSaveAction:
- EditorEntityTab.JS === segment ? saveJSObjectName : saveActionName,
+ nameSaveAction: (params) => saveEntityName({ params, segment, entity }),
});
const isLoading = useSelector((state) =>
- EditorEntityTab.JS === segment
- ? getIsSavingForJSObjectName(state, id)
- : getIsSavingForApiName(state, id),
+ getIsSavingEntityName(state, { id, segment, entity }),
);
const editorConfig = useMemo(
@@ -55,6 +60,7 @@ export function EditableTab(props: EditableTabProps) {
editorConfig={editorConfig}
icon={icon}
isActive={isActive}
+ isChangePermitted={isChangePermitted}
isLoading={isLoading}
onClick={onClick}
onClose={handleClose}
diff --git a/app/client/src/pages/Editor/IDE/EditorTabs/index.tsx b/app/client/src/pages/Editor/IDE/EditorTabs/index.tsx
index 09a923da0704..eeea9f8e7eb6 100644
--- a/app/client/src/pages/Editor/IDE/EditorTabs/index.tsx
+++ b/app/client/src/pages/Editor/IDE/EditorTabs/index.tsx
@@ -35,6 +35,7 @@ const EditorTabs = () => {
const { segment, segmentMode } = useCurrentEditorState();
const { closeClickHandler, tabClickHandler } = useIDETabClickHandlers();
const tabsConfig = TabSelectors[segment];
+ const entities = useSelector(tabsConfig.listSelector, shallowEqual);
const files = useSelector(tabsConfig.tabsSelector, shallowEqual);
const isListViewActive = useSelector(getListViewActiveState);
@@ -122,21 +123,26 @@ const EditorTabs = () => {
gap="spaces-2"
height="100%"
>
- {files.map((tab) => (
-
- ))}
+ {files.map((tab) => {
+ const entity = entities.find((entity) => entity.key === tab.key);
+
+ return (
+
+ );
+ })}