chore: Handling the updation of action name in the plugin action toolbar (#36560)

## Description

Handling the updation of action name in the plugin action toolbar in the
new modularised flow.

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

## 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/11071023786>
> Commit: 73647e50cfeb6919b30c674f8f3a3a219f6f98c8
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11071023786&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Fri, 27 Sep 2024 14:15:24 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 a new component for editing plugin action names, enhancing
user experience in managing plugin actions.
- Added optional icon size property to the editable text component for
improved customization.
- Enhanced the `CommonEditorForm` and `QueryEditorHeader` components to
display plugin-specific information and saving status.

- **Bug Fixes**
- Streamlined action dispatching logic, improving reliability in saving
actions.

- **Documentation**
- Updated interfaces and prop types for better clarity and type safety
in the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ankita Kinger 2024-09-27 19:48:05 +05:30 committed by GitHub
parent b1ed82dbcf
commit 5995e4292a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 194 additions and 93 deletions

View File

@ -0,0 +1,81 @@
import React from "react";
import { useSelector } from "react-redux";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import { usePluginActionContext } from "PluginActionEditor/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";
export interface SaveActionNameParams {
id: string;
name: string;
}
export interface PluginActionNameEditorProps {
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
}
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;
}
}
& .t--plugin-icon-box {
height: 12px;
width: 12px;
img {
width: 12px;
height: auto;
}
}
`;
const PluginActionNameEditor = (props: PluginActionNameEditorProps) => {
const { action, plugin } = usePluginActionContext();
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
const saveStatus = useSelector((state) =>
getSavingStatusForActionName(state, action?.id || ""),
);
const iconUrl = getAssetUrl(plugin?.iconLocation) || "";
const icon = ActionUrlIcon(iconUrl);
return (
<ActionNameEditorWrapper>
<ActionNameEditor
actionConfig={action}
disabled={!isChangePermitted}
enableFontStyling={plugin?.type === PluginType.API}
icon={icon}
saveActionName={props.saveActionName}
saveStatus={saveStatus}
/>
</ActionNameEditorWrapper>
);
};
export default PluginActionNameEditor;

View File

@ -6,3 +6,8 @@ export {
export { default as PluginActionToolbar } from "./components/PluginActionToolbar";
export { default as PluginActionForm } from "./components/PluginActionForm";
export { default as PluginActionResponse } from "./components/PluginActionResponse";
export type {
SaveActionNameParams,
PluginActionNameEditorProps,
} from "./components/PluginActionNameEditor";
export { default as PluginActionNameEditor } from "./components/PluginActionNameEditor";

View File

@ -1,19 +1,13 @@
import React, { memo } from "react";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { removeSpecialChars } from "utils/helpers";
import type { AppState } from "ee/reducers";
import { saveActionName } from "actions/pluginActionActions";
import { Flex } from "@appsmith/ads";
import { getActionByBaseId, getPlugin } from "ee/selectors/entitiesSelector";
import NameEditorComponent, {
IconBox,
IconWrapper,
NameWrapper,
} from "components/utils/NameEditorComponent";
import {
@ -21,14 +15,13 @@ import {
ACTION_NAME_PLACEHOLDER,
createMessage,
} from "ee/constants/messages";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
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";
interface SaveActionNameParams {
id: string;
name: string;
}
interface ActionNameEditorProps {
/*
This prop checks if page is API Pane or Query Pane or Curl Pane
@ -38,38 +31,34 @@ interface ActionNameEditorProps {
*/
enableFontStyling?: boolean;
disabled?: boolean;
saveActionName?: (
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
actionConfig?: Action | ModuleInstance;
icon?: JSX.Element;
saveStatus: { isSaving: boolean; error: boolean };
}
function ActionNameEditor(props: ActionNameEditorProps) {
const params = useParams<{ baseApiId?: string; baseQueryId?: string }>();
const {
actionConfig,
disabled = false,
enableFontStyling = false,
icon = "",
saveActionName,
saveStatus,
} = props;
const currentActionConfig = useSelector((state: AppState) =>
getActionByBaseId(state, params.baseApiId || params.baseQueryId || ""),
);
const currentPlugin = useSelector((state: AppState) =>
getPlugin(state, currentActionConfig?.pluginId || ""),
);
const saveStatus = useSelector((state) =>
getSavingStatusForActionName(state, currentActionConfig?.id || ""),
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
return (
<NameEditorComponent
/**
* This component is used by module editor in EE which uses a different
* action to save the name of an action. The current callers of this component
* pass the existing saveAction action but as fallback the saveActionName is used here
* as a guard.
*/
dispatchAction={props.saveActionName || saveActionName}
id={currentActionConfig?.id}
id={actionConfig?.id}
idUndefinedErrorMessage={ACTION_ID_NOT_FOUND_IN_URL}
name={currentActionConfig?.name}
name={actionConfig?.name}
onSaveName={saveActionName}
saveStatus={saveStatus}
>
{({
@ -85,28 +74,22 @@ function ActionNameEditor(props: ActionNameEditorProps) {
isNew: boolean;
saveStatus: { isSaving: boolean; error: boolean };
}) => (
<NameWrapper enableFontStyling={props.enableFontStyling}>
<NameWrapper enableFontStyling={enableFontStyling}>
<Flex
alignItems="center"
gap="spaces-3"
overflow="hidden"
width="100%"
>
{currentPlugin && (
<IconBox>
<IconWrapper
alt={currentPlugin.name}
src={getAssetUrl(currentPlugin?.iconLocation)}
/>
</IconBox>
)}
{icon && <IconBox className="t--plugin-icon-box">{icon}</IconBox>}
<EditableText
className="t--action-name-edit-field"
defaultValue={currentActionConfig ? currentActionConfig.name : ""}
disabled={props.disabled}
defaultValue={actionConfig ? actionConfig.name : ""}
disabled={disabled}
editInteractionKind={EditInteractionKind.SINGLE}
errorTooltipClass="t--action-name-edit-error"
forceDefault={forceUpdate}
iconSize={isActionRedesignEnabled ? "sm" : "md"}
isEditingDefault={isNew}
isInvalid={isInvalidNameForEntity}
onTextChanged={handleNameChange}

View File

@ -6,7 +6,13 @@ import {
} from "@blueprintjs/core";
import styled from "styled-components";
import _ from "lodash";
import { Button, Spinner, toast, Tooltip } from "@appsmith/ads";
import {
Button,
Spinner,
toast,
Tooltip,
type ButtonSizes,
} from "@appsmith/ads";
import { INVALID_NAME_ERROR, createMessage } from "ee/constants/messages";
export enum EditInteractionKind {
@ -39,6 +45,7 @@ interface EditableTextProps {
minLines?: number;
customErrorTooltip?: string;
useFullWidth?: boolean;
iconSize?: ButtonSizes;
}
// using the !important keyword here is mandatory because a style is being applied to that element using the style attribute
@ -129,6 +136,7 @@ export function EditableText(props: EditableTextProps) {
errorTooltipClass,
forceDefault,
hideEditIcon,
iconSize = "md",
isEditingDefault,
isInvalid,
maxLength,
@ -275,7 +283,7 @@ export function EditableText(props: EditableTextProps) {
className="t--action-name-edit-icon"
isIconButton
kind="tertiary"
size="md"
size={iconSize}
startIcon="pencil-line"
/>
))}

View File

@ -11,6 +11,8 @@ import {
} from "ee/constants/messages";
import styled from "styled-components";
import { Classes } from "@blueprintjs/core";
import type { SaveActionNameParams } from "PluginActionEditor";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
export const NameWrapper = styled.div<{ enableFontStyling?: boolean }>`
min-width: 50%;
@ -71,9 +73,9 @@ interface NameEditorProps {
children: (params: any) => JSX.Element;
id?: string;
name?: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dispatchAction: (a: any) => any;
onSaveName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
suffixErrorMessage?: (params?: any) => string;
@ -90,10 +92,10 @@ interface NameEditorProps {
function NameEditor(props: NameEditorProps) {
const {
dispatchAction,
id: entityId,
idUndefinedErrorMessage,
name: entityName,
onSaveName,
saveStatus,
suffixErrorMessage = ACTION_NAME_CONFLICT_ERROR,
} = props;
@ -131,8 +133,8 @@ function NameEditor(props: NameEditorProps) {
const handleNameChange = useCallback(
(name: string) => {
if (name !== entityName && !isInvalidNameForEntity(name)) {
dispatch(dispatchAction({ id: entityId, name }));
if (name !== entityName && !isInvalidNameForEntity(name) && entityId) {
dispatch(onSaveName({ id: entityId, name }));
}
},
[dispatch, isInvalidNameForEntity, entityId, entityName],

View File

@ -1,11 +1,7 @@
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import type { PaginationField } from "api/ActionAPI";
import React, { createContext, useMemo } from "react";
interface SaveActionNameParams {
id: string;
name: string;
}
import type { SaveActionNameParams } from "PluginActionEditor";
interface ApiEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
@ -15,7 +11,7 @@ interface ApiEditorContextContextProps {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingsConfig: any;
saveActionName?: (
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
closeEditorLink?: React.ReactNode;

View File

@ -35,6 +35,9 @@ import {
InfoFields,
RequestTabs,
} from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
const Form = styled.form`
position: relative;
@ -245,6 +248,18 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
currentActionConfig?.userPermissions,
);
const currentPlugin = useSelector((state: AppState) =>
getPlugin(state, currentActionConfig?.pluginId || ""),
);
const saveStatus = useSelector((state) =>
getSavingStatusForActionName(state, currentActionConfig?.id || ""),
);
const iconUrl = getAssetUrl(currentPlugin?.iconLocation) || "";
const icon = ActionUrlIcon(iconUrl);
const plugin = useSelector((state: AppState) =>
getPlugin(state, pluginId ?? ""),
);
@ -281,9 +296,12 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
<FormRow className="form-row-header">
<NameWrapper className="t--nameOfApi">
<ActionNameEditor
actionConfig={currentActionConfig}
disabled={!isChangePermitted}
enableFontStyling
icon={icon}
saveActionName={saveActionName}
saveStatus={saveStatus}
/>
</NameWrapper>
<ActionButtons className="t--formActionButtons">

View File

@ -8,7 +8,11 @@ import {
getPluginSettingConfigs,
getPlugins,
} from "ee/selectors/entitiesSelector";
import { deleteAction, runAction } from "actions/pluginActionActions";
import {
deleteAction,
runAction,
saveActionName,
} from "actions/pluginActionActions";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import Editor from "./Editor";
import BackToCanvas from "components/common/BackToCanvas";
@ -151,15 +155,7 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
});
dispatch(runAction(action?.id ?? "", paginationField));
},
[
action?.id,
apiName,
pageName,
getPageName,
plugins,
pluginId,
datasourceId,
],
[action?.id, apiName, pageName, plugins, pluginId, datasourceId, dispatch],
);
const actionRightPaneBackLink = useMemo(() => {
@ -173,13 +169,13 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
pageName,
});
dispatch(deleteAction({ id: action?.id ?? "", name: apiName }));
}, [getPageName, pages, basePageId, apiName]);
}, [pages, basePageId, apiName, action?.id, dispatch, pageName]);
const notification = useMemo(() => {
if (!isConverting) return null;
return <ConvertEntityNotification icon={icon} name={action?.name || ""} />;
}, [action?.name, isConverting]);
}, [action?.name, isConverting, icon]);
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
@ -196,6 +192,7 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
handleRunClick={handleRunClick}
moreActionsMenu={moreActionsMenu}
notification={notification}
saveActionName={saveActionName}
settingsConfig={settingsConfig}
>
<Disabler isDisabled={isConverting}>

View File

@ -16,6 +16,8 @@ import {
import { Tooltip } from "@appsmith/ads";
import { useSelector } from "react-redux";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import type { SaveActionNameParams } from "PluginActionEditor";
export const searchHighlightSpanClassName = "token";
export const searchTokenizationDelimiter = "!!";
@ -84,7 +86,7 @@ export interface EntityNameProps {
name: string;
isEditing?: boolean;
onChange?: (name: string) => void;
updateEntityName: (name: string) => void;
updateEntityName: (name: string) => ReduxAction<SaveActionNameParams>;
entityId: string;
searchKeyword?: string;
className?: string;
@ -164,10 +166,10 @@ export const EntityName = React.memo(
return (
<NameEditorComponent
dispatchAction={handleUpdateName}
id={props.entityId}
idUndefinedErrorMessage={ACTION_ID_NOT_FOUND_IN_URL}
name={updatedName}
onSaveName={handleUpdateName}
saveStatus={saveStatus}
suffixErrorMessage={ENTITY_EXPLORER_ACTION_NAME_CONFLICT_ERROR}
>

View File

@ -25,11 +25,8 @@ import NameEditorComponent, {
} from "components/utils/NameEditorComponent";
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import type { SaveActionNameParams } from "PluginActionEditor";
export interface SaveActionNameParams {
id: string;
name: string;
}
export interface JSObjectNameEditorProps {
/*
This prop checks if page is API Pane or Query Pane or Curl Pane
@ -64,10 +61,10 @@ export function JSObjectNameEditor(props: JSObjectNameEditorProps) {
return (
<NameEditorComponent
dispatchAction={props.saveJSObjectName}
id={currentJSObjectConfig?.id}
idUndefinedErrorMessage={JSOBJECT_ID_NOT_FOUND_IN_URL}
name={currentJSObjectConfig?.name}
onSaveName={props.saveJSObjectName}
saveStatus={saveStatus}
>
{({

View File

@ -1,18 +1,14 @@
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
import type { SaveActionNameParams } from "PluginActionEditor";
import React, { createContext, useMemo } from "react";
interface SaveActionNameParams {
id: string;
name: string;
}
interface QueryEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
onCreateDatasourceClick?: () => void;
onEntityNotFoundBackClick?: () => void;
changeQueryPage?: (baseQueryId: string) => void;
actionRightPaneBackLink?: React.ReactNode;
saveActionName?: (
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
closeEditorLink?: React.ReactNode;

View File

@ -13,6 +13,7 @@ import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import { useSelector } from "react-redux";
import {
getActionByBaseId,
getPlugin,
getPluginNameFromId,
} from "ee/selectors/entitiesSelector";
import { QueryEditorContext } from "./QueryEditorContext";
@ -21,6 +22,9 @@ import type { Datasource } from "entities/Datasource";
import type { AppState } from "ee/reducers";
import { SQL_DATASOURCES } from "constants/QueryEditorConstants";
import DatasourceSelector from "./DatasourceSelector";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
const NameWrapper = styled.div`
display: flex;
@ -79,6 +83,18 @@ const QueryEditorHeader = (props: Props) => {
currentActionConfig?.userPermissions,
);
const currentPlugin = useSelector((state: AppState) =>
getPlugin(state, currentActionConfig?.pluginId || ""),
);
const saveStatus = useSelector((state) =>
getSavingStatusForActionName(state, currentActionConfig?.id || ""),
);
const iconUrl = getAssetUrl(currentPlugin?.iconLocation) || "";
const icon = ActionUrlIcon(iconUrl);
// get the current action's plugin name
const currentActionPluginName = useSelector((state: AppState) =>
getPluginNameFromId(state, currentActionConfig?.pluginId || ""),
@ -106,8 +122,11 @@ const QueryEditorHeader = (props: Props) => {
<StyledFormRow>
<NameWrapper>
<ActionNameEditor
actionConfig={currentActionConfig}
disabled={!isChangePermitted}
icon={icon}
saveActionName={saveActionName}
saveStatus={saveStatus}
/>
</NameWrapper>
<ActionsWrapper>

View File

@ -42,6 +42,7 @@ import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import { AppPluginActionEditor } from "../AppPluginActionEditor";
import { saveActionName } from "actions/pluginActionActions";
type QueryEditorProps = RouteComponentProps<QueryEditorRouteParams>;
@ -126,6 +127,7 @@ function QueryEditor(props: QueryEditorProps) {
}, [
action?.id,
action?.name,
action?.pluginType,
isChangePermitted,
isDeletePermitted,
basePageId,
@ -143,7 +145,7 @@ function QueryEditor(props: QueryEditorProps) {
changeQuery({ baseQueryId: baseQueryId, basePageId, applicationId }),
);
},
[basePageId, applicationId],
[basePageId, applicationId, dispatch],
);
const onCreateDatasourceClick = useCallback(() => {
@ -159,13 +161,7 @@ function QueryEditor(props: QueryEditorProps) {
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
entryPoint,
});
}, [
basePageId,
history,
integrationEditorURL,
DatasourceCreateEntryPoints,
AnalyticsUtil,
]);
}, [basePageId]);
// custom function to return user to integrations page if action is not found
const onEntityNotFoundBackClick = useCallback(
@ -176,7 +172,7 @@ function QueryEditor(props: QueryEditorProps) {
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
),
[basePageId, history, integrationEditorURL],
[basePageId],
);
const notification = useMemo(() => {
@ -189,7 +185,7 @@ function QueryEditor(props: QueryEditorProps) {
withPadding
/>
);
}, [action?.name, isConverting]);
}, [action?.name, isConverting, icon]);
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
@ -207,6 +203,7 @@ function QueryEditor(props: QueryEditorProps) {
notification={notification}
onCreateDatasourceClick={onCreateDatasourceClick}
onEntityNotFoundBackClick={onEntityNotFoundBackClick}
saveActionName={saveActionName}
>
<Disabler isDisabled={isConverting}>
<Editor