chore: Replace entities in entity explorer with new ADS templates (#38750)

## Description

Replace entities in entity explorer with new ADS templates

Fixes [#38286](https://github.com/appsmithorg/appsmith/issues/38286)
[#39289](https://github.com/appsmithorg/appsmith/issues/38289)

## 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/12945261277>
> Commit: bf508e2291441102ab2260f39118e269022650b3
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12945261277&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Fri, 24 Jan 2025 08:55:33 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

## Summary by CodeRabbit

## Release Notes

- **New Features**
  - Added new context menu components for JavaScript and Query entities.
- Introduced enhanced entity item rendering for JavaScript and Query
collections.
  - Implemented more granular permission-based context menu actions.
- Added new components: `Rename`, `Copy`, `Move`, `Delete`,
`ShowBindings`, `Prettify`, and `EntityContextMenu`.

- **Improvements**
  - Streamlined context menu functionality across editor interfaces.
  - Enhanced user permissions handling for entity actions.
  - Improved modularity of editor components.
- Updated rendering logic for JavaScript and Query lists based on
feature flags.

- **Bug Fixes**
  - Refined component prop management.
- Updated navigation and analytics event logging for entity
interactions.

- **Refactoring**
  - Simplified component structures.
  - Removed deprecated prop usage.
  - Consolidated import and export statements.

These changes primarily focus on improving the user experience and
developer flexibility within the application's editor interfaces.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ankita Kinger 2025-01-24 16:21:50 +05:30 committed by GitHub
parent eb72ece5e8
commit 8f5aad96df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1105 additions and 526 deletions

View File

@ -2,6 +2,7 @@ import React, { useCallback } from "react";
import { MenuItem } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import { setRenameEntity } from "actions/ideActions";
import { CONTEXT_RENAME, createMessage } from "ee/constants/messages";
interface Props {
disabled?: boolean;
@ -24,7 +25,7 @@ export const RenameMenuItem = ({ disabled, entityId }: Props) => {
onSelect={setRename}
startIcon="input-cursor-move"
>
Rename
{createMessage(CONTEXT_RENAME)}
</MenuItem>
);
};

View File

@ -1,27 +1,7 @@
import React from "react";
import ExplorerJSCollectionEntity from "pages/Editor/Explorer/JSActions/JSActionEntity";
import { Flex } from "@appsmith/ads";
import type { EntityItem } from "ee/entities/IDE/constants";
import { JSEntityItem } from "pages/Editor/IDE/EditorPane/JS/EntityItem/JSEntityItem";
export interface JSListItemProps {
item: EntityItem;
isActive: boolean;
parentEntityId: string;
}
export const JSListItem = (props: JSListItemProps) => {
const { isActive, item, parentEntityId } = props;
return (
<Flex data-testid="t--ide-list-item" flexDirection={"column"}>
<ExplorerJSCollectionEntity
baseCollectionId={item.key}
isActive={isActive}
key={item.key}
parentEntityId={parentEntityId}
searchKeyword={""}
step={1}
/>
</Flex>
);
export const JSEntity = (props: { item: EntityItem }) => {
return <JSEntityItem {...props} />;
};

View File

@ -0,0 +1,27 @@
import React from "react";
import ExplorerJSCollectionEntity from "pages/Editor/Explorer/JSActions/JSActionEntity";
import { Flex } from "@appsmith/ads";
import type { EntityItem } from "ee/entities/IDE/constants";
export interface JSListItemProps {
item: EntityItem;
isActive: boolean;
parentEntityId: string;
}
export const JSListItem = (props: JSListItemProps) => {
const { isActive, item, parentEntityId } = props;
return (
<Flex data-testid="t--ide-list-item" flexDirection={"column"}>
<ExplorerJSCollectionEntity
baseCollectionId={item.key}
isActive={isActive}
key={item.key}
parentEntityId={parentEntityId}
searchKeyword={""}
step={1}
/>
</Flex>
);
};

View File

@ -0,0 +1,20 @@
import React from "react";
import { IDE_TYPE, type IDEType } from "ee/entities/IDE/constants";
import EntityContextMenu from "pages/Editor/IDE/EditorPane/components/EntityContextMenu";
import { AppJSContextMenuItems } from "pages/Editor/IDE/EditorPane/JS/EntityItem/AppJSContextMenuItems";
import type { JSCollection } from "entities/JSCollection";
export const getJSContextMenuByIdeType = (
ideType: IDEType,
jsAction: JSCollection,
) => {
switch (ideType) {
case IDE_TYPE.App: {
return (
<EntityContextMenu>
<AppJSContextMenuItems jsAction={jsAction} />
</EntityContextMenu>
);
}
}
};

View File

@ -1,25 +1,7 @@
import React from "react";
import ExplorerActionEntity from "pages/Editor/Explorer/Actions/ActionEntity";
import { QueryEntityItem } from "pages/Editor/IDE/EditorPane/Query/EntityItem/QueryEntityItem";
import type { EntityItem } from "ee/entities/IDE/constants";
export interface QueryListItemProps {
item: EntityItem;
isActive: boolean;
parentEntityId: string;
}
export const QueryListItem = (props: QueryListItemProps) => {
const { isActive, item, parentEntityId } = props;
return (
<ExplorerActionEntity
baseId={item.key}
isActive={isActive}
key={item.key}
parentEntityId={parentEntityId}
searchKeyword={""}
step={1}
type={item.type}
/>
);
export const ActionEntityItem = (props: { item: EntityItem }) => {
return <QueryEntityItem {...props} />;
};

View File

@ -0,0 +1,25 @@
import React from "react";
import ExplorerActionEntity from "pages/Editor/Explorer/Actions/ActionEntity";
import type { EntityItem } from "ee/entities/IDE/constants";
export interface QueryListItemProps {
item: EntityItem;
isActive: boolean;
parentEntityId: string;
}
export const QueryListItem = (props: QueryListItemProps) => {
const { isActive, item, parentEntityId } = props;
return (
<ExplorerActionEntity
baseId={item.key}
isActive={isActive}
key={item.key}
parentEntityId={parentEntityId}
searchKeyword={""}
step={1}
type={item.type}
/>
);
};

View File

@ -0,0 +1,20 @@
import React from "react";
import { IDE_TYPE, type IDEType } from "ee/entities/IDE/constants";
import type { Action } from "entities/Action";
import { AppQueryContextMenuItems } from "pages/Editor/IDE/EditorPane/Query/EntityItem/AppQueryContextMenuItems";
import EntityContextMenu from "pages/Editor/IDE/EditorPane/components/EntityContextMenu";
export const getQueryContextMenuByIdeType = (
ideType: IDEType,
action: Action,
) => {
switch (ideType) {
case IDE_TYPE.App: {
return (
<EntityContextMenu>
<AppQueryContextMenuItems action={action} />
</EntityContextMenu>
);
}
}
};

View File

@ -0,0 +1 @@
export * from "ce/pages/Editor/IDE/EditorPane/JS/old/ListItem";

View File

@ -0,0 +1 @@
export * from "ce/pages/Editor/IDE/EditorPane/JS/utils/getJSContextMenuByIdeType";

View File

@ -0,0 +1 @@
export * from "ce/pages/Editor/IDE/EditorPane/Query/old/ListItem";

View File

@ -0,0 +1 @@
export * from "ce/pages/Editor/IDE/EditorPane/Query/utils/getQueryContextMenuByIdeType";

View File

@ -1,5 +1,4 @@
import React from "react";
import { usePluginActionContext } from "PluginActionEditor";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { useSelector } from "react-redux";
@ -11,21 +10,27 @@ import {
import { MODULE_TYPE } from "ee/constants/ModuleConstants";
import ConvertToModuleInstanceCTA from "ee/pages/Editor/EntityEditor/ConvertToModuleInstanceCTA";
import { PluginType } from "entities/Plugin";
import type { Action } from "entities/Action";
const ConvertToModuleCTA = () => {
const { action, plugin } = usePluginActionContext();
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
interface Props {
action: Action;
}
export const ConvertToModule = ({ action }: Props) => {
const pagePermissions = useSelector(getPagePermissions);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isCreatePermitted = getHasCreateActionPermission(
isFeatureEnabled,
pagePermissions,
);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
action.userPermissions,
);
if (plugin.type === PluginType.INTERNAL) {
if (action.pluginType === PluginType.INTERNAL) {
// Workflow queries cannot be converted to modules
return null;
}
@ -39,5 +44,3 @@ const ConvertToModuleCTA = () => {
return <ConvertToModuleInstanceCTA {...convertToModuleProps} />;
};
export default ConvertToModuleCTA;

View File

@ -1,19 +1,20 @@
import { useDispatch, useSelector } from "react-redux";
import { getPageList } from "ee/selectors/entitiesSelector";
import { usePluginActionContext } from "PluginActionEditor";
import React, { useCallback } from "react";
import { copyActionRequest } from "actions/pluginActionActions";
import React from "react";
import { MenuSub, MenuSubContent, MenuSubTrigger } from "@appsmith/ads";
import { CONTEXT_COPY, createMessage } from "ee/constants/messages";
import { useDispatch, useSelector } from "react-redux";
import { getPageList } from "selectors/editorSelectors";
import { PageMenuItem } from "./PageMenuItem";
import { useCallback } from "react";
import type { Action } from "entities/Action";
import { copyActionRequest } from "actions/pluginActionActions";
import { CONTEXT_COPY, createMessage } from "ee/constants/messages";
interface Props {
action: Action;
disabled?: boolean;
}
export const Copy = ({ disabled }: Props) => {
export const Copy = ({ action, disabled }: Props) => {
const menuPages = useSelector(getPageList);
const { action } = usePluginActionContext();
const dispatch = useDispatch();
const copyActionToPage = useCallback(
@ -30,13 +31,14 @@ export const Copy = ({ disabled }: Props) => {
return (
<MenuSub>
<MenuSubTrigger disabled={disabled} startIcon="duplicate">
<MenuSubTrigger startIcon="duplicate">
{createMessage(CONTEXT_COPY)}
</MenuSubTrigger>
<MenuSubContent>
<MenuSubContent style={{ maxHeight: "350px" }} width="220px">
{menuPages.map((page) => {
return (
<PageMenuItem
disabled={disabled}
key={page.basePageId}
onSelect={copyActionToPage}
page={page}

View File

@ -1,4 +1,3 @@
import { useHandleDeleteClick } from "PluginActionEditor/hooks";
import React, { useCallback, useState } from "react";
import {
CONFIRM_CONTEXT_DELETE,
@ -6,19 +5,37 @@ import {
createMessage,
} from "ee/constants/messages";
import { MenuItem } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import { deleteAction } from "actions/pluginActionActions";
import type { Action } from "entities/Action";
interface Props {
action: Action;
disabled?: boolean;
}
export const Delete = ({ disabled }: Props) => {
const { handleDeleteClick } = useHandleDeleteClick();
export const Delete = ({ action, disabled }: Props) => {
const dispatch = useDispatch();
const [confirmDelete, setConfirmDelete] = useState(false);
const handleDeleteClick = useCallback(
({ onSuccess }: { onSuccess?: () => void }) => {
dispatch(
deleteAction({
id: action?.id ?? "",
name: action?.name ?? "",
onSuccess,
}),
);
},
[action.id, action.name, dispatch],
);
const handleSelect = useCallback(
(e?: Event) => {
e?.preventDefault();
confirmDelete ? handleDeleteClick({}) : setConfirmDelete(true);
e?.stopPropagation();
},
[confirmDelete, handleDeleteClick],
);
@ -29,7 +46,7 @@ export const Delete = ({ disabled }: Props) => {
return (
<MenuItem
className="t--apiFormDeleteBtn error-menuitem"
className="t--apiFormDeleteBtn single-select error-menuitem"
disabled={disabled}
onSelect={handleSelect}
startIcon="trash"

View File

@ -1,5 +1,4 @@
import { useDispatch, useSelector } from "react-redux";
import { usePluginActionContext } from "PluginActionEditor";
import { getCurrentPageId } from "selectors/editorSelectors";
import { getPageList } from "ee/selectors/entitiesSelector";
import React, { useCallback, useMemo } from "react";
@ -12,14 +11,15 @@ import {
} from "@appsmith/ads";
import { CONTEXT_MOVE, createMessage } from "ee/constants/messages";
import { PageMenuItem } from "./PageMenuItem";
import type { Action } from "entities/Action";
interface Props {
action: Action;
disabled?: boolean;
}
export const Move = ({ disabled }: Props) => {
export const Move = ({ action, disabled }: Props) => {
const dispatch = useDispatch();
const { action } = usePluginActionContext();
const currentPageId = useSelector(getCurrentPageId);
const allPages = useSelector(getPageList);
@ -42,14 +42,15 @@ export const Move = ({ disabled }: Props) => {
return (
<MenuSub>
<MenuSubTrigger disabled={disabled} startIcon="swap-horizontal">
<MenuSubTrigger startIcon="swap-horizontal">
{createMessage(CONTEXT_MOVE)}
</MenuSubTrigger>
<MenuSubContent>
<MenuSubContent style={{ maxHeight: "350px" }} width="220px">
{menuPages.length ? (
menuPages.map((page) => {
return (
<PageMenuItem
disabled={disabled}
key={page.basePageId}
onSelect={moveActionToPage}
page={page}

View File

@ -5,10 +5,15 @@ import { MenuItem } from "@appsmith/ads";
export const PageMenuItem = (props: {
page: Page;
onSelect: (id: string) => void;
disabled?: boolean;
}) => {
const handleOnSelect = useCallback(() => {
props.onSelect(props.page.pageId);
}, [props]);
return <MenuItem onSelect={handleOnSelect}>{props.page.pageName}</MenuItem>;
return (
<MenuItem disabled={props.disabled} onSelect={handleOnSelect}>
{props.page.pageName}
</MenuItem>
);
};

View File

@ -0,0 +1,32 @@
import React, { useCallback } from "react";
import { MenuItem } from "@appsmith/ads";
import type { Action } from "entities/Action";
import { useDispatch } from "react-redux";
import { initExplorerEntityNameEdit } from "actions/explorerActions";
import { CONTEXT_RENAME, createMessage } from "ee/constants/messages";
interface Props {
action: Action;
disabled?: boolean;
}
export const Rename = ({ action, disabled }: Props) => {
const dispatch = useDispatch();
const setRename = useCallback(() => {
// We add a delay to avoid having the focus stuck in the menu trigger
setTimeout(() => {
dispatch(initExplorerEntityNameEdit(action.id));
}, 100);
}, [dispatch, action.id]);
return (
<MenuItem
disabled={disabled}
onSelect={setRename}
startIcon="input-cursor-move"
>
{createMessage(CONTEXT_RENAME)}
</MenuItem>
);
};

View File

@ -0,0 +1,38 @@
import React, { useCallback } from "react";
import { CONTEXT_SHOW_BINDING, createMessage } from "ee/constants/messages";
import { MenuItem } from "@appsmith/ads";
import type { Action } from "entities/Action";
import { useDispatch } from "react-redux";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
interface Props {
action: Action;
disabled?: boolean;
}
export const ShowBindings = ({ action, disabled }: Props) => {
const dispatch = useDispatch();
const handleSelect = useCallback(() => {
dispatch({
type: ReduxActionTypes.SET_ENTITY_INFO,
payload: {
entityId: action.id,
entityName: action.name,
entityType: ENTITY_TYPE.ACTION,
show: true,
},
});
}, [action.id, action.name]);
return (
<MenuItem
disabled={disabled}
onSelect={handleSelect}
startIcon="binding-new"
>
{createMessage(CONTEXT_SHOW_BINDING)}
</MenuItem>
);
};

View File

@ -0,0 +1,6 @@
export { Copy } from "./Copy";
export { Move } from "./Move";
export { Delete } from "./Delete";
export { Rename } from "./Rename";
export { ShowBindings } from "./ShowBindings";
export { ConvertToModule } from "./ConvertToModule";

View File

@ -1,3 +1,2 @@
export { default as ConvertToModuleCTA } from "./ConvertToModuleCTA";
export { default as ConvertToModuleDisabler } from "./ConvertToModuleDisabler";
export { default as ConvertToModuleCallout } from "./ConvertToModuleCallout";

View File

@ -1,14 +0,0 @@
import React from "react";
import { MenuItem } from "@appsmith/ads";
interface Props {
disabled?: boolean;
}
export const Rename = ({ disabled }: Props) => {
return (
<MenuItem disabled={disabled} startIcon="input-cursor-move">
Rename
</MenuItem>
);
};

View File

@ -10,10 +10,7 @@ import {
usePluginActionContext,
DocsMenuItem as Docs,
} from "PluginActionEditor";
import { ConvertToModuleCTA } from "../ConvertToModule";
import { Move } from "./Move";
import { Copy } from "./Copy";
import { Delete } from "./Delete";
import { ConvertToModule, Copy, Delete, Move } from "../ContextMenuItems";
import { RenameMenuItem } from "IDE";
export const ToolbarMenu = () => {
@ -32,12 +29,12 @@ export const ToolbarMenu = () => {
return (
<>
<RenameMenuItem disabled={!isChangePermitted} entityId={action.id} />
<ConvertToModuleCTA />
<Copy disabled={!isChangePermitted} />
<Move disabled={!isChangePermitted} />
<ConvertToModule action={action} />
<Copy action={action} disabled={!isChangePermitted} />
<Move action={action} disabled={!isChangePermitted} />
<Docs />
<MenuSeparator />
<Delete disabled={!isDeletePermitted} />
<Delete action={action} disabled={!isDeletePermitted} />
</>
);
};

View File

@ -140,7 +140,7 @@ function Files() {
);
}
}),
[files, activeActionBaseId, parentEntityId, parentEntityType],
[files, activeActionBaseId, parentEntityId],
);
const handleClick = useCallback(

View File

@ -0,0 +1,48 @@
import React from "react";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import {
getHasDeleteActionPermission,
getHasManageActionPermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import type { JSCollection } from "entities/JSCollection";
import {
Copy,
Delete,
Move,
Rename,
ShowBindings,
} from "pages/Editor/JSEditor/ContextMenuItems";
import { MenuSeparator } from "@appsmith/ads";
export interface Props {
jsAction: JSCollection;
}
export function AppJSContextMenuItems(props: Props) {
const { jsAction } = props;
const jsActionPermissions = jsAction.userPermissions || [];
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const canDeleteJSAction = getHasDeleteActionPermission(
isFeatureEnabled,
jsActionPermissions,
);
const canManageJSAction = getHasManageActionPermission(
isFeatureEnabled,
jsActionPermissions,
);
return (
<>
<Rename disabled={!canManageJSAction} jsAction={jsAction} />
<ShowBindings jsAction={jsAction} />
<Copy disabled={!canManageJSAction} jsAction={jsAction} />
<Move disabled={!canManageJSAction} jsAction={jsAction} />
<MenuSeparator />
<Delete disabled={!canDeleteJSAction} jsAction={jsAction} />
</>
);
}

View File

@ -0,0 +1,109 @@
import React, { useCallback, useMemo } from "react";
import { EntityItem } from "@appsmith/ads";
import type { EntityItem as EntityItemProps } from "ee/entities/IDE/constants";
import type { AppState } from "ee/reducers";
import { getJsCollectionByBaseId } from "ee/selectors/entitiesSelector";
import { useDispatch, useSelector } from "react-redux";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import history, { NavigationMethod } from "utils/history";
import { saveJSObjectNameBasedOnIdeType } from "ee/actions/helpers";
import { useNameEditorState } from "pages/Editor/IDE/EditorPane/hooks/useNameEditorState";
import { useValidateEntityName } from "IDE";
import { useLocation } from "react-router";
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import { useParentEntityInfo } from "ee/IDE/hooks/useParentEntityInfo";
import type { JSCollection } from "entities/JSCollection";
import { jsCollectionIdURL } from "ee/RouteBuilder";
import { JsFileIconV2 } from "pages/Editor/Explorer/ExplorerIcons";
import { getJSContextMenuByIdeType } from "ee/pages/Editor/IDE/EditorPane/JS/utils/getJSContextMenuByIdeType";
export const JSEntityItem = ({ item }: { item: EntityItemProps }) => {
const jsAction = useSelector((state: AppState) =>
getJsCollectionByBaseId(state, item.key),
) as JSCollection;
const location = useLocation();
const ideType = getIDETypeByUrl(location.pathname);
const activeActionBaseId = useActiveActionBaseId();
const { parentEntityId } = useParentEntityInfo(ideType);
const { editingEntity, enterEditMode, exitEditMode, updatingEntity } =
useNameEditorState();
const validateName = useValidateEntityName({
entityName: item.title,
});
const dispatch = useDispatch();
const contextMenu = getJSContextMenuByIdeType(ideType, jsAction);
const jsActionPermissions = jsAction.userPermissions || [];
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const canManageJSAction = getHasManageActionPermission(
isFeatureEnabled,
jsActionPermissions,
);
const navigateToUrl = jsCollectionIdURL({
baseParentEntityId: parentEntityId,
baseCollectionId: jsAction.baseId,
params: {},
});
const navigateToJSCollection = useCallback(() => {
if (jsAction.baseId) {
AnalyticsUtil.logEvent("ENTITY_EXPLORER_CLICK", {
type: "JSOBJECT",
fromUrl: location.pathname,
toUrl: navigateToUrl,
name: jsAction.name,
});
history.push(navigateToUrl, {
invokedBy: NavigationMethod.EntityExplorer,
});
}
}, [parentEntityId, jsAction.baseId, jsAction.name, location.pathname]);
const nameEditorConfig = useMemo(() => {
return {
canEdit: canManageJSAction && !Boolean(jsAction.isMainJSCollection),
isEditing: editingEntity === jsAction.id,
isLoading: updatingEntity === jsAction.id,
onEditComplete: exitEditMode,
onNameSave: (newName: string) =>
dispatch(saveJSObjectNameBasedOnIdeType(jsAction.id, newName, ideType)),
validateName: (newName: string) => validateName(newName, item.title),
};
}, [
canManageJSAction,
editingEntity,
exitEditMode,
ideType,
item.title,
jsAction.id,
jsAction.isMainJSCollection,
dispatch,
updatingEntity,
validateName,
]);
return (
<EntityItem
className="t--jsaction"
id={jsAction.id}
isSelected={activeActionBaseId === jsAction.id}
key={jsAction.id}
nameEditorConfig={nameEditorConfig}
onClick={navigateToJSCollection}
onDoubleClick={() => enterEditMode(jsAction.id)}
rightControl={contextMenu}
rightControlVisibility="hover"
startIcon={JsFileIconV2(16, 16)}
title={item.title}
/>
);
};

View File

@ -1,25 +1,31 @@
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { Flex, Text, SearchAndAdd, NoSearchResults } from "@appsmith/ads";
import {
Flex,
Text,
SearchAndAdd,
NoSearchResults,
EntityGroupsList,
} from "@appsmith/ads";
import styled from "styled-components";
import { selectJSSegmentEditorList } from "ee/selectors/appIDESelectors";
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import {
getCurrentApplicationId,
getCurrentPageId,
getPagePermissions,
} from "selectors/editorSelectors";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasCreateActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { ActionParentEntityType } from "ee/entities/Engine/actionHelpers";
import { FilesContextProvider } from "pages/Editor/Explorer/Files/FilesContextProvider";
import { useJSAdd } from "ee/pages/Editor/IDE/EditorPane/JS/hooks";
import { JSListItem } from "ee/pages/Editor/IDE/EditorPane/JS/ListItem";
import { JSListItem } from "ee/pages/Editor/IDE/EditorPane/JS/old/ListItem";
import { BlankState } from "./BlankState";
import { EDITOR_PANE_TEXTS, createMessage } from "ee/constants/messages";
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
import { useLocation } from "react-router";
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
import { useParentEntityInfo } from "ee/IDE/hooks/useParentEntityInfo";
import { useCreateActionsPermissions } from "ee/entities/IDE/hooks/useCreateActionsPermissions";
import type { EntityItem } from "ee/entities/IDE/constants";
import { JSEntity } from "ee/pages/Editor/IDE/EditorPane/JS/ListItem";
const JSContainer = styled(Flex)`
& .t--entity-item {
@ -30,25 +36,23 @@ const JSContainer = styled(Flex)`
const ListJSObjects = () => {
const [searchTerm, setSearchTerm] = useState("");
const pageId = useSelector(getCurrentPageId);
const itemGroups = useSelector(selectJSSegmentEditorList);
const activeActionBaseId = useActiveActionBaseId();
const applicationId = useSelector(getCurrentApplicationId);
const pagePermissions = useSelector(getPagePermissions);
const location = useLocation();
const ideType = getIDETypeByUrl(location.pathname);
const { editorId, parentEntityId } = useParentEntityInfo(ideType);
const canCreateActions = useCreateActionsPermissions(ideType);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isNewADSTemplatesEnabled = useFeatureFlag(
FEATURE_FLAG.release_ads_entity_item_enabled,
);
const filteredItemGroups = filterEntityGroupsBySearchTerm(
searchTerm,
itemGroups,
);
const canCreateActions = getHasCreateActionPermission(
isFeatureEnabled,
pagePermissions,
);
const { openAddJS } = useJSAdd();
return (
@ -70,46 +74,60 @@ const ListJSObjects = () => {
showAddButton={canCreateActions}
/>
) : null}
<Flex
data-testid="t--ide-list"
flexDirection="column"
gap="spaces-4"
overflowY="auto"
>
{filteredItemGroups.map(({ group, items }) => {
return (
<Flex flexDirection={"column"} key={group}>
{group !== "NA" ? (
<Flex py="spaces-1">
<Text
className="overflow-hidden overflow-ellipsis whitespace-nowrap"
kind="body-s"
>
{group}
</Text>
</Flex>
) : null}
<FilesContextProvider
canCreateActions={canCreateActions}
editorId={applicationId}
parentEntityId={pageId}
parentEntityType={ActionParentEntityType.PAGE}
>
{items.map((item) => {
return (
<JSListItem
isActive={item.key === activeActionBaseId}
item={item}
key={item.key}
parentEntityId={pageId}
/>
);
})}
</FilesContextProvider>
</Flex>
);
})}
{isNewADSTemplatesEnabled ? (
<EntityGroupsList
groups={filteredItemGroups.map(({ group, items }) => {
return {
groupTitle: group === "NA" ? "" : group,
items: items,
className: "",
renderList: (item: EntityItem) => {
return <JSEntity item={item} />;
},
};
})}
/>
) : (
filteredItemGroups.map(({ group, items }) => {
return (
<Flex flexDirection={"column"} key={group}>
{group !== "NA" ? (
<Flex py="spaces-1">
<Text
className="overflow-hidden overflow-ellipsis whitespace-nowrap"
kind="body-s"
>
{group}
</Text>
</Flex>
) : null}
<FilesContextProvider
canCreateActions={canCreateActions}
editorId={editorId}
parentEntityId={parentEntityId}
parentEntityType={ActionParentEntityType.PAGE}
>
{items.map((item) => {
return (
<JSListItem
isActive={item.key === activeActionBaseId}
item={item}
key={item.key}
parentEntityId={parentEntityId}
/>
);
})}
</FilesContextProvider>
</Flex>
);
})
)}
{filteredItemGroups.length === 0 && searchTerm !== "" ? (
<NoSearchResults
text={createMessage(

View File

@ -0,0 +1,50 @@
import React from "react";
import type { Action } from "entities/Action";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import {
getHasDeleteActionPermission,
getHasManageActionPermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import {
ConvertToModule,
Copy,
Delete,
Move,
Rename,
ShowBindings,
} from "pages/Editor/AppPluginActionEditor/components/ContextMenuItems";
import { MenuSeparator } from "@appsmith/ads";
export interface Props {
action: Action;
}
export function AppQueryContextMenuItems(props: Props) {
const { action } = props;
const actionPermissions = action.userPermissions || [];
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const canDeleteAction = getHasDeleteActionPermission(
isFeatureEnabled,
actionPermissions,
);
const canManageAction = getHasManageActionPermission(
isFeatureEnabled,
actionPermissions,
);
return (
<>
<Rename action={action} disabled={!canManageAction} />
<ShowBindings action={action} />
<ConvertToModule action={action} />
<Copy action={action} disabled={!canManageAction} />
<Move action={action} disabled={!canManageAction} />
<MenuSeparator />
<Delete action={action} disabled={!canDeleteAction} />
</>
);
}

View File

@ -0,0 +1,122 @@
import React, { useCallback, useMemo } from "react";
import { EntityItem } from "@appsmith/ads";
import type { EntityItem as EntityItemProps } from "ee/entities/IDE/constants";
import type { AppState } from "ee/reducers";
import {
getActionByBaseId,
getDatasource,
getPlugins,
} from "ee/selectors/entitiesSelector";
import { type Action, type StoredDatasource } from "entities/Action";
import { useDispatch, useSelector } from "react-redux";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import type { Datasource } from "entities/Datasource";
import history, { NavigationMethod } from "utils/history";
import { keyBy } from "lodash";
import { saveActionNameBasedOnIdeType } from "ee/actions/helpers";
import { useNameEditorState } from "pages/Editor/IDE/EditorPane/hooks/useNameEditorState";
import { useValidateEntityName } from "IDE";
import { useLocation } from "react-router";
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import { PluginType } from "entities/Plugin";
import { useParentEntityInfo } from "ee/IDE/hooks/useParentEntityInfo";
import { getQueryContextMenuByIdeType } from "ee/pages/Editor/IDE/EditorPane/Query/utils/getQueryContextMenuByIdeType";
export const QueryEntityItem = ({ item }: { item: EntityItemProps }) => {
const action = useSelector((state: AppState) =>
getActionByBaseId(state, item.key),
) as Action;
const datasource = useSelector((state) =>
getDatasource(state, (action?.datasource as StoredDatasource)?.id),
) as Datasource;
const plugins = useSelector(getPlugins);
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
const location = useLocation();
const ideType = getIDETypeByUrl(location.pathname);
const activeActionBaseId = useActiveActionBaseId();
const { parentEntityId } = useParentEntityInfo(ideType);
const { editingEntity, enterEditMode, exitEditMode, updatingEntity } =
useNameEditorState();
const validateName = useValidateEntityName({
entityName: item.title,
});
const dispatch = useDispatch();
const contextMenu = getQueryContextMenuByIdeType(ideType, action);
const actionPermissions = action.userPermissions || [];
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const canManageAction = getHasManageActionPermission(
isFeatureEnabled,
actionPermissions,
);
const config = getActionConfig(action.pluginType);
const url = config?.getURL(
parentEntityId ?? "",
action.baseId,
action.pluginType,
pluginGroups[action.pluginId],
);
const switchToAction = useCallback(() => {
url && history.push(url, { invokedBy: NavigationMethod.EntityExplorer });
AnalyticsUtil.logEvent("ENTITY_EXPLORER_CLICK", {
type: "QUERIES/APIs",
fromUrl: location.pathname,
toUrl: url,
name: action.name,
});
AnalyticsUtil.logEvent("EDIT_ACTION_CLICK", {
actionId: action?.id,
datasourceId: datasource?.id,
pluginName: pluginGroups[action?.pluginId]?.name,
actionType: action?.pluginType === PluginType.DB ? "Query" : "API",
isMock: !!datasource?.isMock,
});
}, [url, location.pathname, action, datasource, pluginGroups]);
const nameEditorConfig = useMemo(() => {
return {
canEdit: canManageAction,
isEditing: editingEntity === action.id,
isLoading: updatingEntity === action.id,
onEditComplete: exitEditMode,
onNameSave: (newName: string) =>
dispatch(saveActionNameBasedOnIdeType(action.id, newName, ideType)),
validateName: (newName: string) => validateName(newName, item.title),
};
}, [
canManageAction,
editingEntity,
exitEditMode,
ideType,
item.title,
action.id,
updatingEntity,
]);
return (
<EntityItem
className="action t--action-entity"
id={action.id}
isSelected={activeActionBaseId === action.id}
key={action.id}
nameEditorConfig={nameEditorConfig}
onClick={switchToAction}
onDoubleClick={() => enterEditMode(action.id)}
rightControl={contextMenu}
rightControlVisibility="hover"
startIcon={item.icon}
title={item.title}
/>
);
};

View File

@ -1,47 +1,55 @@
import React, { useState } from "react";
import { Flex, Text, SearchAndAdd, NoSearchResults } from "@appsmith/ads";
import {
Flex,
Text,
SearchAndAdd,
NoSearchResults,
EntityGroupsList,
} from "@appsmith/ads";
import { useSelector } from "react-redux";
import { getHasCreateActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import {
getCurrentApplicationId,
getCurrentPageId,
getPagePermissions,
} from "selectors/editorSelectors";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { selectQuerySegmentEditorList } from "ee/selectors/appIDESelectors";
import { ActionParentEntityType } from "ee/entities/Engine/actionHelpers";
import { FilesContextProvider } from "pages/Editor/Explorer/Files/FilesContextProvider";
import { useQueryAdd } from "ee/pages/Editor/IDE/EditorPane/Query/hooks";
import { QueryListItem } from "ee/pages/Editor/IDE/EditorPane/Query/ListItem";
import { QueryListItem } from "ee/pages/Editor/IDE/EditorPane/Query/old/ListItem";
import { getShowWorkflowFeature } from "ee/selectors/workflowSelectors";
import { BlankState } from "./BlankState";
import { EDITOR_PANE_TEXTS, createMessage } from "ee/constants/messages";
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
import type { EntityItem } from "ee/entities/IDE/constants";
import { ActionEntityItem } from "ee/pages/Editor/IDE/EditorPane/Query/ListItem";
import { useLocation } from "react-router";
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
import { useParentEntityInfo } from "ee/IDE/hooks/useParentEntityInfo";
import { useCreateActionsPermissions } from "ee/entities/IDE/hooks/useCreateActionsPermissions";
import { objectKeys } from "@appsmith/utils";
const ListQuery = () => {
const [searchTerm, setSearchTerm] = useState("");
const pageId = useSelector(getCurrentPageId) as string;
const itemGroups = useSelector(selectQuerySegmentEditorList);
const activeActionBaseId = useActiveActionBaseId();
const pagePermissions = useSelector(getPagePermissions);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const location = useLocation();
const ideType = getIDETypeByUrl(location.pathname);
const { editorId, parentEntityId } = useParentEntityInfo(ideType);
const canCreateActions = useCreateActionsPermissions(ideType);
const showWorkflows = useSelector(getShowWorkflowFeature);
const isNewADSTemplatesEnabled = useFeatureFlag(
FEATURE_FLAG.release_ads_entity_item_enabled,
);
const filteredItemGroups = filterEntityGroupsBySearchTerm(
searchTerm,
itemGroups,
);
const canCreateActions = getHasCreateActionPermission(
isFeatureEnabled,
pagePermissions,
);
const applicationId = useSelector(getCurrentApplicationId);
const { openAddQuery } = useQueryAdd();
const showWorkflows = useSelector(getShowWorkflowFeature);
return (
<Flex
@ -52,7 +60,7 @@ const ListQuery = () => {
px="spaces-3"
py="spaces-3"
>
{Object.keys(itemGroups).length === 0 && <BlankState />}
{objectKeys(itemGroups).length === 0 && <BlankState />}
{itemGroups.length > 0 ? (
<SearchAndAdd
@ -61,39 +69,59 @@ const ListQuery = () => {
showAddButton={canCreateActions}
/>
) : null}
<Flex flexDirection={"column"} gap="spaces-4" overflowY="auto">
{filteredItemGroups.map(({ group, items }) => {
return (
<Flex flexDirection={"column"} key={group}>
<Flex py="spaces-1">
<Text
className="overflow-hidden overflow-ellipsis whitespace-nowrap"
kind="body-s"
<Flex
data-testid="t--ide-list"
flexDirection={"column"}
gap="spaces-4"
overflowY="auto"
>
{isNewADSTemplatesEnabled ? (
<EntityGroupsList
groups={filteredItemGroups.map(({ group, items }) => {
return {
groupTitle: group,
items: items,
className: "",
renderList: (item: EntityItem) => {
return <ActionEntityItem item={item} />;
},
};
})}
/>
) : (
filteredItemGroups.map(({ group, items }) => {
return (
<Flex flexDirection={"column"} key={group}>
<Flex py="spaces-1">
<Text
className="overflow-hidden overflow-ellipsis whitespace-nowrap"
kind="body-s"
>
{group}
</Text>
</Flex>
<FilesContextProvider
canCreateActions={canCreateActions}
editorId={editorId}
parentEntityId={parentEntityId}
parentEntityType={ActionParentEntityType.PAGE}
showWorkflows={showWorkflows}
>
{group}
</Text>
{items.map((file) => {
return (
<QueryListItem
isActive={file.key === activeActionBaseId}
item={file}
key={file.key}
parentEntityId={parentEntityId}
/>
);
})}
</FilesContextProvider>
</Flex>
<FilesContextProvider
canCreateActions={canCreateActions}
editorId={applicationId}
parentEntityId={pageId}
parentEntityType={ActionParentEntityType.PAGE}
showWorkflows={showWorkflows}
>
{items.map((file) => {
return (
<QueryListItem
isActive={file.key === activeActionBaseId}
item={file}
key={file.key}
parentEntityId={pageId}
/>
);
})}
</FilesContextProvider>
</Flex>
);
})}
);
})
)}
{filteredItemGroups.length === 0 && searchTerm !== "" ? (
<NoSearchResults
text={createMessage(

View File

@ -0,0 +1,50 @@
import React from "react";
import { Button, Menu, MenuContent, MenuTrigger, Tooltip } from "@appsmith/ads";
import { useToggle } from "@mantine/hooks";
import {
createMessage,
ENTITY_MORE_ACTIONS_TOOLTIP,
} from "ee/constants/messages";
import { EntityClassNames } from "pages/Editor/Explorer/Entity";
interface Props {
children?: React.ReactNode[] | React.ReactNode;
}
const EntityContextMenu = (props: Props) => {
const [isMenuOpen, toggleMenuOpen] = useToggle([false, true]);
return (
<Menu onOpenChange={toggleMenuOpen} open={isMenuOpen}>
<MenuTrigger className="t--context-menu">
<div className="relative">
<Tooltip
content={createMessage(ENTITY_MORE_ACTIONS_TOOLTIP)}
isDisabled={isMenuOpen}
mouseLeaveDelay={0}
placement="right"
>
<Button
className={EntityClassNames.CONTEXT_MENU}
data-testid="t--more-action-trigger"
isIconButton
kind="tertiary"
startIcon="more-2-fill"
/>
</Tooltip>
</div>
</MenuTrigger>
<MenuContent
align="start"
className={`t--entity-context-menu ${EntityClassNames.CONTEXT_MENU_CONTENT}`}
side="right"
style={{ maxHeight: "unset" }}
width="220px"
>
{props.children}
</MenuContent>
</Menu>
);
};
export default EntityContextMenu;

View File

@ -1,32 +1,4 @@
import React, { useCallback, useMemo } from "react";
import { useBoolean } from "usehooks-ts";
import { useDispatch, useSelector } from "react-redux";
import {
moveJSCollectionRequest,
copyJSCollectionRequest,
deleteJSCollection,
} from "actions/jsActionActions";
import noop from "lodash/noop";
import {
CONTEXT_COPY,
CONTEXT_DELETE,
CONFIRM_CONTEXT_DELETE,
CONTEXT_MOVE,
createMessage,
CONTEXT_RENAME,
} from "ee/constants/messages";
import { getPageListAsOptions } from "ee/selectors/entitiesSelector";
import {
autoIndentCode,
getAutoIndentShortcutKeyText,
} from "components/editorComponents/CodeEditor/utils/autoIndentUtils";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { updateJSCollectionBody } from "actions/jsPaneActions";
import type { IconName } from "@blueprintjs/icons";
import type { ContextMenuOption } from "./JSEditorContextMenu";
import JSEditorContextMenu from "./JSEditorContextMenu";
import equal from "fast-deep-equal/es6";
import React from "react";
import {
getHasDeleteActionPermission,
getHasManageActionPermission,
@ -34,26 +6,18 @@ import {
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import type { JSCollection } from "entities/JSCollection";
import { setRenameEntity } from "actions/ideActions";
import type CodeMirror from "codemirror";
import { Copy, Delete, Move, Prettify } from "./ContextMenuItems";
import { RenameMenuItem } from "IDE";
import { MenuSeparator } from "@appsmith/ads";
import EntityContextMenu from "../IDE/EditorPane/components/EntityContextMenu";
interface AppJSEditorContextMenuProps {
pageId: string;
jsCollection: JSCollection;
}
const prettifyCodeKeyboardShortCut = getAutoIndentShortcutKeyText();
export function AppJSEditorContextMenu({
jsCollection,
pageId,
}: AppJSEditorContextMenuProps) {
const {
setFalse: cancelConfirmDelete,
setValue: setConfirmDelete,
value: confirmDelete,
} = useBoolean(false);
const dispatch = useDispatch();
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
@ -64,169 +28,18 @@ export function AppJSEditorContextMenu({
jsCollection?.userPermissions || [],
);
const renameJS = useCallback(() => {
// We add a delay to avoid having the focus stuck in the menu trigger
setTimeout(() => {
dispatch(setRenameEntity(jsCollection.id));
}, 100);
}, [dispatch, jsCollection.id]);
const copyJSCollectionToPage = useCallback(
(actionId: string, actionName: string, pageId: string) => {
dispatch(
copyJSCollectionRequest({
id: actionId,
destinationPageId: pageId,
name: actionName,
}),
);
},
[dispatch],
);
const moveJSCollectionToPage = useCallback(
(actionId: string, actionName: string, destinationPageId: string) => {
dispatch(
moveJSCollectionRequest({
id: actionId,
destinationPageId,
name: actionName,
}),
);
},
[dispatch],
);
const deleteJSCollectionFromPage = useCallback(
(actionId: string, actionName: string) => {
dispatch(deleteJSCollection({ id: actionId, name: actionName }));
setConfirmDelete(false);
},
[dispatch, setConfirmDelete],
);
const menuPages = useSelector(getPageListAsOptions, equal);
const options = useMemo(() => {
const confirmDeletion = (value: boolean, event?: Event) => {
event?.preventDefault?.();
setConfirmDelete(value);
};
const renameOption = {
icon: "input-cursor-move" as IconName,
value: "rename",
onSelect: renameJS,
label: createMessage(CONTEXT_RENAME),
disabled: !isChangePermitted,
};
const copyOption = {
icon: "duplicate" as IconName,
value: "copy",
onSelect: noop,
label: createMessage(CONTEXT_COPY),
children: menuPages.map((page) => {
return {
...page,
onSelect: () =>
copyJSCollectionToPage(jsCollection.id, jsCollection.name, page.id),
};
}),
};
const moveOption = {
icon: "swap-horizontal" as IconName,
value: "move",
onSelect: noop,
label: createMessage(CONTEXT_MOVE),
children:
menuPages.length > 1
? menuPages
.filter((page) => page.id !== pageId) // Remove current page from the list
.map((page) => {
return {
...page,
onSelect: () =>
moveJSCollectionToPage(
jsCollection.id,
jsCollection.name,
page.id,
),
};
})
: [{ value: "No Pages", onSelect: noop, label: "No Pages" }],
};
const prettifyOptions = {
value: "prettify",
icon: "code" as IconName,
subText: prettifyCodeKeyboardShortCut,
onSelect: () => {
const editorElement = document.querySelector(".CodeMirror");
if (
editorElement &&
"CodeMirror" in editorElement &&
editorElement.CodeMirror
) {
const editor = editorElement.CodeMirror as CodeMirror.Editor;
autoIndentCode(editor);
dispatch(updateJSCollectionBody(editor.getValue(), jsCollection.id));
AnalyticsUtil.logEvent("PRETTIFY_CODE_MANUAL_TRIGGER");
}
},
label: "Prettify code",
};
const deleteOption = {
confirmDelete: confirmDelete,
icon: "delete-bin-line" as IconName,
value: "delete",
onSelect: (event?: Event): void => {
confirmDelete
? deleteJSCollectionFromPage(jsCollection.id, jsCollection.name)
: confirmDeletion(true, event);
},
label: confirmDelete
? createMessage(CONFIRM_CONTEXT_DELETE)
: createMessage(CONTEXT_DELETE),
className: "t--apiFormDeleteBtn error-menuitem",
};
const options: ContextMenuOption[] = [renameOption];
if (isChangePermitted) {
options.push(copyOption);
options.push(moveOption);
options.push(prettifyOptions);
}
if (isDeletePermitted) options.push(deleteOption);
return options;
}, [
confirmDelete,
copyJSCollectionToPage,
deleteJSCollectionFromPage,
dispatch,
isChangePermitted,
isDeletePermitted,
jsCollection.id,
jsCollection.name,
menuPages,
moveJSCollectionToPage,
pageId,
renameJS,
setConfirmDelete,
]);
return (
<JSEditorContextMenu
className="t--more-action-menu"
onMenuClose={cancelConfirmDelete}
options={options}
/>
<EntityContextMenu>
<RenameMenuItem
disabled={!isChangePermitted}
entityId={jsCollection.id}
/>
<Copy disabled={!isChangePermitted} jsAction={jsCollection} />
<Move disabled={!isChangePermitted} jsAction={jsCollection} />
<Prettify disabled={!isChangePermitted} jsAction={jsCollection} />
<MenuSeparator />
<Delete disabled={!isDeletePermitted} jsAction={jsCollection} />
</EntityContextMenu>
);
}

View File

@ -0,0 +1,51 @@
import React from "react";
import { MenuSub, MenuSubContent, MenuSubTrigger } from "@appsmith/ads";
import { useDispatch, useSelector } from "react-redux";
import { getPageList } from "selectors/editorSelectors";
import { PageMenuItem } from "./PageMenuItem";
import { useCallback } from "react";
import { CONTEXT_COPY, createMessage } from "ee/constants/messages";
import { copyJSCollectionRequest } from "actions/jsActionActions";
import type { JSCollection } from "entities/JSCollection";
interface Props {
jsAction: JSCollection;
disabled?: boolean;
}
export const Copy = ({ disabled, jsAction }: Props) => {
const menuPages = useSelector(getPageList);
const dispatch = useDispatch();
const copyJSActionToPage = useCallback(
(pageId: string) =>
dispatch(
copyJSCollectionRequest({
id: jsAction.id,
destinationPageId: pageId,
name: jsAction.name,
}),
),
[jsAction.id, jsAction.name, dispatch],
);
return (
<MenuSub>
<MenuSubTrigger startIcon="duplicate">
{createMessage(CONTEXT_COPY)}
</MenuSubTrigger>
<MenuSubContent style={{ maxHeight: "350px" }} width="220px">
{menuPages.map((page) => {
return (
<PageMenuItem
disabled={disabled}
key={page.basePageId}
onSelect={copyJSActionToPage}
page={page}
/>
);
})}
</MenuSubContent>
</MenuSub>
);
};

View File

@ -0,0 +1,56 @@
import React, { useCallback, useState } from "react";
import {
CONFIRM_CONTEXT_DELETE,
CONTEXT_DELETE,
createMessage,
} from "ee/constants/messages";
import { MenuItem } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import { deleteJSCollection } from "actions/jsActionActions";
import type { JSCollection } from "entities/JSCollection";
interface Props {
jsAction: JSCollection;
disabled?: boolean;
deleteJSAction?: () => void;
}
export const Delete = ({ deleteJSAction, disabled, jsAction }: Props) => {
const dispatch = useDispatch();
const [confirmDelete, setConfirmDelete] = useState(false);
const handleDeleteClick = useCallback(() => {
jsAction.isPublic && deleteJSAction
? deleteJSAction()
: dispatch(
deleteJSCollection({
id: jsAction?.id ?? "",
name: jsAction?.name ?? "",
}),
);
}, [jsAction.id, jsAction.name, jsAction.isPublic, deleteJSAction, dispatch]);
const handleSelect = useCallback(
(e?: Event) => {
e?.preventDefault();
confirmDelete ? handleDeleteClick() : setConfirmDelete(true);
e?.stopPropagation();
},
[confirmDelete, handleDeleteClick],
);
const menuLabel = confirmDelete
? createMessage(CONFIRM_CONTEXT_DELETE)
: createMessage(CONTEXT_DELETE);
return (
<MenuItem
className="t--apiFormDeleteBtn single-select error-menuitem"
disabled={disabled}
onSelect={handleSelect}
startIcon="trash"
>
{menuLabel}
</MenuItem>
);
};

View File

@ -0,0 +1,65 @@
import { useDispatch, useSelector } from "react-redux";
import { getCurrentPageId } from "selectors/editorSelectors";
import { getPageList } from "ee/selectors/entitiesSelector";
import React, { useCallback, useMemo } from "react";
import {
MenuItem,
MenuSub,
MenuSubContent,
MenuSubTrigger,
} from "@appsmith/ads";
import { CONTEXT_MOVE, createMessage } from "ee/constants/messages";
import { PageMenuItem } from "./PageMenuItem";
import { moveJSCollectionRequest } from "actions/jsActionActions";
import type { JSCollection } from "entities/JSCollection";
interface Props {
jsAction: JSCollection;
disabled?: boolean;
}
export const Move = ({ disabled, jsAction }: Props) => {
const dispatch = useDispatch();
const currentPageId = useSelector(getCurrentPageId);
const allPages = useSelector(getPageList);
const menuPages = useMemo(() => {
return allPages.filter((page) => page.pageId !== currentPageId);
}, [allPages, currentPageId]);
const moveJSActionToPage = useCallback(
(destinationPageId: string) =>
dispatch(
moveJSCollectionRequest({
id: jsAction.id,
destinationPageId,
name: jsAction.name,
}),
),
[dispatch, jsAction.id, jsAction.name],
);
return (
<MenuSub>
<MenuSubTrigger startIcon="swap-horizontal">
{createMessage(CONTEXT_MOVE)}
</MenuSubTrigger>
<MenuSubContent style={{ maxHeight: "350px" }} width="220px">
{menuPages.length ? (
menuPages.map((page) => {
return (
<PageMenuItem
disabled={disabled}
key={page.basePageId}
onSelect={moveJSActionToPage}
page={page}
/>
);
})
) : (
<MenuItem key="no-pages">No pages</MenuItem>
)}
</MenuSubContent>
</MenuSub>
);
};

View File

@ -0,0 +1,19 @@
import type { Page } from "entities/Page";
import React, { useCallback } from "react";
import { MenuItem } from "@appsmith/ads";
export const PageMenuItem = (props: {
page: Page;
onSelect: (id: string) => void;
disabled?: boolean;
}) => {
const handleOnSelect = useCallback(() => {
props.onSelect(props.page.pageId);
}, [props]);
return (
<MenuItem disabled={props.disabled} onSelect={handleOnSelect}>
{props.page.pageName}
</MenuItem>
);
};

View File

@ -0,0 +1,50 @@
import React, { useCallback } from "react";
import { MenuItem, Text } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import type { JSCollection } from "entities/JSCollection";
import { updateJSCollectionBody } from "actions/jsPaneActions";
import {
autoIndentCode,
getAutoIndentShortcutKeyText,
} from "components/editorComponents/CodeEditor/utils/autoIndentUtils";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
interface Props {
jsAction: JSCollection;
disabled?: boolean;
}
const prettifyCodeKeyboardShortCut = getAutoIndentShortcutKeyText();
export const Prettify = ({ disabled, jsAction }: Props) => {
const dispatch = useDispatch();
const handleSelect = useCallback(() => {
const editorElement = document.querySelector(".CodeMirror");
if (
editorElement &&
"CodeMirror" in editorElement &&
editorElement.CodeMirror
) {
const editor = editorElement.CodeMirror as CodeMirror.Editor;
autoIndentCode(editor);
dispatch(updateJSCollectionBody(editor.getValue(), jsAction.id));
AnalyticsUtil.logEvent("PRETTIFY_CODE_MANUAL_TRIGGER");
}
}, [jsAction.id, jsAction.name]);
return (
<MenuItem disabled={disabled} onSelect={handleSelect} startIcon="code">
<Text color={"var(--ads-v2-color-fg)"}>Prettify code</Text>
<Text
color={"var(--ads-v2-color-fg-muted)"}
kind="body-s"
style={{ marginLeft: "7px" }}
>
{prettifyCodeKeyboardShortCut}
</Text>
</MenuItem>
);
};

View File

@ -0,0 +1,32 @@
import React, { useCallback } from "react";
import { MenuItem } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import { initExplorerEntityNameEdit } from "actions/explorerActions";
import { CONTEXT_RENAME, createMessage } from "ee/constants/messages";
import type { JSCollection } from "entities/JSCollection";
interface Props {
jsAction: JSCollection;
disabled?: boolean;
}
export const Rename = ({ disabled, jsAction }: Props) => {
const dispatch = useDispatch();
const setRename = useCallback(() => {
// We add a delay to avoid having the focus stuck in the menu trigger
setTimeout(() => {
dispatch(initExplorerEntityNameEdit(jsAction.id));
}, 100);
}, [dispatch, jsAction.id]);
return (
<MenuItem
disabled={disabled}
onSelect={setRename}
startIcon="input-cursor-move"
>
{createMessage(CONTEXT_RENAME)}
</MenuItem>
);
};

View File

@ -0,0 +1,38 @@
import React, { useCallback } from "react";
import { CONTEXT_SHOW_BINDING, createMessage } from "ee/constants/messages";
import { MenuItem } from "@appsmith/ads";
import { useDispatch } from "react-redux";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
import type { JSCollection } from "entities/JSCollection";
interface Props {
jsAction: JSCollection;
disabled?: boolean;
}
export const ShowBindings = ({ disabled, jsAction }: Props) => {
const dispatch = useDispatch();
const handleSelect = useCallback(() => {
dispatch({
type: ReduxActionTypes.SET_ENTITY_INFO,
payload: {
entityId: jsAction.id,
entityName: jsAction.name,
entityType: ENTITY_TYPE.JSACTION,
show: true,
},
});
}, [jsAction.id, jsAction.name]);
return (
<MenuItem
disabled={disabled}
onSelect={handleSelect}
startIcon="binding-new"
>
{createMessage(CONTEXT_SHOW_BINDING)}
</MenuItem>
);
};

View File

@ -0,0 +1,6 @@
export { Copy } from "./Copy";
export { Move } from "./Move";
export { Delete } from "./Delete";
export { Rename } from "./Rename";
export { ShowBindings } from "./ShowBindings";
export { Prettify } from "./Prettify";

View File

@ -1,116 +0,0 @@
import React from "react";
import type { IconName } from "@blueprintjs/icons";
import {
Button,
Menu,
MenuContent,
MenuItem,
MenuSub,
MenuSubContent,
MenuSubTrigger,
MenuTrigger,
Text,
} from "@appsmith/ads";
export interface ContextMenuOption {
id?: string;
icon: IconName;
value: string;
onSelect?: (event?: Event) => void;
label: string;
subText?: string;
className?: string;
children?: Omit<ContextMenuOption, "children" | "icon">[];
}
interface EntityContextMenuProps {
className?: string;
options: ContextMenuOption[];
onMenuClose: (() => void) | undefined;
}
export function JSEditorContextMenu({
className,
onMenuClose,
options,
}: EntityContextMenuProps) {
if (options.length === 0) {
return null;
}
return (
<Menu
className={className}
onOpenChange={(open) => {
if (!open) {
onMenuClose?.();
}
}}
>
<MenuTrigger>
<Button
data-testid="t--more-action-trigger"
isIconButton
kind="tertiary"
size={"sm"}
startIcon={"more-2-fill"}
/>
</MenuTrigger>
<MenuContent align="end" avoidCollisions>
{options.map((option, index) => {
if (option.children) {
return (
<MenuSub key={index}>
<MenuSubTrigger startIcon={option.icon}>
{option.label}
</MenuSubTrigger>
<MenuSubContent>
{option.children.map((children) => (
<MenuItem key={children.value} onSelect={children.onSelect}>
{children.label}
</MenuItem>
))}
</MenuSubContent>
</MenuSub>
);
}
return (
<MenuItem
className={option?.className}
key={option.value}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSelect={option.onSelect as any}
startIcon={option.icon}
>
<div>
<Text
color={
option?.value === "delete"
? "var(--ads-v2-color-fg-error)"
: "var(--ads-v2-color-fg)"
}
>
{option.label}
</Text>
{option.subText && (
<Text
color={"var(--ads-v2-color-fg-muted)"}
kind="body-s"
style={{ marginLeft: "7px" }}
>
{option.subText}
</Text>
)}
</div>
</MenuItem>
);
})}
</MenuContent>
</Menu>
);
}
export default JSEditorContextMenu;

View File

@ -3,10 +3,7 @@ import type { RouteComponentProps } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import JsEditorForm from "./Form";
import * as Sentry from "@sentry/react";
import {
getCurrentPageId,
getJSCollectionDataByBaseId,
} from "selectors/editorSelectors";
import { getJSCollectionDataByBaseId } from "selectors/editorSelectors";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import Spinner from "components/editorComponents/Spinner";
import styled from "styled-components";
@ -27,7 +24,6 @@ type Props = RouteComponentProps<{
function JSEditor(props: Props) {
const { baseCollectionId } = props.match.params;
const pageId = useSelector(getCurrentPageId);
const dispatch = useDispatch();
const jsCollectionData = useSelector((state) =>
getJSCollectionDataByBaseId(state, baseCollectionId),
@ -40,10 +36,8 @@ function JSEditor(props: Props) {
return null;
}
return (
<AppJSEditorContextMenu jsCollection={jsCollection} pageId={pageId} />
);
}, [jsCollection, pageId]);
return <AppJSEditorContextMenu jsCollection={jsCollection} />;
}, [jsCollection]);
if (isCreating) {
return (

View File

@ -1062,6 +1062,7 @@ function* handleMoveOrCopySaga(actionPayload: ReduxAction<Action>) {
const isApi = pluginType === PluginType.API;
const isQuery = pluginType === PluginType.DB;
const isSaas = pluginType === PluginType.SAAS;
const isInternal = pluginType === PluginType.INTERNAL;
const { parentEntityId } = resolveParentEntityMetadata(actionPayload.payload);
if (!parentEntityId) return;
@ -1080,7 +1081,7 @@ function* handleMoveOrCopySaga(actionPayload: ReduxAction<Action>) {
);
}
if (isQuery) {
if (isQuery || isInternal) {
history.push(
queryEditorIdURL({
baseParentEntityId,