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:
parent
a29f9cadee
commit
9a315eabdc
|
|
@ -37,6 +37,7 @@ describe("FileTab", () => {
|
|||
editorConfig={mockEditorConfig}
|
||||
icon={<TabIcon />}
|
||||
isActive
|
||||
isChangePermitted
|
||||
isLoading={isLoading}
|
||||
onClick={mockOnClick}
|
||||
onClose={mockOnClose}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export interface EntityItem {
|
|||
key: string;
|
||||
icon?: ReactNode;
|
||||
group?: string;
|
||||
userPermissions?: string[];
|
||||
}
|
||||
|
||||
export type UseRoutes = Array<{
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user