chore: Adding new name editor for JS object in toolbar (#37056)
## Description Adding new name editor for JS object in toolbar under modularised flow. Fixes [#36964](https://github.com/appsmithorg/appsmith/issues/36964) ## Automation /ok-to-test tags="@tag.Sanity, @tag.JS" ### 🔍 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/11496788011> > Commit: 7ae7ec74dbe45be237b20a07b56b1c32aa2dbba5 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11496788011&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity, @tag.JS` > Spec: > <hr>Thu, 24 Oct 2024 10:42:17 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 optional name editor in the JSEditor, allowing users to edit JavaScript object names dynamically. - Enhanced the JSEditorToolbar to conditionally render the name editor based on user permissions. - **Bug Fixes** - Simplified the props for the JSObjectNameEditor component by removing unnecessary properties. - **Documentation** - Updated export statements to ensure accessibility of the new JSObjectNameEditor component across modules. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
1702304905
commit
3d9d08a6c8
|
|
@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import type { JSAction } from "entities/JSCollection";
|
||||
import type { DropdownOnSelect } from "@appsmith/ads-old";
|
||||
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import type { JSObjectNameEditorProps } from "./JSObjectNameEditor";
|
||||
import type { JSObjectNameEditorProps } from "./JSEditorToolbar/JSObjectNameEditor";
|
||||
import {
|
||||
setActiveJSAction,
|
||||
setJsPaneConfigSelectedTab,
|
||||
|
|
@ -68,6 +68,7 @@ interface JSFormProps {
|
|||
hideContextMenuOnEditor?: boolean;
|
||||
hideEditIconOnEditor?: boolean;
|
||||
notification?: React.ReactNode;
|
||||
showNameEditor?: boolean;
|
||||
}
|
||||
|
||||
type Props = JSFormProps;
|
||||
|
|
@ -108,6 +109,7 @@ function JSEditorForm({
|
|||
notification,
|
||||
onUpdateSettings,
|
||||
saveJSObjectName,
|
||||
showNameEditor = false,
|
||||
showSettings = true,
|
||||
}: Props) {
|
||||
const theme = EditorTheme.LIGHT;
|
||||
|
|
@ -353,6 +355,7 @@ function JSEditorForm({
|
|||
onUpdateSettings={onUpdateSettings}
|
||||
saveJSObjectName={saveJSObjectName}
|
||||
selected={selectedJSActionOption}
|
||||
showNameEditor={showNameEditor}
|
||||
showSettings={showSettings}
|
||||
/>
|
||||
{notification && (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { JSHeader } from "./JSHeader";
|
|||
import { JSFunctionSettings } from "./components/JSFunctionSettings";
|
||||
import type { JSFunctionSettingsProps } from "./components/old/JSFunctionSettings";
|
||||
import { convertJSActionsToDropdownOptions } from "./utils";
|
||||
import { JSObjectNameEditor } from "./JSObjectNameEditor";
|
||||
|
||||
interface Props {
|
||||
changePermitted: boolean;
|
||||
|
|
@ -33,6 +34,7 @@ interface Props {
|
|||
jsActions: JSAction[];
|
||||
selected: JSActionDropdownOption;
|
||||
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
|
||||
showNameEditor?: boolean;
|
||||
showSettings: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +61,14 @@ export const JSEditorToolbar = (props: Props) => {
|
|||
// Render the IDEToolbar with JSFunctionRun and JSFunctionSettings components
|
||||
return (
|
||||
<IDEToolbar>
|
||||
<IDEToolbar.Left />
|
||||
<IDEToolbar.Left>
|
||||
{props.showNameEditor && (
|
||||
<JSObjectNameEditor
|
||||
disabled={!props.changePermitted || props.hideEditIconOnEditor}
|
||||
saveJSObjectName={props.saveJSObjectName}
|
||||
/>
|
||||
)}
|
||||
</IDEToolbar.Left>
|
||||
<IDEToolbar.Right>
|
||||
<div className="t--formActionButtons">
|
||||
<JSFunctionRun
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { SaveActionNameParams } from "PluginActionEditor";
|
|||
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
|
||||
import type { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import type { DropdownOnSelect } from "@appsmith/ads-old";
|
||||
import JSObjectNameEditor from "../JSObjectNameEditor";
|
||||
import { JSObjectNameEditor } from "./JSObjectNameEditor";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
import { convertJSActionsToDropdownOptions } from "./utils";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
|
||||
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
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";
|
||||
import { useParams } from "react-router";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import {
|
||||
getJsCollectionByBaseId,
|
||||
getPlugin,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { JSObjectNameEditor as OldJSObjectNameEditor } from "./old/JSObjectNameEditor";
|
||||
|
||||
export interface SaveActionNameParams {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface JSObjectNameEditorProps {
|
||||
disabled?: boolean;
|
||||
saveJSObjectName: (
|
||||
params: SaveActionNameParams,
|
||||
) => ReduxAction<SaveActionNameParams>;
|
||||
}
|
||||
|
||||
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);
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Text = styled(ADSText)`
|
||||
min-width: 3ch;
|
||||
padding: 0 var(--ads-v2-spaces-1);
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const JSObjectNameEditor = (props: JSObjectNameEditorProps) => {
|
||||
const params = useParams<{
|
||||
baseCollectionId?: string;
|
||||
baseQueryId?: string;
|
||||
}>();
|
||||
|
||||
const currentJSObjectConfig = useSelector((state: AppState) =>
|
||||
getJsCollectionByBaseId(state, params.baseCollectionId || ""),
|
||||
);
|
||||
|
||||
const currentPlugin = useSelector((state: AppState) =>
|
||||
getPlugin(state, currentJSObjectConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
const isLoading = useSelector(
|
||||
(state) =>
|
||||
getSavingStatusForJSObjectName(state, currentJSObjectConfig?.id || "")
|
||||
.isSaving,
|
||||
);
|
||||
|
||||
const title = currentJSObjectConfig?.name || "";
|
||||
const previousTitle = usePrevious(title);
|
||||
const [editableTitle, setEditableTitle] = useState(title);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { handleNameSave, normalizeName, validateName } = useNameEditor({
|
||||
entityId: params?.baseCollectionId || "",
|
||||
entityName: title,
|
||||
nameSaveAction: props.saveJSObjectName,
|
||||
});
|
||||
|
||||
const {
|
||||
setFalse: exitEditMode,
|
||||
setTrue: enterEditMode,
|
||||
value: isEditing,
|
||||
} = useBoolean(false);
|
||||
|
||||
const currentTitle =
|
||||
isEditing || isLoading || title !== editableTitle ? editableTitle : title;
|
||||
|
||||
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 = props.disabled ? noop : handleEnterEditMode;
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
if (!isActionRedesignEnabled) {
|
||||
return (
|
||||
<OldJSObjectNameEditor
|
||||
disabled={props.disabled}
|
||||
saveJSObjectName={props.saveJSObjectName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NameWrapper onDoubleClick={handleDoubleClick}>
|
||||
{currentPlugin && !isLoading ? (
|
||||
<IconContainer>
|
||||
<img
|
||||
alt={currentPlugin.name}
|
||||
src={getAssetUrl(currentPlugin.iconLocation)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
JSObjectNameEditor,
|
||||
type JSObjectNameEditorProps,
|
||||
} from "./JSObjectNameEditor";
|
||||
|
|
@ -28,13 +28,6 @@ import type { ReduxAction } from "ee/constants/ReduxActionConstants";
|
|||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
|
||||
export interface JSObjectNameEditorProps {
|
||||
/*
|
||||
This prop checks if page is API Pane or Query Pane or Curl Pane
|
||||
So, that we can toggle between ads editable-text component and existing editable-text component
|
||||
Right now, it's optional so that it doesn't impact any other pages other than API Pane.
|
||||
In future, when default component will be ads editable-text, then we can remove this prop.
|
||||
*/
|
||||
page?: string;
|
||||
disabled?: boolean;
|
||||
saveJSObjectName: (
|
||||
params: SaveActionNameParams,
|
||||
Loading…
Reference in New Issue
Block a user