From 3d9d08a6c8544aeb0a6356ea59c8cd5206987d46 Mon Sep 17 00:00:00 2001 From: Ankita Kinger Date: Thu, 24 Oct 2024 17:04:03 +0530 Subject: [PATCH] chore: Adding new name editor for JS object in toolbar (#37056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 7ae7ec74dbe45be237b20a07b56b1c32aa2dbba5 > Cypress dashboard. > Tags: `@tag.Sanity, @tag.JS` > Spec: >
Thu, 24 Oct 2024 10:42:17 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## 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. --- app/client/src/pages/Editor/JSEditor/Form.tsx | 5 +- .../JSEditorToolbar/JSEditorToolbar.tsx | 11 +- .../JSEditor/JSEditorToolbar/JSHeader.tsx | 2 +- .../JSObjectNameEditor/JSObjectNameEditor.tsx | 232 ++++++++++++++++++ .../JSObjectNameEditor/index.tsx | 4 + .../old}/JSObjectNameEditor.tsx | 7 - 6 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/JSObjectNameEditor.tsx create mode 100644 app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/index.tsx rename app/client/src/pages/Editor/JSEditor/{ => JSEditorToolbar/JSObjectNameEditor/old}/JSObjectNameEditor.tsx (90%) diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index 6eabdd909634..68028eb4ebaf 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -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 && ( diff --git a/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSEditorToolbar.tsx b/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSEditorToolbar.tsx index 06f3df58cd2f..d2c6237ebf3c 100644 --- a/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSEditorToolbar.tsx +++ b/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSEditorToolbar.tsx @@ -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 ( - + + {props.showNameEditor && ( + + )} +
ReduxAction; +} + +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(null); + const inputRef = useRef(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) => { + 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 = 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 ( + + ); + } + + return ( + + {currentPlugin && !isLoading ? ( + + {currentPlugin.name} + + ) : null} + {isLoading && } + + + + {currentTitle} + + + + ); +}; diff --git a/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/index.tsx b/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/index.tsx new file mode 100644 index 000000000000..6dd2a6a8ae90 --- /dev/null +++ b/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/index.tsx @@ -0,0 +1,4 @@ +export { + JSObjectNameEditor, + type JSObjectNameEditorProps, +} from "./JSObjectNameEditor"; diff --git a/app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx b/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/old/JSObjectNameEditor.tsx similarity index 90% rename from app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx rename to app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/old/JSObjectNameEditor.tsx index 0ce1112c3c1f..53e1e920850a 100644 --- a/app/client/src/pages/Editor/JSEditor/JSObjectNameEditor.tsx +++ b/app/client/src/pages/Editor/JSEditor/JSEditorToolbar/JSObjectNameEditor/old/JSObjectNameEditor.tsx @@ -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,