chore: Updating plugin action name editor component to use ADS text component (#36960)

## Description

Updating plugin action name editor component to use ADS text component
instead of EditableText from blueprint. Also, adding permission checks
for editable tabs to not allow the name edits when the user doesn't have
the permission.

Fixes [#36793](https://github.com/appsmithorg/appsmith/issues/36793)

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/11402567877>
> Commit: 0b695e8837a4c510df51ff3e7fcb799ed50a3666
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11402567877&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Fri, 18 Oct 2024 12:36:26 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced conditional editing functionality in the FileTab component
based on user permissions.
- Enhanced the PluginActionNameEditor with real-time validation and
improved user interaction.
- Added user permissions information to entity items and selectors for
better access control.
- Integrated new functionality for saving entity names and managing
editable tab permissions.
- Expanded the EditorTabs component to include contextual entity
information for each tab.

- **Bug Fixes**
- Simplified the ActionNameEditor by removing unnecessary feature flag
checks.

- **Documentation**
- Updated interfaces and components to improve clarity and
functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ankita Kinger 2024-10-18 19:07:50 +05:30 committed by GitHub
parent a29f9cadee
commit 9a315eabdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 271 additions and 75 deletions

View File

@ -37,6 +37,7 @@ describe("FileTab", () => {
editorConfig={mockEditorConfig}
icon={<TabIcon />}
isActive
isChangePermitted
isLoading={isLoading}
onClick={mockOnClick}
onClose={mockOnClose}

View File

@ -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(
() => ({

View File

@ -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<SaveActionNameParams>;
}
const ActionNameEditorWrapper = styled.div`
& .ads-v2-box {
gap: var(--ads-v2-spaces-2);
}
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--action-name-edit-field {
font-size: 12px;
.bp3-editable-text-content {
height: unset !important;
line-height: unset !important;
}
}
& .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<string | null>(null);
const inputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<ActionNameEditorWrapper>
<ActionNameEditor
actionConfig={action}
disabled={!isChangePermitted}
enableFontStyling={plugin?.type === PluginType.API}
icon={icon}
saveActionName={props.saveActionName}
saveStatus={saveStatus}
/>
</ActionNameEditorWrapper>
<NameWrapper onDoubleClick={handleDoubleClick}>
{icon && !isLoading ? <IconContainer>{icon}</IconContainer> : null}
{isLoading && <Spinner size="sm" />}
<Tooltip content={validationError} visible={Boolean(validationError)}>
<Text
inputProps={inputProps}
inputRef={inputRef}
isEditable={isEditing}
kind="body-s"
>
{currentTitle}
</Text>
</Tooltip>
</NameWrapper>
);
};

View File

@ -122,6 +122,7 @@ export interface EntityItem {
key: string;
icon?: ReactNode;
group?: string;
userPermissions?: string[];
}
export type UseRoutes = Array<{

View File

@ -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 || [],
);
};

View File

@ -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;
};

View File

@ -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 (
<NameEditorComponent
id={actionConfig?.id}
@ -89,7 +83,7 @@ function ActionNameEditor(props: ActionNameEditorProps) {
editInteractionKind={EditInteractionKind.SINGLE}
errorTooltipClass="t--action-name-edit-error"
forceDefault={forceUpdate}
iconSize={isActionRedesignEnabled ? "sm" : "md"}
iconSize={"md"}
isEditingDefault={isNew}
isInvalid={isInvalidNameForEntity}
onTextChanged={handleNameChange}

View File

@ -2,38 +2,43 @@ import React, { useMemo } from "react";
import { FileTab, type FileTabProps } from "IDE/Components/FileTab";
import { useNameEditor } from "utils/hooks/useNameEditor";
import { EditorEntityTab } from "ee/entities/IDE/constants";
import { saveActionName } from "actions/pluginActionActions";
import { saveJSObjectName } from "actions/jsActionActions";
import { type EntityItem } from "ee/entities/IDE/constants";
import { useCurrentEditorState } from "../hooks";
import {
getIsSavingForApiName,
getIsSavingForJSObjectName,
} from "selectors/ui";
import { 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";
import {
getEditableTabPermissions,
saveEntityName,
} from "ee/entities/IDE/utils";
interface EditableTabProps extends Omit<FileTabProps, "isLoading" | "onClose"> {
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}

View File

@ -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) => (
<EditableTab
icon={tab.icon}
id={tab.key}
isActive={
currentEntity.id === tab.key &&
segmentMode !== EditorEntityTabState.Add &&
!isListViewActive
}
key={tab.key}
onClick={handleTabClick(tab)}
onClose={closeClickHandler}
title={tab.title}
/>
))}
{files.map((tab) => {
const entity = entities.find((entity) => entity.key === tab.key);
return (
<EditableTab
entity={entity}
icon={tab.icon}
id={tab.key}
isActive={
currentEntity.id === tab.key &&
segmentMode !== EditorEntityTabState.Add &&
!isListViewActive
}
key={tab.key}
onClick={handleTabClick(tab)}
onClose={closeClickHandler}
title={tab.title}
/>
);
})}
<AddTab
isListActive={isListViewActive}
newTabClickCallback={handleNewTabClick}