Skip to content

Commit

Permalink
chore: Adding new name editor for JS object in toolbar (#37056)
Browse files Browse the repository at this point in the history
## Description

Adding new name editor for JS object in toolbar under modularised flow.

Fixes [#36964](#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: 7ae7ec7
> <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 -->
  • Loading branch information
ankitakinger authored Oct 24, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1702304 commit 3d9d08a
Showing 6 changed files with 251 additions and 10 deletions.
5 changes: 4 additions & 1 deletion app/client/src/pages/Editor/JSEditor/Form.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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";

Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
JSObjectNameEditor,
type JSObjectNameEditorProps,
} from "./JSObjectNameEditor";
Original file line number Diff line number Diff line change
@@ -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,

0 comments on commit 3d9d08a

Please sign in to comment.