chore: Move action redesign to GA (#38659)

This commit is contained in:
Hetu Nandu 2025-01-16 22:26:34 +05:30 committed by GitHub
parent 5a98f55024
commit 1963a9a27d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 79 additions and 4107 deletions

View File

@ -5,7 +5,6 @@ import produce from "immer";
const defaultFlags = {
release_side_by_side_ide_enabled: true,
rollout_remove_feature_walkthrough_enabled: false, // remove this flag from here when it's removed from code
release_actions_redesign_enabled: true,
release_git_modularisation_enabled: true,
};

View File

@ -1,148 +0,0 @@
import styled from "styled-components";
import { Tab, TabPanel, Tabs, TabsList } from "@appsmith/ads";
import FormLabel from "components/editorComponents/FormLabel";
import type { AutoGeneratedHeader } from "pages/Editor/APIEditor/helpers";
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import React from "react";
import { API_EDITOR_TABS } from "../../../../constants/CommonApiConstants";
import { DatasourceConfig } from "./components/DatasourceConfig";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import ApiAuthentication from "./components/ApiAuthentication";
import ActionSettings from "pages/Editor/ActionSettings";
import { API_EDITOR_TAB_TITLES, createMessage } from "ee/constants/messages";
import { useSelectedFormTab } from "./hooks/useSelectedFormTab";
import { getHeadersCount, getParamsCount } from "./utils";
import type { Property } from "entities/Action";
const SettingsWrapper = styled.div`
padding: var(--ads-v2-spaces-4) 0;
height: 100%;
${FormLabel} {
padding: 0;
}
`;
const StyledTabPanel = styled(TabPanel)`
height: calc(100% - 50px);
overflow: auto;
`;
/**
* @deprecated This component will be deleted along with APIEditor/CommonEditorForm.
*/
export function RequestTabs(props: {
autogeneratedHeaders: AutoGeneratedHeader[] | undefined;
datasourceHeaders: Property[];
actionConfigurationHeaders: Property[];
actionName: string;
pushFields: boolean;
theme: EditorTheme.LIGHT;
datasourceParams: Property[];
actionConfigurationParams: Property[];
bodyUIComponent: React.ReactNode;
paginationUiComponent: React.ReactNode;
formName: string;
showSettings: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionSettingsConfig?: any;
}) {
const [value, onValueChange] = useSelectedFormTab();
const headersCount = getHeadersCount(
props.actionConfigurationHeaders,
props.datasourceHeaders,
props.autogeneratedHeaders,
);
const paramsCount = getParamsCount(
props.actionConfigurationParams,
props.datasourceHeaders,
);
return (
<Tabs
onValueChange={onValueChange}
style={{
height: "calc(100% - 36px)",
overflow: "hidden",
maxHeight: "unset",
}}
value={value}
>
<TabsList>
{Object.values(API_EDITOR_TABS)
.filter((tab) => {
return !(!props.showSettings && tab === API_EDITOR_TABS.SETTINGS);
})
.map((tab) => (
<Tab
data-testid={`t--api-editor-${tab}`}
key={tab}
notificationCount={
tab == "HEADERS"
? headersCount
: tab == "PARAMS"
? paramsCount
: undefined
}
value={tab}
>
{createMessage(API_EDITOR_TAB_TITLES[tab])}
</Tab>
))}
</TabsList>
<StyledTabPanel value={API_EDITOR_TABS.HEADERS}>
<DatasourceConfig
attributeName="header"
autogeneratedHeaders={props.autogeneratedHeaders}
data={props.datasourceHeaders}
/>
<KeyValueFieldArray
actionConfig={props.actionConfigurationHeaders}
dataTreePath={`${props.actionName}.config.headers`}
hideHeader
label="Headers"
name="actionConfiguration.headers"
placeholder="Value"
pushFields={props.pushFields}
theme={props.theme}
/>
</StyledTabPanel>
<StyledTabPanel value={API_EDITOR_TABS.PARAMS}>
<DatasourceConfig
attributeName={"param"}
data={props.datasourceParams}
/>
<KeyValueFieldArray
actionConfig={props.actionConfigurationParams}
dataTreePath={`${props.actionName}.config.queryParameters`}
hideHeader
label="Params"
name="actionConfiguration.queryParameters"
pushFields={props.pushFields}
theme={props.theme}
/>
</StyledTabPanel>
<StyledTabPanel className="h-full" value={API_EDITOR_TABS.BODY}>
{props.bodyUIComponent}
</StyledTabPanel>
<StyledTabPanel value={API_EDITOR_TABS.PAGINATION}>
{props.paginationUiComponent}
</StyledTabPanel>
<StyledTabPanel value={API_EDITOR_TABS.AUTHENTICATION}>
<ApiAuthentication formName={props.formName} />
</StyledTabPanel>
{props.showSettings ? (
<StyledTabPanel value={API_EDITOR_TABS.SETTINGS}>
<SettingsWrapper>
<ActionSettings
actionSettingsConfig={props.actionSettingsConfig}
formName={props.formName}
theme={props.theme}
/>
</SettingsWrapper>
</StyledTabPanel>
) : null}
</Tabs>
);
}

View File

@ -1,29 +1,4 @@
export const sortedDatasourcesHandler = (
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
datasources: Record<string, any>,
currentDatasourceId: string,
) => {
// this function sorts the datasources list, with the current action's datasource first, followed by others.
let sortedArr = [];
sortedArr = datasources.filter(
(d: { id: string }) => d?.id === currentDatasourceId,
);
sortedArr = [
...sortedArr,
...datasources.filter((d: { id: string }) => d?.id !== currentDatasourceId),
];
return sortedArr;
};
export interface AutoGeneratedHeader {
key: string;
value: string;
isInvalid: boolean;
}
import type { AutoGeneratedHeader } from "entities/Action";
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState } from "react";
import { isArray, isString } from "lodash";
import { isHtml } from "../utils";
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
import { SegmentedControlContainer } from "pages/Editor/QueryEditor/EditorJSONtoForm";
import { Flex, SegmentedControl } from "@appsmith/ads";
import type { ActionResponse } from "api/ActionAPI";
import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
@ -12,6 +11,16 @@ import { useDispatch } from "react-redux";
import styled from "styled-components";
import { ResponseFormatTabs } from "./ResponseFormatTabs";
const SegmentedControlContainer = styled.div`
padding: 0 var(--ads-v2-spaces-7);
padding-top: var(--ads-v2-spaces-4);
display: flex;
flex-direction: column;
gap: var(--ads-v2-spaces-4);
overflow-y: clip;
overflow-x: scroll;
`;
const ResponseBodyContainer = styled.div`
overflow-y: clip;
height: 100%;

View File

@ -7,7 +7,6 @@ import pluralize from "pluralize";
import { Callout, Tooltip, type CalloutLinkProps } from "@appsmith/ads";
import type { ActionResponse } from "api/ActionAPI";
import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView";
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { type Action } from "entities/Action";
import { PluginType } from "entities/Plugin";
@ -16,14 +15,7 @@ import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
import { actionResponseDisplayDataFormats } from "pages/Editor/utils";
import { scrollbarWidth } from "utils/helpers";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import {
openPluginActionSettings,
setPluginActionEditorSelectedTab,
} from "PluginActionEditor/store";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { EDITOR_TABS } from "constants/QueryEditorConstants";
import { openPluginActionSettings } from "../../../../store";
import {
createMessage,
PREPARED_STATEMENT_WARNING,
@ -39,6 +31,7 @@ import { RESPONSE_TABLE_HEIGHT_OFFSET } from "./constants";
import * as Styled from "./styles";
import { checkForPreparedStatement, parseActionResponse } from "./utils";
import ActionExecutionInProgressView from "./components/ActionExecutionInProgressView";
interface ResponseProps {
action: Action;
@ -51,10 +44,6 @@ interface ResponseProps {
}
export function Response(props: ResponseProps) {
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
const {
action,
actionResponse,
@ -148,11 +137,7 @@ export function Response(props: ResponseProps) {
const preparedStatementCalloutLinks: CalloutLinkProps[] = useMemo(() => {
const navigateToSettings = () => {
if (isActionRedesignEnabled) {
dispatch(openPluginActionSettings(true));
} else {
dispatch(setPluginActionEditorSelectedTab(EDITOR_TABS.SETTINGS));
}
};
return [
@ -161,7 +146,7 @@ export function Response(props: ResponseProps) {
children: createMessage(PREPARED_STATEMENT_WARNING.LINK),
},
];
}, [dispatch, isActionRedesignEnabled]);
}, [dispatch]);
const handleContentTypeChange = useEventCallback((e?: Event) => {
if (e?.target && e.target instanceof HTMLElement) {

View File

@ -7,8 +7,8 @@ import {
import ActionAPI from "api/ActionAPI";
import { Button, Spinner, Text } from "@appsmith/ads";
import styled from "styled-components";
import type { EditorTheme } from "./CodeEditor/EditorConfig";
import LoadingOverlayScreen from "./LoadingOverlayScreen";
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
const Wrapper = styled.div`
position: relative;

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import { Link } from "@appsmith/ads";
import ActionSettings from "pages/Editor/ActionSettings";
import ActionSettings from "../PluginActionToolbar/components/ActionSettings";
import { usePluginActionContext } from "../../PluginActionContext";
import styled from "styled-components";
import {

View File

@ -1,11 +1,11 @@
import React from "react";
import type { ControlProps } from "components/formControls/BaseControl";
import FormControl from "./FormControl";
import FormControl from "pages/Editor/FormControl";
import log from "loglevel";
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import styled from "styled-components";
import { Text } from "@appsmith/ads";
import CenteredWrapper from "../../components/designSystems/appsmith/CenteredWrapper";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
interface ActionSettingsProps {
// TODO: Fix this the next time the file is edited

View File

@ -26,7 +26,6 @@ export const FEATURE_FLAG = {
"ab_one_click_learning_popover_enabled",
release_side_by_side_ide_enabled: "release_side_by_side_ide_enabled",
ab_appsmith_ai_query: "ab_appsmith_ai_query",
release_actions_redesign_enabled: "release_actions_redesign_enabled",
rollout_remove_feature_walkthrough_enabled:
"rollout_remove_feature_walkthrough_enabled",
rollout_eslint_enabled: "rollout_eslint_enabled",
@ -82,7 +81,6 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
ab_one_click_learning_popover_enabled: false,
release_side_by_side_ide_enabled: false,
ab_appsmith_ai_query: false,
release_actions_redesign_enabled: false,
rollout_remove_feature_walkthrough_enabled: true,
rollout_eslint_enabled: false,
rollout_side_by_side_enabled: false,

View File

@ -154,13 +154,6 @@ const PluginActionEditor = lazy(async () =>
),
);
const ApiEditor = lazy(async () =>
retryPromise(
async () =>
import(/* webpackChunkName: "APIEditor" */ "pages/Editor/APIEditor"),
),
);
const AddQuery = lazy(async () =>
retryPromise(
async () =>
@ -169,30 +162,20 @@ const AddQuery = lazy(async () =>
),
),
);
const QueryEditor = lazy(async () =>
retryPromise(
async () =>
import(/* webpackChunkName: "QueryEditor" */ "pages/Editor/QueryEditor"),
),
);
const QueryEmpty = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "QueryEmpty" */ "pages/Editor/QueryEditor/QueriesBlankState"
/* webpackChunkName: "QueryEmpty" */ "../../../../../../PluginActionEditor/components/PluginActionForm/components/UQIEditor/QueriesBlankState"
),
),
);
export const useQueryEditorRoutes = (path: string): UseRoutes => {
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
const skeleton = useMemo(() => <Skeleton />, []);
const newComponents = useMemo(
return useMemo(
() => [
{
key: "AddQuery",
@ -237,82 +220,6 @@ export const useQueryEditorRoutes = (path: string): UseRoutes => {
],
[path, skeleton],
);
const oldComponents = useMemo(
() => [
{
key: "ApiEditor",
component: (args: object) => {
return (
<Suspense fallback={skeleton}>
<ApiEditor {...args} />
</Suspense>
);
},
exact: true,
path: [
BUILDER_PATH + API_EDITOR_ID_PATH,
BUILDER_CUSTOM_PATH + API_EDITOR_ID_PATH,
BUILDER_PATH_DEPRECATED + API_EDITOR_ID_PATH,
],
},
{
key: "AddQuery",
exact: true,
component: () => (
<Suspense fallback={skeleton}>
<AddQuery />
</Suspense>
),
path: [`${path}${ADD_PATH}`, `${path}/:baseQueryId${ADD_PATH}`],
},
{
key: "SAASEditor",
component: (args: object) => {
return (
<Suspense fallback={skeleton}>
<QueryEditor {...args} />
</Suspense>
);
},
exact: true,
path: [
BUILDER_PATH + SAAS_EDITOR_API_ID_PATH,
BUILDER_CUSTOM_PATH + SAAS_EDITOR_API_ID_PATH,
BUILDER_PATH_DEPRECATED + SAAS_EDITOR_API_ID_PATH,
],
},
{
key: "QueryEditor",
component: (args: object) => {
return (
<Suspense fallback={skeleton}>
<QueryEditor {...args} />
</Suspense>
);
},
exact: true,
path: [path + "/:baseQueryId"],
},
{
key: "QueryEmpty",
component: () => (
<Suspense fallback={skeleton}>
<QueryEmpty />
</Suspense>
),
exact: true,
path: [path],
},
],
[path, skeleton],
);
if (isActionRedesignEnabled) {
return newComponents;
}
return oldComponents;
};
export const useAddQueryListItems = () => {

View File

@ -1,103 +0,0 @@
import React, { memo } from "react";
import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { removeSpecialChars } from "utils/helpers";
import { Flex } from "@appsmith/ads";
import NameEditorComponent, {
IconBox,
NameWrapper,
} from "components/utils/NameEditorComponent";
import {
ACTION_ID_NOT_FOUND_IN_URL,
ACTION_NAME_PLACEHOLDER,
createMessage,
} from "ee/constants/messages";
import type { ReduxAction } from "actions/ReduxActionTypes";
import type { SaveActionNameParams } from "PluginActionEditor";
import type { Action } from "entities/Action";
import type { ModuleInstance } from "ee/constants/ModuleInstanceConstants";
interface ActionNameEditorProps {
/*
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.
*/
enableFontStyling?: boolean;
disabled?: boolean;
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
actionConfig?: Action | ModuleInstance;
icon?: JSX.Element;
saveStatus: { isSaving: boolean; error: boolean };
}
function ActionNameEditor(props: ActionNameEditorProps) {
const {
actionConfig,
disabled = false,
enableFontStyling = false,
icon = "",
saveActionName,
saveStatus,
} = props;
return (
<NameEditorComponent
id={actionConfig?.id}
idUndefinedErrorMessage={ACTION_ID_NOT_FOUND_IN_URL}
name={actionConfig?.name}
onSaveName={saveActionName}
saveStatus={saveStatus}
>
{({
forceUpdate,
handleNameChange,
isInvalidNameForEntity,
isNew,
saveStatus,
}: {
forceUpdate: boolean;
handleNameChange: (value: string) => void;
isInvalidNameForEntity: (value: string) => string | boolean;
isNew: boolean;
saveStatus: { isSaving: boolean; error: boolean };
}) => (
<NameWrapper enableFontStyling={enableFontStyling}>
<Flex
alignItems="center"
gap="spaces-3"
overflow="hidden"
width="100%"
>
{icon && <IconBox className="t--plugin-icon-box">{icon}</IconBox>}
<EditableText
className="t--action-name-edit-field"
defaultValue={actionConfig ? actionConfig.name : ""}
disabled={disabled}
editInteractionKind={EditInteractionKind.SINGLE}
errorTooltipClass="t--action-name-edit-error"
forceDefault={forceUpdate}
iconSize={"md"}
isEditingDefault={isNew}
isInvalid={isInvalidNameForEntity}
onTextChanged={handleNameChange}
placeholder={createMessage(ACTION_NAME_PLACEHOLDER, "Api")}
type="text"
underline
updating={saveStatus.isSaving}
valueTransform={removeSpecialChars}
/>
</Flex>
</NameWrapper>
)}
</NameEditorComponent>
);
}
export default memo(ActionNameEditor);

View File

@ -1,117 +0,0 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import { getTypographyByKey } from "@appsmith/ads-old";
import { useSelector } from "react-redux";
import type { AppState } from "ee/reducers";
import { getDependenciesFromInverseDependencies } from "../Debugger/helpers";
import {
CollapsibleGroup,
CollapsibleGroupContainer,
} from "components/common/Collapsible";
const SideBar = styled.div`
height: 100%;
width: 100%;
& > a {
margin-top: 0;
margin-left: 0;
}
.icon-text {
display: flex;
.connection-type {
${getTypographyByKey("p1")}
}
}
.icon-text:nth-child(2) {
padding-top: ${(props) => props.theme.spaces[7]}px;
}
.description {
${getTypographyByKey("p1")}
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
padding-bottom: ${(props) => props.theme.spaces[7]}px;
}
@-webkit-keyframes slide-left {
0% {
-webkit-transform: translateX(100%);
transform: translateX(100%);
}
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
}
@keyframes slide-left {
0% {
-webkit-transform: translateX(100%);
transform: translateX(100%);
}
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
}
`;
const Wrapper = styled.div`
border-left: 1px solid var(--ads-v2-color-border);
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-4);
overflow: hidden;
border-bottom: 0;
display: flex;
width: ${(props) => props.theme.actionSidePane.width}px;
margin-top: 10px;
/* margin-left: var(--ads-v2-spaces-7); */
`;
export function useEntityDependencies(actionName: string) {
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
const entityDependencies = useMemo(
() =>
getDependenciesFromInverseDependencies(
deps.inverseDependencyMap,
actionName,
),
[actionName, deps.inverseDependencyMap],
);
const hasDependencies =
entityDependencies &&
(entityDependencies?.directDependencies.length > 0 ||
entityDependencies?.inverseDependencies.length > 0);
return {
hasDependencies,
entityDependencies,
};
}
function ActionSidebar({
additionalSections,
}: {
additionalSections?: React.ReactNode;
}) {
if (!additionalSections) {
return null;
}
return (
<Wrapper>
<SideBar>
<CollapsibleGroupContainer>
{additionalSections && (
<CollapsibleGroup height={"100%"}>
{additionalSections}
</CollapsibleGroup>
)}
</CollapsibleGroupContainer>
</SideBar>
</Wrapper>
);
}
export default ActionSidebar;

View File

@ -20,7 +20,6 @@ import {
import { actionPathFromName } from "components/formControls/utils";
import type { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { getSqlEditorModeFromPluginName } from "components/editorComponents/CodeEditor/sql/config";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
import { Flex } from "@appsmith/ads";
const Wrapper = styled.div`
@ -110,14 +109,12 @@ const mapStateToProps = (state: AppState, props: DynamicTextFieldProps) => {
const pluginId = valueSelector(state, "datasource.pluginId");
const responseTypes = getPluginResponseTypes(state);
const pluginName = getPluginNameFromId(state, pluginId);
const { release_actions_redesign_enabled } = selectFeatureFlags(state);
return {
actionName,
pluginId,
responseType: responseTypes[pluginId],
pluginName,
isActionRedesignEnabled: release_actions_redesign_enabled,
};
};

View File

@ -2,7 +2,6 @@ import type { EmbeddedRestDatasource } from "entities/Datasource";
import type { DynamicPath } from "utils/DynamicBindingUtils";
import _ from "lodash";
import type { LayoutOnLoadActionErrors } from "constants/AppsmithActionConstants/ActionConstants";
import type { AutoGeneratedHeader } from "pages/Editor/APIEditor/helpers";
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers";
import {
@ -82,6 +81,12 @@ export interface BodyFormData {
type: string;
}
export interface AutoGeneratedHeader {
key: string;
value: string;
isInvalid: boolean;
}
export interface ApiActionConfig extends Omit<ActionConfig, "formData"> {
headers: Property[];
autoGeneratedHeaders?: AutoGeneratedHeader[];

View File

@ -1,67 +0,0 @@
import type { ReduxAction } from "actions/ReduxActionTypes";
import type { PaginationField } from "api/ActionAPI";
import React, { createContext, useMemo } from "react";
import type { SaveActionNameParams } from "PluginActionEditor";
interface ApiEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
handleRunClick: (paginationField?: PaginationField) => void;
actionRightPaneBackLink?: React.ReactNode;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingsConfig: any;
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
showRightPaneTabbedSection?: boolean;
actionRightPaneAdditionSections?: React.ReactNode;
notification?: React.ReactNode | string;
}
type ApiEditorContextProviderProps =
React.PropsWithChildren<ApiEditorContextContextProps>;
export const ApiEditorContext = createContext<ApiEditorContextContextProps>(
{} as ApiEditorContextContextProps,
);
export function ApiEditorContextProvider({
actionRightPaneAdditionSections,
actionRightPaneBackLink,
children,
handleRunClick,
moreActionsMenu,
notification,
saveActionName,
settingsConfig,
showRightPaneTabbedSection,
}: ApiEditorContextProviderProps) {
const value = useMemo(
() => ({
actionRightPaneAdditionSections,
actionRightPaneBackLink,
showRightPaneTabbedSection,
handleRunClick,
moreActionsMenu,
saveActionName,
settingsConfig,
notification,
}),
[
actionRightPaneBackLink,
actionRightPaneAdditionSections,
showRightPaneTabbedSection,
handleRunClick,
moreActionsMenu,
saveActionName,
settingsConfig,
notification,
],
);
return (
<ApiEditorContext.Provider value={value}>
{children}
</ApiEditorContext.Provider>
);
}

View File

@ -1,352 +0,0 @@
import React, { useContext } from "react";
import { useSelector } from "react-redux";
import styled from "styled-components";
import FormLabel from "components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow";
import type { ActionResponse, PaginationField } from "api/ActionAPI";
import type { Action, PaginationType } from "entities/Action";
import ApiResponseView from "components/editorComponents/ApiResponseView";
import type { AppState } from "ee/reducers";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { Button } from "@appsmith/ads";
import { useParams } from "react-router";
import equal from "fast-deep-equal/es6";
import { getPlugin } from "ee/selectors/entitiesSelector";
import type { AutoGeneratedHeader } from "./helpers";
import { noop } from "lodash";
import { DEFAULT_DATASOURCE_NAME } from "PluginActionEditor/constants/ApiEditorConstants";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import {
getHasExecuteActionPermission,
getHasManageActionPermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { ApiEditorContext } from "./ApiEditorContext";
import RunHistory from "ee/components/RunHistory";
import { HintMessages } from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/HintMessages";
import { InfoFields } from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/InfoFields";
import { RequestTabs } from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/RequestTabs";
import { getSavingStatusForActionName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
const Form = styled.form`
position: relative;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
width: 100%;
${FormLabel} {
padding: ${(props) => props.theme.spaces[3]}px;
}
${FormRow} {
align-items: center;
${FormLabel} {
padding: 0;
width: 100%;
}
}
.api-info-row {
input {
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
}
}
`;
const MainConfiguration = styled.div`
z-index: 7;
padding: 0 var(--ads-v2-spaces-7);
.api-info-row {
padding-top: var(--ads-v2-spaces-5);
}
.form-row-header {
padding-top: var(--ads-v2-spaces-5);
}
`;
const ActionButtons = styled.div`
justify-self: flex-end;
display: flex;
align-items: center;
gap: var(--ads-v2-spaces-3);
`;
const HelpSection = styled.div`
padding: var(--ads-v2-spaces-4) var(--ads-v2-spaces-7);
`;
const SecondaryWrapper = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
width: 100%;
`;
const TabbedViewContainer = styled.div`
flex: 1;
overflow: auto;
position: relative;
height: 100%;
padding: 0 var(--ads-v2-spaces-7);
`;
const Wrapper = styled.div`
display: flex;
flex-direction: row;
height: 100%;
position: relative;
overflow: hidden;
`;
const MainContainer = styled.div`
display: flex;
position: relative;
height: 100%;
flex-direction: column;
/* padding: var(--ads-v2-spaces-7); */
`;
export interface CommonFormProps {
actionResponse?: ActionResponse;
pluginId: string;
onRunClick: (paginationField?: PaginationField) => void;
isRunning: boolean;
isDeleting: boolean;
paginationType: PaginationType;
appName: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfigurationHeaders?: any;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfigurationParams?: any;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
datasourceHeaders?: any;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
datasourceParams?: any;
actionName: string;
apiName: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingsConfig: any;
hintMessages?: Array<string>;
autoGeneratedActionConfigHeaders?: AutoGeneratedHeader[];
}
type CommonFormPropsWithExtraParams = CommonFormProps & {
formName: string;
// Body Tab Component which is passed on from the Parent Component
bodyUIComponent: JSX.Element;
// Pagination Tab Component which is passed on from the Parent Component
paginationUIComponent: JSX.Element;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleSubmit: any;
httpsMethods: { value: string }[];
};
export const NameWrapper = styled.div`
display: flex;
align-items: center;
input {
margin: 0;
box-sizing: border-box;
}
`;
const StyledNotificationWrapper = styled.div`
padding-top: var(--ads-v2-spaces-5);
`;
/**
* Commons editor form which is being used by API and GraphQL. Since most of the things were common to both so picking out the common part was a better option. For now Body and Pagination component are being passed on by the using component.
* @param props type CommonFormPropsWithExtraParams
* @returns Editor with respect to which type is using it
*/
function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
const {
actionRightPaneAdditionSections,
moreActionsMenu,
notification,
saveActionName,
} = useContext(ApiEditorContext);
const {
actionConfigurationHeaders,
actionConfigurationParams,
actionResponse,
autoGeneratedActionConfigHeaders,
formName,
handleSubmit,
hintMessages,
isRunning,
onRunClick,
pluginId,
settingsConfig,
} = props;
const params = useParams<{ baseApiId?: string; baseQueryId?: string }>();
// passing lodash's equality function to ensure that this selector does not cause a rerender multiple times.
// it checks each value to make sure none has changed before recomputing the actions.
const actions: Action[] = useSelector(
(state: AppState) => state.entities.actions.map((action) => action.config),
equal,
);
const currentActionConfig: Action | undefined = actions.find(
(action) =>
action.baseId === params.baseApiId || action.id === params.baseQueryId,
);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const isExecutePermitted = getHasExecuteActionPermission(
isFeatureEnabled,
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 ?? ""),
);
if (!currentActionConfig) return null;
// this gets the url of the current action's datasource
const actionDatasourceUrl =
currentActionConfig?.datasource?.datasourceConfiguration?.url || "";
const actionDatasourceUrlPath =
currentActionConfig?.actionConfiguration?.path || "";
// this gets the name of the current action's datasource
const actionDatasourceName = currentActionConfig?.datasource.name || "";
// if the url is empty and the action's datasource name is the default datasource name (this means the api does not have a datasource attached)
// or the user does not have permission,
// we block action execution.
const blockExecution =
(!actionDatasourceUrl &&
!actionDatasourceUrlPath &&
actionDatasourceName === DEFAULT_DATASOURCE_NAME) ||
!isExecutePermitted;
const theme = EditorTheme.LIGHT;
return (
<MainContainer>
<Form
data-testid={`t--action-form-${plugin?.type}`}
onSubmit={handleSubmit(noop)}
>
<MainConfiguration>
<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">
{moreActionsMenu}
<Button
className="t--apiFormRunBtn"
isDisabled={blockExecution}
isLoading={isRunning}
onClick={() => {
onRunClick();
}}
size="md"
>
Run
</Button>
</ActionButtons>
</FormRow>
{notification && (
<StyledNotificationWrapper>
{notification}
</StyledNotificationWrapper>
)}
<FormRow className="api-info-row">
<InfoFields
actionName={props.actionName}
changePermitted={isChangePermitted}
formName={props.formName}
options={props.httpsMethods}
pluginId={props.pluginId}
theme={EditorTheme.LIGHT}
/>
</FormRow>
</MainConfiguration>
{hintMessages && (
<HelpSection>
<HintMessages hintMessages={hintMessages} />
</HelpSection>
)}
<Wrapper>
<div className="flex flex-1">
<SecondaryWrapper>
<TabbedViewContainer>
<RequestTabs
actionConfigurationHeaders={actionConfigurationHeaders}
actionConfigurationParams={actionConfigurationParams}
actionName={props.actionName}
actionSettingsConfig={settingsConfig}
autogeneratedHeaders={autoGeneratedActionConfigHeaders}
bodyUIComponent={props.bodyUIComponent}
datasourceHeaders={props.datasourceHeaders}
datasourceParams={props.datasourceParams}
formName={formName}
paginationUiComponent={props.paginationUIComponent}
pushFields={isChangePermitted}
showSettings
theme={EditorTheme.LIGHT}
/>
</TabbedViewContainer>
<ApiResponseView
actionResponse={actionResponse}
currentActionConfig={currentActionConfig}
isRunDisabled={blockExecution}
isRunning={isRunning}
onRunClick={onRunClick}
theme={theme}
/>
<RunHistory />
</SecondaryWrapper>
</div>
{actionRightPaneAdditionSections}
</Wrapper>
</Form>
</MainContainer>
);
}
export default CommonEditorForm;

View File

@ -1,276 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { submit } from "redux-form";
import RestApiEditorForm from "./RestAPIForm";
import type { AppState } from "ee/reducers";
import type { RouteComponentProps } from "react-router";
import type {
ActionData,
ActionDataState,
} from "ee/reducers/entityReducers/actionsReducer";
import _ from "lodash";
import { getCurrentApplication } from "ee/selectors/applicationSelectors";
import {
getCurrentApplicationId,
getCurrentPageName,
} from "selectors/editorSelectors";
import { type Plugin, PluginPackageName } from "entities/Plugin";
import type { Action, PaginationType } from "entities/Action";
import Spinner from "components/editorComponents/Spinner";
import type { CSSProperties } from "styled-components";
import styled from "styled-components";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import {
changeApi,
isActionDeleting,
isActionRunning,
isPluginActionCreating,
} from "PluginActionEditor/store";
import * as Sentry from "@sentry/react";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import type { ApplicationPayload } from "entities/Application";
import {
getActionByBaseId,
getPageList,
getPlugins,
} from "ee/selectors/entitiesSelector";
import history from "utils/history";
import { saasEditorApiIdURL } from "ee/RouteBuilder";
import GraphQLEditorForm from "./GraphQL/GraphQLEditorForm";
import type { APIEditorRouteParams } from "constants/routes";
import { ApiEditorContext } from "./ApiEditorContext";
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;
interface ReduxStateProps {
actions: ActionDataState;
isRunning: boolean;
isDeleting: boolean;
isCreating: boolean;
apiId: string;
apiName: string;
currentApplication?: ApplicationPayload;
currentPageName: string | undefined;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pages: any;
plugins: Plugin[];
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pluginId: any;
apiAction: Action | ActionData | undefined;
paginationType: PaginationType;
applicationId: string;
}
interface OwnProps {
isEditorInitialized: boolean;
}
interface ReduxActionProps {
submitForm: (name: string) => void;
changeAPIPage: (apiId: string, isSaas: boolean) => void;
}
function getPackageNameFromPluginId(pluginId: string, plugins: Plugin[]) {
const plugin = plugins.find((plugin: Plugin) => plugin.id === pluginId);
return plugin?.packageName;
}
type Props = ReduxActionProps &
ReduxStateProps &
RouteComponentProps<APIEditorRouteParams> &
OwnProps;
class ApiEditor extends React.Component<Props> {
static contextType = ApiEditorContext;
context!: React.ContextType<typeof ApiEditorContext>;
componentDidMount() {
const type = this.getFormName();
if (this.props.apiId) {
this.props.changeAPIPage(this.props.apiId, type === "SAAS");
}
}
getFormName = () => {
const plugins = this.props.plugins;
const pluginId = this.props.pluginId;
const plugin =
plugins &&
plugins.find((plug) => {
if (plug.id === pluginId) return plug;
});
return plugin && plugin.type;
};
componentDidUpdate(prevProps: Props) {
if (prevProps.apiId !== this.props.apiId) {
const type = this.getFormName();
this.props.changeAPIPage(this.props.apiId || "", type === "SAAS");
}
}
getPluginUiComponentOfId = (
id: string,
plugins: Plugin[],
): string | undefined => {
const plugin = plugins.find((plugin) => plugin.id === id);
if (!plugin) return undefined;
return plugin.uiComponent;
};
getPluginUiComponentOfName = (plugins: Plugin[]): string | undefined => {
const plugin = plugins.find(
(plugin) => plugin.packageName === PluginPackageName.REST_API,
);
if (!plugin) return undefined;
return plugin.uiComponent;
};
render() {
const {
isCreating,
isDeleting,
isEditorInitialized,
isRunning,
match: {
params: { baseApiId },
},
paginationType,
pluginId,
plugins,
} = this.props;
if (!pluginId && baseApiId) {
return <EntityNotFoundPane />;
}
if (isCreating || !isEditorInitialized) {
return (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
);
}
let formUiComponent: string | undefined;
if (baseApiId) {
if (pluginId) {
formUiComponent = this.getPluginUiComponentOfId(pluginId, plugins);
} else {
formUiComponent = this.getPluginUiComponentOfName(plugins);
}
}
return (
<div style={formStyles}>
{formUiComponent === "ApiEditorForm" && (
<RestApiEditorForm
apiName={this.props.apiName}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
isDeleting={isDeleting}
isRunning={isRunning}
onRunClick={this.context.handleRunClick}
paginationType={paginationType}
pluginId={pluginId}
settingsConfig={this.context.settingsConfig}
/>
)}
{formUiComponent === "GraphQLEditorForm" && (
<GraphQLEditorForm
apiName={this.props.apiName}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
isDeleting={isDeleting}
isRunning={isRunning}
match={this.props.match}
onRunClick={this.context.handleRunClick}
paginationType={paginationType}
pluginId={pluginId}
settingsConfig={this.context.settingsConfig}
/>
)}
{formUiComponent === "SaaSEditorForm" &&
history.push(
saasEditorApiIdURL({
basePageId: this.props.match.params.basePageId,
pluginPackageName:
getPackageNameFromPluginId(
this.props.pluginId,
this.props.plugins,
) ?? "",
baseApiId: this.props.match.params.baseApiId || "",
}),
)}
</div>
);
}
}
const formStyles: CSSProperties = {
position: "relative",
display: "flex",
flexDirection: "column",
flexGrow: "1",
overflow: "auto",
};
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const apiAction = getActionByBaseId(state, props?.match?.params?.baseApiId);
const apiName = apiAction?.name ?? "";
const apiId = apiAction?.id ?? "";
const isCreating = isPluginActionCreating(state);
const isDeleting = isActionDeleting(apiId)(state);
const isRunning = isActionRunning(apiId)(state);
const pluginId = _.get(apiAction, "pluginId", "");
return {
actions: state.entities.actions,
currentApplication: getCurrentApplication(state),
currentPageName: getCurrentPageName(state),
pages: getPageList(state),
apiId,
apiName,
plugins: getPlugins(state),
pluginId,
paginationType: _.get(apiAction, "actionConfiguration.paginationType"),
apiAction,
isRunning,
isDeleting,
isCreating,
applicationId: getCurrentApplicationId(state),
};
};
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({
submitForm: (name: string) => dispatch(submit(name)),
changeAPIPage: (actionId: string, isSaas: boolean) =>
dispatch(changeApi(actionId, isSaas)),
});
export default Sentry.withProfiler(
connect(mapStateToProps, mapDispatchToProps)(ApiEditor),
);

View File

@ -1,105 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import type { InjectedFormProps } from "redux-form";
import { formValueSelector, reduxForm } from "redux-form";
import { API_EDITOR_FORM_NAME } from "ee/constants/forms";
import type { Action } from "entities/Action";
import type { AppState } from "ee/reducers";
import get from "lodash/get";
import {
getActionByBaseId,
getActionData,
} from "ee/selectors/entitiesSelector";
import type { CommonFormProps } from "../CommonEditorForm";
import CommonEditorForm from "../CommonEditorForm";
import Pagination from "PluginActionEditor/components/PluginActionForm/components/GraphQLEditor/Pagination";
import { GRAPHQL_HTTP_METHOD_OPTIONS } from "PluginActionEditor/constants/GraphQLEditorConstants";
import PostBodyData from "PluginActionEditor/components/PluginActionForm/components/GraphQLEditor/PostBodyData";
type APIFormProps = {
actionConfigurationBody: string;
} & CommonFormProps;
type Props = APIFormProps & InjectedFormProps<Action, APIFormProps>;
/**
* Graphql Editor form which uses the Common Editor and pass on the differentiating components from the API Editor.
* @param props using type Props
* @returns Graphql Editor Area which is used to editor APIs using GraphQL datasource.
*/
function GraphQLEditorForm(props: Props) {
const { actionName } = props;
return (
<CommonEditorForm
{...props}
bodyUIComponent={<PostBodyData actionName={actionName} />}
formName={API_EDITOR_FORM_NAME}
httpsMethods={GRAPHQL_HTTP_METHOD_OPTIONS}
paginationUIComponent={
<Pagination
actionName={actionName}
formName={API_EDITOR_FORM_NAME}
paginationType={props.paginationType}
query={props.actionConfigurationBody}
/>
}
/>
);
}
const selector = formValueSelector(API_EDITOR_FORM_NAME);
export default connect(
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(state: AppState, props: { pluginId: string; match?: any }) => {
const actionConfigurationHeaders =
selector(state, "actionConfiguration.headers") || [];
const actionConfigurationParams =
selector(state, "actionConfiguration.queryParameters") || [];
let datasourceFromAction = selector(state, "datasource");
if (datasourceFromAction && datasourceFromAction.hasOwnProperty("id")) {
datasourceFromAction = state.entities.datasources.list.find(
(d) => d.id === datasourceFromAction.id,
);
}
const { baseApiId, baseQueryId } = props.match?.params || {};
const baseActionId = baseQueryId || baseApiId;
const action = getActionByBaseId(state, baseActionId);
const apiId = action?.id ?? "";
const actionName = action?.name ?? "";
const hintMessages = action?.messages;
const datasourceHeaders =
get(datasourceFromAction, "datasourceConfiguration.headers") || [];
const datasourceParams =
get(datasourceFromAction, "datasourceConfiguration.queryParameters") ||
[];
const actionConfigurationBody =
selector(state, "actionConfiguration.body") || "";
const actionResponse = getActionData(state, apiId);
return {
actionName,
actionResponse,
actionConfigurationHeaders,
actionConfigurationParams,
actionConfigurationBody,
datasourceHeaders,
datasourceParams,
hintMessages,
};
},
)(
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reduxForm<Action, any>({
form: API_EDITOR_FORM_NAME,
enableReinitialize: true,
})(GraphQLEditorForm),
);

View File

@ -1,110 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import type { InjectedFormProps } from "redux-form";
import { formValueSelector, reduxForm } from "redux-form";
import { API_EDITOR_FORM_NAME } from "ee/constants/forms";
import type { Action } from "entities/Action";
import PostBodyData from "PluginActionEditor/components/PluginActionForm/components/ApiEditor/PostBodyData";
import type { AppState } from "ee/reducers";
import { getApiName } from "selectors/formSelectors";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import get from "lodash/get";
import { getAction, getActionResponses } from "ee/selectors/entitiesSelector";
import type { CommonFormProps } from "./CommonEditorForm";
import CommonEditorForm from "./CommonEditorForm";
import Pagination from "PluginActionEditor/components/PluginActionForm/components/ApiEditor/Pagination";
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
import { HTTP_METHOD_OPTIONS } from "PluginActionEditor/constants/CommonApiConstants";
type APIFormProps = {
httpMethodFromForm: string;
} & CommonFormProps;
type Props = APIFormProps & InjectedFormProps<Action, APIFormProps>;
function ApiEditorForm(props: Props) {
const { actionName } = props;
const theme = EditorTheme.LIGHT;
return (
<CommonEditorForm
{...props}
bodyUIComponent={
<PostBodyData dataTreePath={`${actionName}.config`} theme={theme} />
}
formName={API_EDITOR_FORM_NAME}
httpsMethods={HTTP_METHOD_OPTIONS}
paginationUIComponent={
<Pagination
actionName={actionName}
onTestClick={props.onRunClick}
paginationType={props.paginationType}
theme={theme}
/>
}
/>
);
}
const selector = formValueSelector(API_EDITOR_FORM_NAME);
export default connect((state: AppState) => {
const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod");
const actionConfigurationHeaders =
selector(state, "actionConfiguration.headers") || [];
const autoGeneratedActionConfigHeaders =
selector(state, "actionConfiguration.autoGeneratedHeaders") || [];
const actionConfigurationParams =
selector(state, "actionConfiguration.queryParameters") || [];
let datasourceFromAction = selector(state, "datasource");
if (datasourceFromAction && datasourceFromAction.hasOwnProperty("id")) {
datasourceFromAction = state.entities.datasources.list.find(
(d) => d.id === datasourceFromAction.id,
);
}
// get messages from action itself
const actionId = selector(state, "id");
const action = getAction(state, actionId);
const currentEnvironment = getCurrentEnvironmentId(state);
const hintMessages = action?.messages;
const datasourceHeaders =
get(
datasourceFromAction,
`datasourceStorages.${currentEnvironment}.datasourceConfiguration.headers`,
) || [];
const datasourceParams =
get(
datasourceFromAction,
`datasourceStorages.${currentEnvironment}.datasourceConfiguration.queryParameters`,
) || [];
const apiId = selector(state, "id");
const currentActionDatasourceId = selector(state, "datasource.id");
const actionName = getApiName(state, apiId) || "";
const responses = getActionResponses(state);
const actionResponse = responses[apiId];
return {
actionName,
actionResponse,
apiId,
httpMethodFromForm,
actionConfigurationHeaders,
actionConfigurationParams,
autoGeneratedActionConfigHeaders,
currentActionDatasourceId,
datasourceHeaders,
datasourceParams,
hintMessages,
};
})(
reduxForm<Action, APIFormProps>({
form: API_EDITOR_FORM_NAME,
enableReinitialize: true,
})(ApiEditorForm),
);

View File

@ -1,182 +0,0 @@
import React, { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import {
getIsActionConverting,
getPageList,
getPluginSettingConfigs,
getPlugins,
} from "ee/selectors/entitiesSelector";
import { runAction, saveActionName } from "actions/pluginActionActions";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import Editor from "./Editor";
import BackToCanvas from "components/common/BackToCanvas";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import {
getIsEditorInitialized,
getPagePermissions,
} from "selectors/editorSelectors";
import { getActionByBaseId } from "ee/selectors/entitiesSelector";
import type { APIEditorRouteParams } from "constants/routes";
import {
getHasCreateActionPermission,
getHasDeleteActionPermission,
getHasManageActionPermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { ApiEditorContextProvider } from "./ApiEditorContext";
import type { PaginationField } from "api/ActionAPI";
import { get, keyBy } from "lodash";
import ConvertToModuleInstanceCTA from "ee/pages/Editor/EntityEditor/ConvertToModuleInstanceCTA";
import { MODULE_TYPE } from "ee/constants/ModuleConstants";
import Disabler from "pages/common/Disabler";
import ConvertEntityNotification from "ee/pages/common/ConvertEntityNotification";
import { Icon } from "@appsmith/ads";
import { resolveIcon } from "../utils";
import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
type ApiEditorWrapperProps = RouteComponentProps<APIEditorRouteParams>;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getPageName(pages: any, basePageId: string) {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const page = pages.find((page: any) => page.basePageId === basePageId);
return page ? page.pageName : "";
}
function ApiEditorWrapper(props: ApiEditorWrapperProps) {
const { baseApiId = "", basePageId } = props.match.params;
const dispatch = useDispatch();
const isEditorInitialized = useSelector(getIsEditorInitialized);
const action = useSelector((state) => getActionByBaseId(state, baseApiId));
const apiName = action?.name || "";
const pluginId = get(action, "pluginId", "");
const datasourceId = action?.datasource.id || "";
const plugins = useSelector(getPlugins);
const pages = useSelector(getPageList);
const pageName = getPageName(pages, basePageId);
const settingsConfig = useSelector((state) =>
getPluginSettingConfigs(state, pluginId),
);
const pagePermissions = useSelector(getPagePermissions);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isConverting = useSelector((state) =>
getIsActionConverting(state, action?.id || ""),
);
const editorMode = useSelector(getIDEViewMode);
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
const icon = resolveIcon({
iconLocation: pluginGroups[pluginId]?.iconLocation || "",
pluginType: action?.pluginType || "",
moduleType: action?.actionConfiguration?.body?.moduleType,
}) || (
<EntityIcon
height={`${ENTITY_ICON_SIZE}px`}
width={`${ENTITY_ICON_SIZE}px`}
>
<Icon name="module" />
</EntityIcon>
);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
const isCreatePermitted = getHasCreateActionPermission(
isFeatureEnabled,
pagePermissions,
);
const moreActionsMenu = useMemo(() => {
const convertToModuleProps = {
canCreateModuleInstance: isCreatePermitted,
canDeleteEntity: isDeletePermitted,
entityId: action?.id || "",
moduleType: MODULE_TYPE.QUERY,
};
return (
<>
<MoreActionsMenu
basePageId={basePageId}
className="t--more-action-menu"
id={action?.id || ""}
isChangePermitted={isChangePermitted}
isDeletePermitted={isDeletePermitted}
name={action?.name || ""}
prefixAdditionalMenus={
editorMode === EditorViewMode.SplitScreen && (
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
)
}
/>
{editorMode !== EditorViewMode.SplitScreen && (
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
)}
</>
);
}, [
action?.id,
action?.name,
isChangePermitted,
isDeletePermitted,
basePageId,
isCreatePermitted,
editorMode,
]);
const handleRunClick = useCallback(
(paginationField?: PaginationField) => {
const pluginName = plugins.find((plugin) => plugin.id === pluginId)?.name;
AnalyticsUtil.logEvent("RUN_API_CLICK", {
apiName,
apiID: action?.id,
pageName: pageName,
datasourceId,
pluginName: pluginName,
isMock: false, // as mock db exists only for postgres and mongo plugins
});
dispatch(runAction(action?.id ?? "", paginationField));
},
[action?.id, apiName, pageName, plugins, pluginId, datasourceId, dispatch],
);
const actionRightPaneBackLink = useMemo(() => {
return <BackToCanvas basePageId={basePageId} />;
}, [basePageId]);
const notification = useMemo(() => {
if (!isConverting) return null;
return <ConvertEntityNotification icon={icon} name={action?.name || ""} />;
}, [action?.name, isConverting, icon]);
return (
<ApiEditorContextProvider
actionRightPaneBackLink={actionRightPaneBackLink}
handleRunClick={handleRunClick}
moreActionsMenu={moreActionsMenu}
notification={notification}
saveActionName={saveActionName}
settingsConfig={settingsConfig}
>
<Disabler isDisabled={isConverting}>
<Editor {...props} isEditorInitialized={isEditorInitialized} />
</Disabler>
</ApiEditorContextProvider>
);
}
export default ApiEditorWrapper;

View File

@ -1,4 +1,3 @@
import localStorage from "utils/localStorage";
import { render, waitFor } from "test/testUtils";
import { Route } from "react-router-dom";
import { BUILDER_PATH } from "ee/constants/routes/appRoutes";
@ -17,7 +16,6 @@ const FeatureFlags = {
const basePageId = "0123456789abcdef00000000";
describe("IDE Render: JS", () => {
localStorage.setItem("SPLITPANE_ANNOUNCEMENT", "false");
describe("JS Blank State", () => {
it("Renders Fullscreen Blank State", async () => {
const { findByText, getByRole, getByText } = render(
@ -132,7 +130,7 @@ describe("IDE Render: JS", () => {
},
});
const { container, getAllByText, getByRole, getByTestId } = render(
const { getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}>
<IDE />
</Route>,
@ -147,13 +145,13 @@ describe("IDE Render: JS", () => {
async () => {
const elements = getAllByText("JSObject1"); // Use the common test ID or selector
expect(elements).toHaveLength(3); // Wait until there are exactly 3 elements
expect(elements).toHaveLength(2); // Wait until there are exactly 2 elements
},
{ timeout: 3000, interval: 500 },
);
// There will be 3 JSObject1 text (Left pane list, editor tab and Editor form)
expect(getAllByText("JSObject1").length).toEqual(3);
// There will be 2 JSObject1 text (Left pane list and editor tab)
expect(getAllByText("JSObject1").length).toEqual(2);
// Left pane active state
expect(
getByTestId("t--entity-item-JSObject1").classList.contains("active"),
@ -162,13 +160,12 @@ describe("IDE Render: JS", () => {
expect(
getByTestId("t--ide-tab-jsobject1").classList.contains("active"),
).toBe(true);
// Check if the form is rendered
expect(container.querySelector(".js-editor-tab")).not.toBeNull();
// Check if the code and settings tabs is visible
getByRole("tab", { name: /code/i });
getByRole("tab", { name: /settings/i });
// Check if run button is visible
// Check toolbar elements
getByRole("button", { name: /myFun1/i });
getByRole("button", { name: /run/i });
getByTestId("t--js-settings-trigger");
getByTestId("t--more-action-trigger");
// Check if the Add new button is shown
getByTestId("t--add-item");
});
@ -190,7 +187,7 @@ describe("IDE Render: JS", () => {
ideView: EditorViewMode.SplitScreen,
});
const { container, getAllByText, getByRole, getByTestId } = render(
const { getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}>
<IDE />
</Route>,
@ -206,19 +203,17 @@ describe("IDE Render: JS", () => {
getByTestId("t--widgets-editor");
// Check if js is rendered in side by side
expect(getAllByText("JSObject2").length).toBe(2);
expect(getAllByText("JSObject2").length).toBe(1);
// Tabs active state
expect(
getByTestId("t--ide-tab-jsobject2").classList.contains("active"),
).toBe(true);
// Check if the form is rendered
expect(container.querySelector(".js-editor-tab")).not.toBeNull();
// Check if the code and settings tabs is visible
getByRole("tab", { name: /code/i });
getByRole("tab", { name: /settings/i });
// Check if run button is visible
// Check toolbar elements
getByRole("button", { name: /myFun1/i });
getByRole("button", { name: /run/i });
getByTestId("t--more-action-trigger");
// Check if the Add new button is shown
getByTestId("t--ide-tabs-add-button");
});

View File

@ -6,10 +6,8 @@ import { createMessage, EDITOR_PANE_TEXTS } from "ee/constants/messages";
import { BUILDER_PATH } from "ee/constants/routes/appRoutes";
import { EditorEntityTab, EditorViewMode } from "ee/entities/IDE/constants";
import { APIFactory } from "test/factories/Actions/API";
import localStorage from "utils/localStorage";
import { PostgresFactory } from "test/factories/Actions/Postgres";
import { sagasToRunForTests } from "test/sagas";
import userEvent from "@testing-library/user-event";
import { getIDETestState } from "test/factories/AppIDEFactoryUtils";
import { PageFactory } from "test/factories/PageFactory";
import { screen, waitFor } from "@testing-library/react";
@ -22,7 +20,6 @@ const FeatureFlags = {
const basePageId = "0123456789abcdef00000000";
describe("IDE URL rendering of Queries", () => {
localStorage.setItem("SPLITPANE_ANNOUNCEMENT", "false");
describe("Query Blank State", () => {
it("Renders Fullscreen Blank State", async () => {
const { findByText, getByRole, getByText } = render(
@ -144,7 +141,7 @@ describe("IDE URL rendering of Queries", () => {
},
});
const { getAllByText, getByRole, getByTestId } = render(
const { getAllByRole, getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}>
<IDE />
</Route>,
@ -159,13 +156,13 @@ describe("IDE URL rendering of Queries", () => {
async () => {
const elements = getAllByText("Api1"); // Use the common test ID or selector
expect(elements).toHaveLength(3); // Wait until there are exactly 3 elements
expect(elements).toHaveLength(2); // Wait until there are exactly 3 elements
},
{ timeout: 3000, interval: 500 },
);
// There will be 3 Api1 text (Left pane list, editor tab and Editor form)
expect(getAllByText("Api1").length).toEqual(3);
// There will be 2 Api1 text (Left pane list, editor tab)
expect(getAllByText("Api1").length).toEqual(2);
// Left pane active state
expect(
getByTestId("t--entity-item-Api1").classList.contains("active"),
@ -175,11 +172,11 @@ describe("IDE URL rendering of Queries", () => {
true,
);
// Check if the form is rendered
getByTestId("t--action-form-API");
getByTestId("t--api-editor-form");
// Check if the params tabs is visible
getByRole("tab", { name: /params/i });
// Check if run button is visible
getByRole("button", { name: /run/i });
expect(getAllByRole("button", { name: /run/i })).toHaveLength(2);
// Check if the Add new button is shown
getByTestId("t--add-item");
});
@ -201,7 +198,7 @@ describe("IDE URL rendering of Queries", () => {
ideView: EditorViewMode.SplitScreen,
});
const { getAllByText, getByRole, getByTestId } = render(
const { getAllByRole, getAllByText, getByTestId } = render(
<Route path={BUILDER_PATH}>
<IDE />
</Route>,
@ -217,15 +214,15 @@ describe("IDE URL rendering of Queries", () => {
getByTestId("t--widgets-editor");
// Check if api is rendered in side by side
expect(getAllByText("Api2").length).toBe(2);
expect(getAllByText("Api2").length).toBe(1);
// Tabs active state
expect(getByTestId("t--ide-tab-api2").classList.contains("active")).toBe(
true,
);
// Check if the form is rendered
getByTestId("t--action-form-API");
getByTestId("t--api-editor-form");
// Check if run button is visible
getByRole("button", { name: /run/i });
expect(getAllByRole("button", { name: /run/i }).length).toBe(2);
// Check if the Add new button is shown
getByTestId("t--ide-tabs-add-button");
});
@ -358,12 +355,12 @@ describe("IDE URL rendering of Queries", () => {
async () => {
const elements = getAllByText("Query1"); // Use the common test ID or selector
expect(elements).toHaveLength(3); // Wait until there are exactly 3 elements
expect(elements).toHaveLength(2); // Wait until there are exactly 3 elements
},
{ timeout: 3000, interval: 500 },
);
// There will be 3 Query1 text (Left pane list, editor tab and Editor form)
expect(getAllByText("Query1").length).toBe(3);
// There will be 2 Query1 text (Left pane list, editor tab)
expect(getAllByText("Query1").length).toBe(2);
// Left pane active state
expect(
getByTestId("t--entity-item-Query1").classList.contains("active"),
@ -372,11 +369,8 @@ describe("IDE URL rendering of Queries", () => {
expect(
getByTestId("t--ide-tab-query1").classList.contains("active"),
).toBe(true);
await userEvent.click(getByRole("tab", { name: "Query" }));
// Check if the form is rendered
getByTestId("t--action-form-DB");
getByTestId("t--uqi-editor-form");
// Check if run button is visible
getByRole("button", { name: /run/i });
// Check if the Add new button is shown
@ -417,16 +411,14 @@ describe("IDE URL rendering of Queries", () => {
getByTestId("t--widgets-editor");
// Check if api is rendered in side by side
expect(getAllByText("Query2").length).toBe(2);
expect(getAllByText("Query2").length).toBe(1);
// Tabs active state
expect(
getByTestId("t--ide-tab-query2").classList.contains("active"),
).toBe(true);
await userEvent.click(getByRole("tab", { name: "Query" }));
// Check if the form is rendered
getByTestId("t--action-form-DB");
getByTestId("t--uqi-editor-form");
// Check if run button is visible
getByRole("button", { name: /run/i });
// Check if the Add new button is shown
@ -449,7 +441,7 @@ describe("IDE URL rendering of Queries", () => {
},
});
const { container, getByTestId, getByText } = render(
const { getByTestId, getByText } = render(
<Route path={BUILDER_PATH}>
<IDE />
</Route>,
@ -461,8 +453,6 @@ describe("IDE URL rendering of Queries", () => {
},
);
screen.logTestingPlaygroundURL(container);
// Create options are rendered
getByText(createMessage(EDITOR_PANE_TEXTS.queries_create_from_existing));
getByText("New datasource");
@ -562,8 +552,8 @@ describe("IDE URL rendering of Queries", () => {
},
);
// There will be 3 Query1 text (Left pane list, editor tab and Editor form)
expect(getAllByText("Sheets1").length).toBe(3);
// There will be 2 Query1 text (Left pane list, editor tab)
expect(getAllByText("Sheets1").length).toBe(2);
// Left pane active state
expect(
getByTestId("t--entity-item-Sheets1").classList.contains("active"),
@ -573,10 +563,8 @@ describe("IDE URL rendering of Queries", () => {
getByTestId("t--ide-tab-sheets1").classList.contains("active"),
).toBe(true);
await userEvent.click(getByRole("tab", { name: "Query" }));
// Check if the form is rendered
getByTestId("t--action-form-SAAS");
getByTestId("t--uqi-editor-form");
// Check if run button is visible
getByRole("button", { name: /run/i });
// Check if the Add new button is shown
@ -618,18 +606,16 @@ describe("IDE URL rendering of Queries", () => {
getByTestId("t--widgets-editor");
// Check if api is rendered in side by side
expect(getAllByText("Sheets2").length).toBe(2);
expect(getAllByText("Sheets2").length).toBe(1);
// Tabs active state
expect(
getByTestId("t--ide-tab-sheets2").classList.contains("active"),
).toBe(true);
await userEvent.click(getByRole("tab", { name: "Query" }));
screen.logTestingPlaygroundURL(container);
// Check if the form is rendered
getByTestId("t--action-form-SAAS");
getByTestId("t--uqi-editor-form");
// Check if run button is visible
getByRole("button", { name: /run/i });
// Check if the Add new button is shown
@ -653,7 +639,7 @@ describe("IDE URL rendering of Queries", () => {
},
});
const { container, getByTestId, getByText } = render(
const { getByTestId, getByText } = render(
<Route path={BUILDER_PATH}>
<IDE />
</Route>,
@ -665,8 +651,6 @@ describe("IDE URL rendering of Queries", () => {
},
);
screen.logTestingPlaygroundURL(container);
// Create options are rendered
getByText(createMessage(EDITOR_PANE_TEXTS.queries_create_from_existing));
getByText("New datasource");

View File

@ -1,64 +0,0 @@
import React, { useState } from "react";
import { AnnouncementModal, Button } from "@appsmith/ads";
import localStorage, { LOCAL_STORAGE_KEYS } from "utils/localStorage";
import { SPLITPANE_ANNOUNCEMENT, createMessage } from "ee/constants/messages";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
const Announcement = () => {
const localStorageFlag =
localStorage.getItem(LOCAL_STORAGE_KEYS.SPLITPANE_ANNOUNCEMENT) || "true";
const [show, setShow] = useState(JSON.parse(localStorageFlag));
const tryClickHandler = () => {
setShow(false);
localStorage.setItem(LOCAL_STORAGE_KEYS.SPLITPANE_ANNOUNCEMENT, "false");
};
const learnClickHandler = () => {
window.open(
"https://community.appsmith.com/content/blog/discover-ide-20-building-more-efficient-ide",
"_blank",
);
};
const featureIsOutOfBeta = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
const modalFooter = () => (
<>
<Button
data-testid="t--ide-close-announcement"
kind="primary"
onClick={tryClickHandler}
size="md"
>
Try it out
</Button>
<Button kind="tertiary" onClick={learnClickHandler} size="md">
Learn more
</Button>
</>
);
// If the feature is out of beta, don't show the announcement
if (featureIsOutOfBeta) {
return null;
}
return (
<AnnouncementModal
banner={getAssetUrl(`${ASSETS_CDN_URL}/splitpane-banner.svg`)}
description={createMessage(SPLITPANE_ANNOUNCEMENT.DESCRIPTION)}
footer={modalFooter()}
isBeta
isOpen={show}
title={createMessage(SPLITPANE_ANNOUNCEMENT.TITLE)}
/>
);
};
export { Announcement };

View File

@ -15,7 +15,6 @@ const FeatureFlags = {
};
describe("EditorTabs render checks", () => {
localStorage.setItem("SPLITPANE_ANNOUNCEMENT", "false");
const page = PageFactory.build();
const renderComponent = (url: string, state: Partial<AppState>) =>

View File

@ -17,7 +17,6 @@ import Container from "./Container";
import { useCurrentEditorState, useIDETabClickHandlers } from "../hooks";
import { SCROLL_AREA_OPTIONS, TabSelectors } from "./constants";
import { AddButton } from "./AddButton";
import { Announcement } from "../EditorPane/components/Announcement";
import { useLocation } from "react-router";
import { identifyEntityFromPath } from "navigation/FocusEntity";
import { List } from "./List";
@ -162,9 +161,6 @@ const EditorTabs = () => {
{isListViewActive && ideViewMode === EditorViewMode.SplitScreen && (
<List />
)}
{/* Announcement modal */}
{ideViewMode === EditorViewMode.SplitScreen && <Announcement />}
</>
);
};

View File

@ -31,8 +31,6 @@ import { useEditorType } from "ee/hooks";
import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
import { useBoolean } from "usehooks-ts";
import { isWidgetActionConnectionPresent } from "selectors/onboardingSelectors";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import localStorage, { LOCAL_STORAGE_KEYS } from "utils/localStorage";
export const useCurrentEditorState = () => {
@ -212,12 +210,8 @@ export const useShowSideBySideNudge: () => [boolean, () => void] = () => {
LOCAL_STORAGE_KEYS.NUDGE_SHOWN_SPLIT_PANE,
);
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
const { setFalse, value } = useBoolean(
widgetBindingsExist && isActionRedesignEnabled && !localStorageFlag,
widgetBindingsExist && !localStorageFlag,
);
const dismissNudge = useCallback(() => {

View File

@ -55,14 +55,14 @@ import {
type JSActionDropdownOption,
convertJSActionToDropdownOption,
getJSActionOption,
type OnUpdateSettingsProps,
} from "./JSEditorToolbar";
import type { JSFunctionSettingsProps } from "./JSEditorToolbar/components/old/JSFunctionSettings";
interface JSFormProps {
jsCollectionData: JSCollectionData;
contextMenu: React.ReactNode;
showSettings?: boolean;
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
onUpdateSettings: (props: OnUpdateSettingsProps) => void;
saveJSObjectName: JSObjectNameEditorProps["saveJSObjectName"];
backLink?: React.ReactNode;
hideContextMenuOnEditor?: boolean;

View File

@ -12,8 +12,6 @@ import {
MenuTrigger,
Text,
} from "@appsmith/ads";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
export interface ContextMenuOption {
id?: string;
@ -37,10 +35,6 @@ export function JSEditorContextMenu({
onMenuClose,
options,
}: EntityContextMenuProps) {
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
if (options.length === 0) {
return null;
}
@ -59,8 +53,8 @@ export function JSEditorContextMenu({
data-testid="t--more-action-trigger"
isIconButton
kind="tertiary"
size={isActionRedesignEnabled ? "sm" : "md"}
startIcon={isActionRedesignEnabled ? "more-2-fill" : "context-menu"}
size={"sm"}
startIcon={"more-2-fill"}
/>
</MenuTrigger>
<MenuContent align="end" avoidCollisions>

View File

@ -1,7 +1,5 @@
import React from "react";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import type { JSEditorTab } from "reducers/uiReducers/jsPaneReducer";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import {
type BlockCompletion,
CodeEditorBorder,
@ -12,7 +10,6 @@ import {
} from "components/editorComponents/CodeEditor/EditorConfig";
import type { CodeEditorGutter } from "components/editorComponents/CodeEditor";
import type { JSAction, JSCollection } from "entities/JSCollection";
import { OldJSEditorForm } from "./old/JSEditorForm";
import type { OnUpdateSettingsProps } from "../JSEditorToolbar";
import LazyCodeEditor from "components/editorComponents/LazyCodeEditor";
import { Flex } from "@appsmith/ads";
@ -33,29 +30,6 @@ interface Props {
}
export const JSEditorForm = (props: Props) => {
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
if (!isActionRedesignEnabled) {
return (
<OldJSEditorForm
actions={props.actions}
blockCompletions={props.blockCompletions}
changePermitted={props.changePermitted}
currentJSCollection={props.currentJSCollection}
customGutter={props.customGutter}
executing={props.executing}
onChange={props.onChange}
onUpdateSettings={props.onUpdateSettings}
onValueChange={props.onValueChange}
showSettings={props.showSettings}
theme={props.theme}
value={props.value}
/>
);
}
return (
<Flex flex="1" overflowY="scroll">
<LazyCodeEditor

View File

@ -1,104 +0,0 @@
import { JSEditorTab } from "reducers/uiReducers/jsPaneReducer";
import React from "react";
import type {
BlockCompletion,
EditorTheme,
} from "components/editorComponents/CodeEditor/EditorConfig";
import {
CodeEditorBorder,
EditorModes,
EditorSize,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
import { TabbedViewContainer } from "../../styledComponents";
import { Tab, TabPanel, Tabs, TabsList } from "@appsmith/ads";
import LazyCodeEditor from "components/editorComponents/LazyCodeEditor";
import type { CodeEditorGutter } from "components/editorComponents/CodeEditor";
import type { JSAction, JSCollection } from "entities/JSCollection";
import { type OnUpdateSettingsProps } from "../../JSEditorToolbar";
import { JSFunctionSettings } from "../../JSEditorToolbar/components/JSFunctionSettings";
interface Props {
executing: boolean;
onValueChange: (value: string) => void;
value: JSEditorTab;
showSettings: undefined | boolean;
blockCompletions: Array<BlockCompletion>;
customGutter: CodeEditorGutter;
currentJSCollection: JSCollection;
changePermitted: boolean;
onChange: (valueOrEvent: React.ChangeEvent | string) => void;
theme: EditorTheme.LIGHT;
actions: JSAction[];
onUpdateSettings?: (props: OnUpdateSettingsProps) => void;
}
export function OldJSEditorForm(props: Props) {
return (
<TabbedViewContainer isExecuting={props.executing}>
<Tabs
defaultValue={JSEditorTab.CODE}
onValueChange={props.onValueChange}
value={props.value}
>
<TabsList>
<Tab
data-testid={`t--js-editor-` + JSEditorTab.CODE}
value={JSEditorTab.CODE}
>
Code
</Tab>
{props.showSettings && (
<Tab
data-testid={`t--js-editor-` + JSEditorTab.SETTINGS}
value={JSEditorTab.SETTINGS}
>
Settings
</Tab>
)}
</TabsList>
<TabPanel value={JSEditorTab.CODE}>
<div className="js-editor-tab">
<LazyCodeEditor
AIAssisted
blockCompletions={props.blockCompletions}
border={CodeEditorBorder.NONE}
borderLess
className={"js-editor"}
customGutter={props.customGutter}
dataTreePath={`${props.currentJSCollection.name}.body`}
disabled={!props.changePermitted}
folding
height={"100%"}
hideEvaluatedValue
input={{
value: props.currentJSCollection.body,
onChange: props.onChange,
}}
isJSObject
jsObjectName={props.currentJSCollection.name}
mode={EditorModes.JAVASCRIPT}
placeholder="Let's write some code!"
showLightningMenu={false}
showLineNumbers
size={EditorSize.EXTENDED}
tabBehaviour={TabBehaviour.INDENT}
theme={props.theme}
/>
</div>
</TabPanel>
{props.showSettings && (
<TabPanel value={JSEditorTab.SETTINGS}>
<div className="js-editor-tab">
<JSFunctionSettings
actions={props.actions}
disabled={!props.changePermitted}
onUpdateSettings={props.onUpdateSettings}
/>
</div>
</TabPanel>
)}
</Tabs>
</TabbedViewContainer>
);
}

View File

@ -34,17 +34,7 @@ const defaultProps = {
};
describe("JSEditorToolbar", () => {
it("renders JSHeader when action redesign is disabled", () => {
mockUseFeatureFlag.mockReturnValue(false);
render(<JSEditorToolbar {...defaultProps} />);
// Old header shows the name of the JS object
// since we don't provide the name via props, it has the placeholder text
expect(
screen.getByText("Name of the JS Object in camelCase"),
).toBeInTheDocument();
});
it("renders IDEToolbar with JSFunctionRun and JSFunctionSettings when action redesign is enabled", () => {
it("renders IDEToolbar with JSFunctionRun and JSFunctionSettings", () => {
mockUseFeatureFlag.mockReturnValue(true);
render(<JSEditorToolbar {...defaultProps} />);

View File

@ -1,17 +1,13 @@
import React, { useState } from "react";
import { IDEToolbar, ToolbarSettingsPopover } from "IDE";
import { JSFunctionRun } from "./components/JSFunctionRun";
import type { JSActionDropdownOption } from "./types";
import type { JSActionDropdownOption, OnUpdateSettingsProps } from "./types";
import type { SaveActionNameParams } from "PluginActionEditor";
import type { ReduxAction } from "actions/ReduxActionTypes";
import type { JSAction, JSCollection } from "entities/JSCollection";
import type { DropdownOnSelect } from "@appsmith/ads-old";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { createMessage, JS_EDITOR_SETTINGS } from "ee/constants/messages";
import { JSHeader } from "./JSHeader";
import { JSFunctionSettings } from "./components/JSFunctionSettings";
import type { JSFunctionSettingsProps } from "./components/old/JSFunctionSettings";
import { convertJSActionsToDropdownOptions } from "./utils";
import { JSObjectNameEditor } from "./JSObjectNameEditor";
@ -33,7 +29,7 @@ interface Props {
onSelect: DropdownOnSelect;
jsActions: JSAction[];
selected: JSActionDropdownOption;
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
onUpdateSettings: (props: OnUpdateSettingsProps) => void;
showNameEditor?: boolean;
showSettings: boolean;
}
@ -41,23 +37,12 @@ interface Props {
/**
* JSEditorToolbar component.
*
* This component renders a toolbar for the JS editor. It conditionally renders
* different components based on the `release_actions_redesign_enabled` feature flag.
* This component renders a toolbar for the JS editor.
*
*/
export const JSEditorToolbar = (props: Props) => {
// Check if the action redesign feature flag is enabled
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
const [isOpen, setIsOpen] = useState(false);
// If the action redesign is not enabled, render the JSHeader component
if (!isActionRedesignEnabled) {
return <JSHeader {...props} />;
}
// Render the IDEToolbar with JSFunctionRun and JSFunctionSettings components
return (
<IDEToolbar>

View File

@ -1,59 +0,0 @@
import React from "react";
import { JSFunctionRun } from "./components/JSFunctionRun";
import type { JSActionDropdownOption } from "./types";
import { ActionButtons, NameWrapper, StyledFormRow } from "../styledComponents";
import type { SaveActionNameParams } from "PluginActionEditor";
import type { ReduxAction } from "actions/ReduxActionTypes";
import type { JSAction, JSCollection } from "entities/JSCollection";
import type { DropdownOnSelect } from "@appsmith/ads-old";
import { JSObjectNameEditor } from "./JSObjectNameEditor";
import { Flex } from "@appsmith/ads";
import { convertJSActionsToDropdownOptions } from "./utils";
interface Props {
changePermitted: boolean;
hideEditIconOnEditor?: boolean;
saveJSObjectName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
hideContextMenuOnEditor?: boolean;
contextMenu: React.ReactNode;
disableRunFunctionality: boolean;
executePermitted: boolean;
loading: boolean;
jsCollection: JSCollection;
onButtonClick: (
event: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent,
) => void;
onSelect: DropdownOnSelect;
jsActions: JSAction[];
selected: JSActionDropdownOption;
}
export const JSHeader = (props: Props) => {
return (
<Flex paddingTop="spaces-5">
<StyledFormRow className="form-row-header">
<NameWrapper className="t--nameOfJSObject">
<JSObjectNameEditor
disabled={!props.changePermitted || props.hideEditIconOnEditor}
saveJSObjectName={props.saveJSObjectName}
/>
</NameWrapper>
<ActionButtons className="t--formActionButtons">
{!props.hideContextMenuOnEditor && props.contextMenu}
<JSFunctionRun
disabled={props.disableRunFunctionality || !props.executePermitted}
isLoading={props.loading}
jsCollection={props.jsCollection}
onButtonClick={props.onButtonClick}
onSelect={props.onSelect}
options={convertJSActionsToDropdownOptions(props.jsActions)}
selected={props.selected}
showTooltip={!props.selected.data}
/>
</ActionButtons>
</StyledFormRow>
</Flex>
);
};

View File

@ -1,7 +1,5 @@
import React, { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import type { ReduxAction } from "actions/ReduxActionTypes";
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
import { getAssetUrl } from "ee/utils/airgapHelpers";
@ -14,7 +12,6 @@ import {
getJsCollectionByBaseId,
getPlugin,
} from "ee/selectors/entitiesSelector";
import { JSObjectNameEditor as OldJSObjectNameEditor } from "./old/JSObjectNameEditor";
import { EditableName, useIsRenaming } from "IDE";
export interface SaveActionNameParams {
@ -102,10 +99,6 @@ export const JSObjectNameEditor = ({
[currentJSObjectConfig, saveJSObjectName],
);
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
const icon = useMemo(() => {
if (!currentPlugin) return null;
@ -119,15 +112,6 @@ export const JSObjectNameEditor = ({
);
}, [currentPlugin]);
if (!isActionRedesignEnabled) {
return (
<OldJSObjectNameEditor
disabled={disabled}
saveJSObjectName={saveJSObjectName}
/>
);
}
return (
<NameWrapper
data-testid="t--js-object-name-editor"

View File

@ -1,116 +0,0 @@
import React from "react";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { removeSpecialChars } from "utils/helpers";
import type { AppState } from "ee/reducers";
import {
getJsCollectionByBaseId,
getPlugin,
} from "ee/selectors/entitiesSelector";
import {
ACTION_NAME_PLACEHOLDER,
JS_OBJECT_ID_NOT_FOUND_IN_URL,
createMessage,
} from "ee/constants/messages";
import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { Flex } from "@appsmith/ads";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import NameEditorComponent, {
IconBox,
IconWrapper,
NameWrapper,
} from "components/utils/NameEditorComponent";
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
import type { ReduxAction } from "actions/ReduxActionTypes";
import type { SaveActionNameParams } from "PluginActionEditor";
export interface JSObjectNameEditorProps {
disabled?: boolean;
saveJSObjectName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
}
export function 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 saveStatus = useSelector((state) =>
getSavingStatusForJSObjectName(state, currentJSObjectConfig?.id || ""),
);
return (
<NameEditorComponent
id={currentJSObjectConfig?.id}
idUndefinedErrorMessage={JS_OBJECT_ID_NOT_FOUND_IN_URL}
name={currentJSObjectConfig?.name}
onSaveName={props.saveJSObjectName}
saveStatus={saveStatus}
>
{({
forceUpdate,
handleNameChange,
isInvalidNameForEntity,
isNew,
saveStatus,
}: {
forceUpdate: boolean;
handleNameChange: (value: string) => void;
isInvalidNameForEntity: (value: string) => string | boolean;
isNew: boolean;
saveStatus: { isSaving: boolean; error: boolean };
}) => (
<NameWrapper enableFontStyling>
<Flex
alignItems="center"
gap="spaces-3"
overflow="hidden"
width="100%"
>
{currentPlugin && (
<IconBox>
<IconWrapper
alt={currentPlugin.name}
src={getAssetUrl(currentPlugin.iconLocation)}
/>
</IconBox>
)}
<EditableText
className="t--js-action-name-edit-field"
defaultValue={
currentJSObjectConfig ? currentJSObjectConfig.name : ""
}
disabled={props.disabled}
editInteractionKind={EditInteractionKind.SINGLE}
errorTooltipClass="t--action-name-edit-error"
forceDefault={forceUpdate}
isEditingDefault={isNew}
isInvalid={isInvalidNameForEntity}
onTextChanged={handleNameChange}
placeholder={createMessage(ACTION_NAME_PLACEHOLDER, "JS Object")}
type="text"
underline
updating={saveStatus.isSaving}
valueTransform={removeSpecialChars}
/>
</Flex>
</NameWrapper>
)}
</NameEditorComponent>
);
}
export default JSObjectNameEditor;

View File

@ -1,107 +0,0 @@
import React from "react";
import "@testing-library/jest-dom";
import { render, screen, fireEvent } from "test/testUtils";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { JSObjectFactory } from "test/factories/Actions/JSObject";
import { convertJSActionsToDropdownOptions } from "../utils";
import { JSFunctionRun } from "./JSFunctionRun";
import { JS_FUNCTION_RUN_NAME_LENGTH } from "./constants";
jest.mock("utils/hooks/useFeatureFlag");
const mockUseFeatureFlag = useFeatureFlag as jest.Mock;
const JSObject = JSObjectFactory.build();
const mockProps = {
disabled: false,
isLoading: false,
jsCollection: JSObject,
onButtonClick: jest.fn(),
onSelect: jest.fn(),
options: convertJSActionsToDropdownOptions(JSObject.actions),
selected: {
label: JSObject.actions[0].name,
value: JSObject.actions[0].name,
data: JSObject.actions[0],
},
showTooltip: false,
};
describe("JSFunctionRun", () => {
it("renders OldJSFunctionRun when feature flag is disabled", () => {
mockUseFeatureFlag.mockReturnValue(false);
render(<JSFunctionRun {...mockProps} />);
expect(screen.getByText("myFun1")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Run" })).toBeInTheDocument();
});
it("renders new JSFunctionRun when feature flag is enabled", () => {
mockUseFeatureFlag.mockReturnValue(true);
render(<JSFunctionRun {...mockProps} />);
// Assert the Function select is a popup menu
expect(screen.getByText("myFun1")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "myFun1" })).toHaveAttribute(
"aria-haspopup",
"menu",
);
});
// This test is skipped because menu does not open in the test environment
// eslint-disable-next-line jest/no-disabled-tests
it.skip("calls onSelect when a menu item is selected", () => {
mockUseFeatureFlag.mockReturnValue(true);
render(<JSFunctionRun {...mockProps} />);
// click the button to open the menu
fireEvent.click(screen.getByRole("button", { name: "myFun1" }));
fireEvent.click(screen.getByText("myFun2"));
expect(mockProps.onSelect).toHaveBeenCalledWith("myFun2");
});
it("disables the button when props.disabled is true", () => {
mockUseFeatureFlag.mockReturnValue(true);
render(<JSFunctionRun {...mockProps} disabled />);
expect(screen.getByRole("button", { name: "myFun1" })).toBeDisabled();
});
// This test is skipped because tooltip does not show in the test environment
// eslint-disable-next-line jest/no-disabled-tests
it.skip("shows tooltip when showTooltip is true", () => {
mockUseFeatureFlag.mockReturnValue(true);
render(<JSFunctionRun {...mockProps} showTooltip />);
fireEvent.mouseOver(screen.getByText("Run"));
expect(
screen.getByText("No JS function to run in TestCollection"),
).toBeInTheDocument();
});
it("calls onButtonClick when run button is clicked", () => {
mockUseFeatureFlag.mockReturnValue(true);
render(<JSFunctionRun {...mockProps} />);
fireEvent.click(screen.getByText("Run"));
expect(mockProps.onButtonClick).toHaveBeenCalled();
});
it("truncates long names to 30 characters", () => {
mockUseFeatureFlag.mockReturnValue(true);
const options = [
{
label:
"aReallyReallyLongFunctionNameThatConveysALotOfMeaningAndCannotBeShortenedAtAllBecauseItConveysALotOfMeaningAndCannotBeShortened",
value: "1",
},
];
const [selected] = options;
const jsCollection = { name: "CollectionName" };
const params = { options, selected, jsCollection } as Parameters<
typeof JSFunctionRun
>[0];
render(<JSFunctionRun {...params} />);
expect(screen.getByTestId("t--js-function-run").textContent?.length).toBe(
JS_FUNCTION_RUN_NAME_LENGTH,
);
});
});

View File

@ -1,9 +1,6 @@
import React, { useCallback } from "react";
import { truncate } from "lodash";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { JSFunctionRun as OldJSFunctionRun } from "./old/JSFunctionRun";
import type { JSCollection } from "entities/JSCollection";
import {
Button,
@ -37,9 +34,6 @@ interface Props {
*/
export const JSFunctionRun = (props: Props) => {
const { onSelect } = props;
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
// Callback function to handle function selection from the dropdown menu
const onFunctionSelect = useCallback(
@ -51,11 +45,6 @@ export const JSFunctionRun = (props: Props) => {
[onSelect],
);
if (!isActionRedesignEnabled) {
return <OldJSFunctionRun {...props} />;
}
// Render the new version of the component
return (
<Flex gap="spaces-2">
<Menu>

View File

@ -1,22 +1,18 @@
import React, { useCallback, useState } from "react";
import { Flex, Switch, Text } from "@appsmith/ads";
import JSFunctionSettingsView, {
type JSFunctionSettingsProps,
} from "./old/JSFunctionSettings";
import type { JSAction } from "entities/JSCollection";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import {
createMessage,
JS_EDITOR_SETTINGS,
NO_JS_FUNCTIONS,
} from "ee/constants/messages";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import type { OnUpdateSettingsProps } from "../types";
interface Props {
disabled: boolean;
actions: JSAction[];
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
onUpdateSettings: (props: OnUpdateSettingsProps) => void;
}
interface FunctionSettingsRowProps extends Omit<Props, "actions"> {
@ -73,22 +69,6 @@ const FunctionSettingRow = (props: FunctionSettingsRowProps) => {
* It conditionally renders the old or new version of the component based on a feature flag.
*/
export const JSFunctionSettings = (props: Props) => {
const isActionRedesignEnabled = useFeatureFlag(
FEATURE_FLAG.release_actions_redesign_enabled,
);
// If the feature flag is disabled, render the old version of the component
if (!isActionRedesignEnabled) {
return (
<JSFunctionSettingsView
actions={props.actions}
disabled={props.disabled}
onUpdateSettings={props.onUpdateSettings}
/>
);
}
// Render the new version of the component
return (
<Flex flexDirection="column" gap="spaces-4" w="100%">
<Text kind="heading-xs">

View File

@ -1,114 +0,0 @@
import React from "react";
import styled from "styled-components";
import type { JSCollection } from "entities/JSCollection";
import type { SelectProps } from "@appsmith/ads";
import { Button, Option, Select, Tooltip, Text } from "@appsmith/ads";
import { createMessage, NO_JS_FUNCTION_TO_RUN } from "ee/constants/messages";
import type { JSActionDropdownOption } from "../../types";
import { RUN_BUTTON_DEFAULTS, testLocators } from "../../constants";
interface Props {
disabled: boolean;
isLoading: boolean;
jsCollection: JSCollection;
onButtonClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onSelect: SelectProps["onSelect"];
options: JSActionDropdownOption[];
selected: JSActionDropdownOption;
showTooltip: boolean;
}
export interface DropdownWithCTAWrapperProps {
isDisabled: boolean;
}
const DropdownWithCTAWrapper = styled.div<DropdownWithCTAWrapperProps>`
display: flex;
gap: var(--ads-v2-spaces-3);
`;
const OptionWrapper = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;
const OptionLabelWrapper = styled.div<{ fullSize?: boolean }>`
width: ${(props) => (props?.fullSize ? "100%" : "80%")};
overflow: hidden;
`;
const OptionLabel = styled(Text)`
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
`;
export function JSFunctionRun({
disabled,
isLoading,
jsCollection,
onButtonClick,
onSelect,
options,
selected,
showTooltip,
}: Props) {
return (
<DropdownWithCTAWrapper isDisabled={disabled}>
<Select
className="function-select-dropdown"
isDisabled={disabled}
onSelect={onSelect}
size="md"
value={
selected.label && {
key: selected.label,
label: (
<OptionLabelWrapper fullSize>
<OptionLabel renderAs="p">{selected.label}</OptionLabel>
</OptionLabelWrapper>
),
}
}
virtual={false}
>
{options.map((option) => (
<Option key={option.value}>
<OptionWrapper>
<Tooltip
content={option.label}
// Here, 18 is the maximum charecter length because the width of this menu does not change
isDisabled={(option.label?.length || 0) < 18}
placement="right"
>
<OptionLabelWrapper>
<OptionLabel renderAs="p">{option.label}</OptionLabel>
</OptionLabelWrapper>
</Tooltip>
</OptionWrapper>
</Option>
))}
</Select>
<Tooltip
content={createMessage(NO_JS_FUNCTION_TO_RUN, jsCollection.name)}
isDisabled={!showTooltip}
placement="topRight"
>
{/* this span exists to make the disabled button visible to the tooltip */}
<span>
<Button
className={testLocators.runJSAction}
data-testid={testLocators.runJSActionTestID}
isDisabled={disabled}
isLoading={isLoading}
onClick={onButtonClick}
size="md"
>
{RUN_BUTTON_DEFAULTS.CTA_TEXT}
</Button>
</span>
</Tooltip>
</DropdownWithCTAWrapper>
);
}

View File

@ -1,316 +0,0 @@
import {
createMessage,
FUNCTION_SETTINGS_HEADING,
NO_JS_FUNCTIONS,
} from "ee/constants/messages";
import type { JSAction } from "entities/JSCollection";
import React, { useCallback, useState } from "react";
import styled from "styled-components";
import {
CONFIRM_BEFORE_CALLING_HEADING,
SETTINGS_HEADINGS,
} from "../../../constants";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { Icon, Switch, Tooltip } from "@appsmith/ads";
import RemoveConfirmationModal from "../../../RemoveConfirmBeforeCallingDialog";
import type { OnUpdateSettingsProps } from "../../types";
interface SettingsHeadingProps {
text: string;
hasInfo?: boolean;
info?: string;
grow: boolean;
headingCount: number;
hidden?: boolean;
}
interface SettingsItemProps {
headingCount: number;
action: JSAction;
disabled?: boolean;
onUpdateSettings?: (props: OnUpdateSettingsProps) => void;
renderAdditionalColumns?: (
action: JSAction,
headingCount: number,
) => React.ReactNode;
}
export interface JSFunctionSettingsProps {
actions: JSAction[];
disabled?: boolean;
onUpdateSettings: SettingsItemProps["onUpdateSettings"];
renderAdditionalColumns?: SettingsItemProps["renderAdditionalColumns"];
additionalHeadings?: typeof SETTINGS_HEADINGS;
}
const SettingRow = styled.div<{ isHeading?: boolean; noBorder?: boolean }>`
display: flex;
padding: 8px;
${(props) =>
!props.noBorder &&
`
border-bottom: solid 1px var(--ads-v2-color-border);
`}
${(props) =>
props.isHeading &&
`
background: var(--ads-v2-color-bg-subtle);
font-size: ${props.theme.typography.h5.fontSize}px;
`};
`;
const StyledIcon = styled(Icon)`
width: max-content;
height: max-content;
`;
export const SettingColumn = styled.div<{
headingCount: number;
grow?: boolean;
isHeading?: boolean;
hidden?: boolean;
}>`
visibility: ${(props) => (props.hidden ? "hidden" : "visible")};
display: flex;
align-items: center;
flex-grow: ${(props) => (props.grow ? 1 : 0)};
padding: 5px 12px;
width: ${({ headingCount }) => `calc(100% / ${headingCount})`};
${(props) =>
props.isHeading &&
`
font-weight: ${props.theme.fontWeights[2]};
font-size: ${props.theme.fontSizes[2]}px;
margin-right: 9px;
`}
${StyledIcon} {
margin-left: 8px;
}
`;
const JSFunctionSettingsWrapper = styled.div`
height: 100%;
overflow: hidden;
`;
const SettingsContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
& > h3 {
margin: 20px 0;
font-size: ${(props) => props.theme.fontSizes[5]}px;
font-weight: ${(props) => props.theme.fontWeights[2]};
color: var(--ads-v2-color-fg-emphasis);
}
overflow: hidden;
`;
const SettingsRowWrapper = styled.div`
border-radius: var(--ads-v2-border-radius);
height: 100%;
overflow: hidden;
`;
const SettingsHeaderWrapper = styled.div``;
const SettingsBodyWrapper = styled.div`
overflow: auto;
max-height: calc(100% - 48px);
`;
const SwitchWrapper = styled.div`
margin-left: 6ch;
`;
function SettingsHeading({
grow,
hasInfo,
headingCount,
hidden,
info,
text,
}: SettingsHeadingProps) {
return (
<SettingColumn
grow={grow}
headingCount={headingCount}
hidden={hidden}
isHeading
>
<span>{text}</span>
{hasInfo && info && (
<Tooltip content={createMessage(() => info)}>
<StyledIcon name="question-line" size="md" />
</Tooltip>
)}
</SettingColumn>
);
}
function SettingsItem({
action,
headingCount,
onUpdateSettings,
renderAdditionalColumns,
}: SettingsItemProps) {
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
const [executeOnPageLoad, setExecuteOnPageLoad] = useState(
String(!!action.executeOnLoad),
);
const [confirmBeforeExecute, setConfirmBeforeExecute] = useState(
String(!!action.confirmBeforeExecute),
);
const onChangeExecuteOnPageLoad = (value: string) => {
setExecuteOnPageLoad(value);
onUpdateSettings?.({
value: value === "true",
propertyName: "executeOnLoad",
action,
});
AnalyticsUtil.logEvent("JS_OBJECT_SETTINGS_CHANGED", {
toggleSetting: "ON_PAGE_LOAD",
toggleValue: value,
});
};
const onChangeConfirmBeforeExecute = (value: string) => {
setConfirmBeforeExecute(value);
onUpdateSettings?.({
value: value === "true",
propertyName: "confirmBeforeExecute",
action,
});
AnalyticsUtil.logEvent("JS_OBJECT_SETTINGS_CHANGED", {
toggleSetting: "CONFIRM_BEFORE_RUN",
toggleValue: value,
});
};
const showConfirmBeforeExecute = action.confirmBeforeExecute;
const onRemoveConfirm = useCallback(() => {
setShowConfirmationModal(false);
onChangeConfirmBeforeExecute("false");
}, []);
const onCancel = useCallback(() => {
setShowConfirmationModal(false);
}, []);
return (
<SettingRow
className="t--async-js-function-settings"
id={`${action.name}-settings`}
>
<SettingColumn grow headingCount={headingCount}>
<span>{action.name}</span>
</SettingColumn>
<SettingColumn
className={`${action.name}-on-page-load-setting`}
headingCount={headingCount}
>
<SwitchWrapper>
<Switch
defaultSelected={JSON.parse(executeOnPageLoad)}
name={`execute-on-page-load-${action.id}`}
onChange={(isSelected) =>
onChangeExecuteOnPageLoad(String(isSelected))
}
/>
</SwitchWrapper>
</SettingColumn>
<SettingColumn
className={`${action.name}-confirm-before-execute`}
headingCount={headingCount}
>
<SwitchWrapper>
{showConfirmBeforeExecute ? (
<Switch
className="flex justify-center "
isSelected={JSON.parse(confirmBeforeExecute)}
name={`confirm-before-execute-${action.id}`}
onChange={() => setShowConfirmationModal(true)}
/>
) : null}
</SwitchWrapper>
</SettingColumn>
{renderAdditionalColumns?.(action, headingCount)}
<RemoveConfirmationModal
isOpen={showConfirmationModal}
onCancel={onCancel}
onConfirm={onRemoveConfirm}
/>
</SettingRow>
);
}
function JSFunctionSettingsView({
actions,
additionalHeadings = [],
disabled = false,
onUpdateSettings,
renderAdditionalColumns,
}: JSFunctionSettingsProps) {
const showConfirmBeforeExecuteOption = actions.some(
(action) => action.confirmBeforeExecute === true,
);
const headings = [...SETTINGS_HEADINGS, ...additionalHeadings];
headings.forEach((heading) => {
if (heading.key === CONFIRM_BEFORE_CALLING_HEADING.key) {
CONFIRM_BEFORE_CALLING_HEADING.hidden = !showConfirmBeforeExecuteOption;
}
});
return (
<JSFunctionSettingsWrapper>
<SettingsContainer>
<h3>{createMessage(FUNCTION_SETTINGS_HEADING)}</h3>
<SettingsRowWrapper>
<SettingsHeaderWrapper>
<SettingRow isHeading>
{headings.map((setting, index) => (
<SettingsHeading
grow={index === 0}
hasInfo={setting.hasInfo}
headingCount={headings.length}
hidden={setting?.hidden}
info={setting.info}
key={setting.key}
text={setting.text}
/>
))}
</SettingRow>
</SettingsHeaderWrapper>
<SettingsBodyWrapper>
{actions && actions.length ? (
actions.map((action) => (
<SettingsItem
action={action}
disabled={disabled}
headingCount={headings.length}
key={action.id}
onUpdateSettings={onUpdateSettings}
renderAdditionalColumns={renderAdditionalColumns}
/>
))
) : (
<SettingRow noBorder>
<SettingColumn headingCount={0}>
{createMessage(NO_JS_FUNCTIONS)}
</SettingColumn>
</SettingRow>
)}
</SettingsBodyWrapper>
</SettingsRowWrapper>
</SettingsContainer>
</JSFunctionSettingsWrapper>
);
}
export default JSFunctionSettingsView;

View File

@ -1,114 +0,0 @@
import React from "react";
import { useSelector } from "react-redux";
import { Icon } from "@appsmith/ads";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
import { CREATE_NEW_DATASOURCE, createMessage } from "ee/constants/messages";
import styled from "styled-components";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import {
getHasCreateDatasourcePermission,
getHasManageActionPermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import type { Action } from "entities/Action";
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
import { getPluginImages } from "ee/selectors/entitiesSelector";
import type { Datasource } from "entities/Datasource";
import type { Plugin } from "entities/Plugin";
import type { AppState } from "ee/reducers";
import { getCurrentAppWorkspace } from "ee/selectors/selectedWorkspaceSelectors";
const DropdownSelect = styled.div`
font-size: 14px;
width: 230px;
.rc-select-selector {
min-width: unset;
}
`;
const CreateDatasource = styled.div`
display: flex;
gap: 8px;
`;
interface Props {
formName: string;
currentActionConfig?: Action;
plugin?: Plugin;
dataSources: Datasource[];
onCreateDatasourceClick: () => void;
}
interface DATASOURCES_OPTIONS_TYPE {
label: string;
value: string;
image: string;
}
const DatasourceSelector = (props: Props) => {
const {
currentActionConfig,
dataSources,
formName,
onCreateDatasourceClick,
plugin,
} = props;
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const userWorkspacePermissions = useSelector(
(state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [],
);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const canCreateDatasource = getHasCreateDatasourcePermission(
isFeatureEnabled,
userWorkspacePermissions,
);
const showDatasourceSelector = doesPluginRequireDatasource(plugin);
const pluginImages = useSelector(getPluginImages);
const DATASOURCES_OPTIONS: Array<DATASOURCES_OPTIONS_TYPE> =
dataSources.reduce(
(acc: Array<DATASOURCES_OPTIONS_TYPE>, dataSource: Datasource) => {
if (dataSource.pluginId === plugin?.id) {
acc.push({
label: dataSource.name,
value: dataSource.id,
image: pluginImages[dataSource.pluginId],
});
}
return acc;
},
[],
);
if (!showDatasourceSelector) return null;
return (
<DropdownSelect>
<DropdownField
className={"t--switch-datasource"}
formName={formName}
isDisabled={!isChangePermitted}
name="datasource.id"
options={DATASOURCES_OPTIONS}
placeholder="Datasource"
>
{canCreateDatasource && (
// this additional div is here so that rc-select can render the child with the onClick correctly
<div>
<CreateDatasource onClick={() => onCreateDatasourceClick()}>
<Icon className="createIcon" name="plus" size="md" />
{createMessage(CREATE_NEW_DATASOURCE)}
</CreateDatasource>
</div>
)}
</DropdownField>
</DropdownSelect>
);
};
export default DatasourceSelector;

View File

@ -1,376 +0,0 @@
import React from "react";
import type { RouteComponentProps } from "react-router";
import { connect } from "react-redux";
import { getFormValues } from "redux-form";
import styled from "styled-components";
import type { QueryEditorRouteParams } from "constants/routes";
import QueryEditorForm from "./Form";
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions";
import {
deleteAction,
runAction,
setActionResponseDisplayFormat,
setActionProperty,
} from "actions/pluginActionActions";
import type { AppState } from "ee/reducers";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { QUERY_EDITOR_FORM_NAME } from "ee/constants/forms";
import { type Plugin, UIComponentTypes } from "entities/Plugin";
import type { Datasource } from "entities/Datasource";
import {
getPluginIdsOfPackageNames,
getPlugins,
getActionByBaseId,
getActionResponses,
getDatasourceByPluginId,
getDBAndRemoteDatasources,
} from "ee/selectors/entitiesSelector";
import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants";
import type { QueryAction, SaaSAction } from "entities/Action";
import Spinner from "components/editorComponents/Spinner";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { initFormEvaluations } from "actions/evaluationActions";
import { getUIComponent } from "./helpers";
import type { Diff } from "deep-diff";
import { diff } from "deep-diff";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import { getConfigInitialValues } from "components/formControls/utils";
import { merge } from "lodash";
import { getPathAndValueFromActionDiffObject } from "../../../utils/getPathAndValueFromActionDiffObject";
import { getCurrentEnvironmentDetails } from "ee/selectors/environmentSelectors";
import { QueryEditorContext } from "./QueryEditorContext";
import {
isActionDeleting,
isActionRunning,
isPluginActionCreating,
} from "PluginActionEditor/store";
const EmptyStateContainer = styled.div`
display: flex;
height: 100%;
font-size: 20px;
`;
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;
interface ReduxDispatchProps {
runAction: (actionId: string) => void;
deleteAction: (id: string, name: string) => void;
initFormEvaluation: (
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editorConfig: any,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingConfig: any,
formId: string,
) => void;
updateActionResponseDisplayFormat: ({
field,
id,
value,
}: UpdateActionPropertyActionPayload) => void;
setActionProperty: (
actionId: string,
propertyName: string,
value: string,
) => void;
}
interface ReduxStateProps {
plugins: Plugin[];
dataSources: Datasource[];
isRunning: boolean;
isDeleting: boolean;
formData: QueryAction | SaaSAction;
runErrorMessage: Record<string, string>;
pluginId: string | undefined;
pluginIds: Array<string> | undefined;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
responses: any;
isCreating: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editorConfig: any;
uiComponent: UIComponentTypes;
applicationId: string;
actionId: string;
baseActionId: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionObjectDiff?: any;
isSaas: boolean;
datasourceId?: string;
currentEnvironmentId: string;
currentEnvironmentName: string;
}
type StateAndRouteProps = RouteComponentProps<QueryEditorRouteParams>;
type OwnProps = StateAndRouteProps & {
isEditorInitialized: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingsConfig: any;
};
type Props = ReduxDispatchProps & ReduxStateProps & OwnProps;
class QueryEditor extends React.Component<Props> {
static contextType = QueryEditorContext;
context!: React.ContextType<typeof QueryEditorContext>;
constructor(props: Props) {
super(props);
// Call the first evaluations when the page loads
// call evaluations only for queries and not google sheets (which uses apiId)
if (this.props.match.params.baseQueryId) {
this.props.initFormEvaluation(
this.props.editorConfig,
this.props.settingsConfig,
this.props.match.params.baseQueryId,
);
}
}
componentDidMount() {
// if the current action is non existent, do not dispatch change query page action
// this action should only be dispatched when switching from an existent action.
if (!this.props.pluginId) return;
this.context?.changeQueryPage?.(this.props.baseActionId);
// fixes missing where key issue by populating the action with a where object when the component is mounted.
if (this.props.isSaas) {
const { path = "", value = "" } = {
...getPathAndValueFromActionDiffObject(this.props.actionObjectDiff),
};
if (value && path) {
this.props.setActionProperty(this.props.actionId, path, value);
}
}
}
handleDeleteClick = () => {
const { formData } = this.props;
this.props.deleteAction(this.props.actionId, formData.name);
};
handleRunClick = () => {
const { dataSources } = this.props;
const datasource = dataSources.find(
(datasource) => datasource.id === this.props.datasourceId,
);
const pluginName = this.props.plugins.find(
(plugin) => plugin.id === this.props.pluginId,
)?.name;
AnalyticsUtil.logEvent("RUN_QUERY_CLICK", {
actionId: this.props.actionId,
dataSourceSize: dataSources.length,
environmentId: this.props.currentEnvironmentId,
environmentName: this.props.currentEnvironmentName,
pluginName: pluginName,
datasourceId: datasource?.id,
isMock: !!datasource?.isMock,
});
this.props.runAction(this.props.actionId);
};
componentDidUpdate(prevProps: Props) {
// Update the page when the queryID is changed by changing the
// URL or selecting new query from the query pane
// reusing same logic for changing query panes for switching query editor datasources, since the operations are similar.
if (
prevProps.baseActionId !== this.props.baseActionId ||
prevProps.pluginId !== this.props.pluginId
) {
this.context?.changeQueryPage?.(this.props.baseActionId);
}
}
render() {
const {
actionId,
dataSources,
editorConfig,
isCreating,
isDeleting,
isEditorInitialized,
isRunning,
pluginId,
pluginIds,
responses,
runErrorMessage,
uiComponent,
updateActionResponseDisplayFormat,
} = this.props;
const { onCreateDatasourceClick, onEntityNotFoundBackClick } = this.context;
// if the action can not be found, generate a entity not found page
if (!pluginId && actionId) {
return <EntityNotFoundPane goBackFn={onEntityNotFoundBackClick} />;
}
if (!pluginIds?.length) {
return (
<EmptyStateContainer>{"Plugin is not installed"}</EmptyStateContainer>
);
}
if (isCreating || !isEditorInitialized) {
return (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
);
}
return (
<QueryEditorForm
actionResponse={responses[actionId]}
dataSources={dataSources}
datasourceId={this.props.datasourceId}
editorConfig={editorConfig}
formData={this.props.formData}
isDeleting={isDeleting}
isRunning={isRunning}
location={this.props.location}
onCreateDatasourceClick={onCreateDatasourceClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
pluginId={this.props.pluginId}
runErrorMessage={runErrorMessage[actionId]}
settingConfig={this.props.settingsConfig}
uiComponent={uiComponent}
updateActionResponseDisplayFormat={updateActionResponseDisplayFormat}
/>
);
}
}
const mapStateToProps = (state: AppState, props: OwnProps): ReduxStateProps => {
const { baseApiId, baseQueryId } = props.match.params;
const baseActionId = baseQueryId || baseApiId || "";
const { runErrorMessage } = state.ui.pluginActionEditor;
const { plugins } = state.entities;
const { editorConfigs } = plugins;
const action = getActionByBaseId(state, baseActionId) as
| QueryAction
| SaaSAction;
const actionId = action?.id;
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as
| QueryAction
| SaaSAction;
let pluginId;
if (action) {
pluginId = action.pluginId;
}
const isCreating = isPluginActionCreating(state);
const isDeleting = isActionDeleting(actionId)(state);
const isRunning = isActionRunning(actionId)(state);
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let editorConfig: any;
if (editorConfigs && pluginId) {
editorConfig = editorConfigs[pluginId];
}
const initialValues = {};
if (editorConfig) {
merge(initialValues, getConfigInitialValues(editorConfig));
}
if (props.settingsConfig) {
merge(initialValues, getConfigInitialValues(props.settingsConfig));
}
// initialValues contains merge of action, editorConfig, settingsConfig and will be passed to redux form
merge(initialValues, action);
// @ts-expect-error: Types are not available
const actionObjectDiff: undefined | Diff<Action | undefined, Action>[] = diff(
action,
initialValues,
);
const allPlugins = getPlugins(state);
let uiComponent = UIComponentTypes.DbEditorForm;
if (!!pluginId) uiComponent = getUIComponent(pluginId, allPlugins);
const currentEnvDetails = getCurrentEnvironmentDetails(state);
return {
actionId,
baseActionId,
currentEnvironmentId: currentEnvDetails?.id || "",
currentEnvironmentName: currentEnvDetails?.name || "",
pluginId,
plugins: allPlugins,
runErrorMessage,
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
dataSources: !!baseApiId
? getDatasourceByPluginId(state, action?.pluginId)
: getDBAndRemoteDatasources(state),
responses: getActionResponses(state),
isCreating,
isRunning,
isDeleting,
isSaas: !!baseApiId,
formData,
editorConfig,
uiComponent,
applicationId: getCurrentApplicationId(state),
actionObjectDiff,
datasourceId: action?.datasource?.id,
};
};
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
deleteAction: (id: string, name: string) =>
dispatch(deleteAction({ id, name })),
runAction: (actionId: string) => dispatch(runAction(actionId)),
initFormEvaluation: (
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editorConfig: any,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingsConfig: any,
formId: string,
) => {
dispatch(initFormEvaluations(editorConfig, settingsConfig, formId));
},
updateActionResponseDisplayFormat: ({
field,
id,
value,
}: UpdateActionPropertyActionPayload) => {
dispatch(setActionResponseDisplayFormat({ id, field, value }));
},
setActionProperty: (
actionId: string,
propertyName: string,
value: string,
) => {
dispatch(setActionProperty({ actionId, propertyName, value }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(QueryEditor);

View File

@ -1,382 +0,0 @@
import { useContext } from "react";
import React, { useCallback } from "react";
import type { InjectedFormProps } from "redux-form";
import { noop } from "lodash";
import type { Datasource } from "entities/Datasource";
import type { Action, QueryAction, SaaSAction } from "entities/Action";
import { useDispatch, useSelector } from "react-redux";
import ActionSettings from "pages/Editor/ActionSettings";
import { Button, Tab, TabPanel, Tabs, TabsList, Tooltip } from "@appsmith/ads";
import styled from "styled-components";
import FormRow from "components/editorComponents/FormRow";
import {
createMessage,
DOCUMENTATION,
DOCUMENTATION_TOOLTIP,
} from "ee/constants/messages";
import { useParams } from "react-router";
import type { AppState } from "ee/reducers";
import { thinScrollbar } from "constants/DefaultTheme";
import type { ActionResponse } from "api/ActionAPI";
import type { Plugin, UIComponentTypes } from "entities/Plugin";
import { EDITOR_TABS, SQL_DATASOURCES } from "constants/QueryEditorConstants";
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
import {
getPluginActionConfigSelectedTab,
setPluginActionEditorSelectedTab,
} from "PluginActionEditor/store";
import type { SourceEntity } from "entities/AppsmithConsole";
import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
import { DocsLink, openDoc } from "constants/DocumentationLinks";
import { QueryEditorContext } from "./QueryEditorContext";
import QueryDebuggerTabs from "./QueryDebuggerTabs";
import useShowSchema from "PluginActionEditor/components/PluginActionResponse/hooks/useShowSchema";
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
import FormRender from "PluginActionEditor/components/PluginActionForm/components/UQIEditor/FormRender";
import QueryEditorHeader from "./QueryEditorHeader";
import RunHistory from "ee/components/RunHistory";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasExecuteActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { getPluginNameFromId } from "ee/selectors/entitiesSelector";
const QueryFormContainer = styled.form`
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: var(--ads-v2-spaces-5) 0 0;
width: 100%;
.statementTextArea {
font-size: 14px;
line-height: 20px;
margin-top: 5px;
}
.queryInput {
max-width: 30%;
padding-right: 10px;
}
.executeOnLoad {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
`;
const SettingsWrapper = styled.div`
${thinScrollbar};
height: 100%;
`;
const SecondaryWrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
`;
export const StyledFormRow = styled(FormRow)`
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-5)
var(--ads-v2-spaces-7);
flex: 0;
`;
const TabContainerView = styled.div`
display: flex;
align-items: start;
flex: 1;
overflow: auto;
${thinScrollbar}
a {
font-size: 14px;
line-height: 20px;
margin-top: 12px;
}
position: relative;
& > .ads-v2-tabs {
height: 100%;
& > .ads-v2-tabs__panel {
height: calc(100% - 50px);
overflow-y: scroll;
}
}
`;
const TabsListWrapper = styled.div`
padding: 0 var(--ads-v2-spaces-7);
`;
const TabPanelWrapper = styled(TabPanel)`
padding: 0 var(--ads-v2-spaces-7);
`;
const Wrapper = styled.div`
display: flex;
flex-direction: row;
height: calc(100% - 50px);
overflow: hidden;
width: 100%;
`;
const DocumentationButton = styled(Button)`
position: absolute !important;
right: 24px;
margin: 7px 0 0;
z-index: 6;
`;
export const SegmentedControlContainer = styled.div`
padding: 0 var(--ads-v2-spaces-7);
padding-top: var(--ads-v2-spaces-4);
display: flex;
flex-direction: column;
gap: var(--ads-v2-spaces-4);
overflow-y: clip;
overflow-x: scroll;
`;
const StyledNotificationWrapper = styled.div`
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-3)
var(--ads-v2-spaces-7);
`;
interface QueryFormProps {
onDeleteClick: () => void;
onRunClick: () => void;
onCreateDatasourceClick: () => void;
isDeleting: boolean;
isRunning: boolean;
dataSources: Datasource[];
uiComponent: UIComponentTypes;
actionResponse?: ActionResponse;
runErrorMessage: string | undefined;
location: {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: any;
};
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editorConfig?: any;
formName: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settingConfig: any;
formData: SaaSAction | QueryAction;
responseDisplayFormat: { title: string; value: string };
datasourceId: string;
showCloseEditor: boolean;
}
interface ReduxProps {
actionName: string;
plugin?: Plugin;
pluginId: string;
documentationLink: string | undefined;
formEvaluationState: FormEvalOutput;
}
export type EditorJSONtoFormProps = QueryFormProps & ReduxProps;
type Props = EditorJSONtoFormProps &
InjectedFormProps<Action, EditorJSONtoFormProps>;
export function EditorJSONtoForm(props: Props) {
const {
actionName,
actionResponse,
dataSources,
documentationLink,
editorConfig,
formName,
handleSubmit,
isRunning,
onCreateDatasourceClick,
onRunClick,
plugin,
runErrorMessage,
settingConfig,
uiComponent,
} = props;
const { actionRightPaneAdditionSections, notification } =
useContext(QueryEditorContext);
const params = useParams<{ baseApiId?: string; baseQueryId?: string }>();
// fetch the error count from the store.
const actions: Action[] = useSelector((state: AppState) =>
state.entities.actions.map((action) => action.config),
);
const currentActionConfig: Action | undefined = actions.find(
(action) =>
action.baseId === params.baseApiId ||
action.baseId === params.baseQueryId,
);
const pluginRequireDatasource = doesPluginRequireDatasource(plugin);
const showSchema =
useShowSchema(currentActionConfig?.pluginId || "") &&
pluginRequireDatasource;
const dispatch = useDispatch();
const handleDocumentationClick = () => {
openDoc(DocsLink.QUERY, plugin?.documentationLink, plugin?.name);
};
// action source for analytics.
const actionSource: SourceEntity = {
type: SOURCE_ENTITY_TYPE.ACTION,
name: currentActionConfig ? currentActionConfig.name : "",
id: currentActionConfig ? currentActionConfig.id : "",
};
const selectedTab = useSelector(getPluginActionConfigSelectedTab);
const setSelectedConfigTab = useCallback(
(selectedIndex: string) => {
dispatch(setPluginActionEditorSelectedTab(selectedIndex));
},
[dispatch],
);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isExecutePermitted = getHasExecuteActionPermission(
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
// get the current action's plugin name
const currentActionPluginName = useSelector((state: AppState) =>
getPluginNameFromId(state, currentActionConfig?.pluginId || ""),
);
let actionBody = "";
if (!!currentActionConfig?.actionConfiguration) {
if ("formData" in currentActionConfig?.actionConfiguration) {
// if the action has a formData (the action is postUQI e.g. Oracle)
actionBody =
currentActionConfig.actionConfiguration.formData?.body?.data || "";
} else {
// if the action is pre UQI, the path is different e.g. mySQL
actionBody = currentActionConfig.actionConfiguration?.body || "";
}
}
// if (the body is empty and the action is an sql datasource) or the user does not have permission, block action execution.
const blockExecution =
(!actionBody && SQL_DATASOURCES.includes(currentActionPluginName)) ||
!isExecutePermitted;
// when switching between different redux forms, make sure this redux form has been initialized before rendering anything.
// the initialized prop below comes from redux-form.
if (!props.initialized) {
return null;
}
return (
<QueryFormContainer onSubmit={handleSubmit(noop)}>
<QueryEditorHeader
dataSources={dataSources}
formName={formName}
isRunDisabled={blockExecution}
isRunning={isRunning}
onCreateDatasourceClick={onCreateDatasourceClick}
onRunClick={onRunClick}
plugin={plugin}
/>
{notification && (
<StyledNotificationWrapper>{notification}</StyledNotificationWrapper>
)}
<Wrapper>
<div className="flex flex-1 w-full">
<SecondaryWrapper>
<TabContainerView>
<Tabs
onValueChange={setSelectedConfigTab}
value={selectedTab || EDITOR_TABS.QUERY}
>
<TabsListWrapper>
<TabsList>
<Tab
data-testid={`t--query-editor-` + EDITOR_TABS.QUERY}
value={EDITOR_TABS.QUERY}
>
Query
</Tab>
<Tab
data-testid={`t--query-editor-` + EDITOR_TABS.SETTINGS}
value={EDITOR_TABS.SETTINGS}
>
Settings
</Tab>
</TabsList>
</TabsListWrapper>
<TabPanelWrapper
className="tab-panel"
value={EDITOR_TABS.QUERY}
>
<SettingsWrapper
data-testid={`t--action-form-${plugin?.type}`}
>
<FormRender
editorConfig={editorConfig}
formData={props.formData}
formEvaluationState={props.formEvaluationState}
formName={formName}
uiComponent={uiComponent}
/>
</SettingsWrapper>
</TabPanelWrapper>
<TabPanelWrapper value={EDITOR_TABS.SETTINGS}>
<SettingsWrapper>
<ActionSettings
actionSettingsConfig={settingConfig}
formName={formName}
/>
</SettingsWrapper>
</TabPanelWrapper>
</Tabs>
{documentationLink && (
<Tooltip
content={createMessage(DOCUMENTATION_TOOLTIP)}
placement="top"
>
<DocumentationButton
className="t--datasource-documentation-link"
kind="tertiary"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handleDocumentationClick();
}}
size="sm"
startIcon="book-line"
>
{createMessage(DOCUMENTATION)}
</DocumentationButton>
</Tooltip>
)}
</TabContainerView>
<QueryDebuggerTabs
actionName={actionName}
actionResponse={actionResponse}
actionSource={actionSource}
currentActionConfig={currentActionConfig}
isRunDisabled={blockExecution}
isRunning={isRunning}
onRunClick={onRunClick}
runErrorMessage={runErrorMessage}
showSchema={showSchema}
/>
<RunHistory />
</SecondaryWrapper>
</div>
{actionRightPaneAdditionSections}
</Wrapper>
</QueryFormContainer>
);
}

View File

@ -1,59 +0,0 @@
import { formValueSelector, reduxForm } from "redux-form";
import { QUERY_EDITOR_FORM_NAME } from "ee/constants/forms";
import type { Action } from "entities/Action";
import { connect } from "react-redux";
import type { AppState } from "ee/reducers";
import {
getPluginResponseTypes,
getPluginDocumentationLinks,
getPlugin,
getActionData,
} from "ee/selectors/entitiesSelector";
import type { EditorJSONtoFormProps } from "./EditorJSONtoForm";
import { EditorJSONtoForm } from "./EditorJSONtoForm";
import { getFormEvaluationState } from "selectors/formSelectors";
import { actionResponseDisplayDataFormats } from "../utils";
const valueSelector = formValueSelector(QUERY_EDITOR_FORM_NAME);
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapStateToProps = (state: AppState, props: any) => {
const actionId = valueSelector(state, "id");
const actionName = valueSelector(state, "name");
const pluginId = valueSelector(state, "datasource.pluginId");
const selectedDbId = valueSelector(state, "datasource.id");
const actionData = getActionData(state, actionId);
const { responseDataTypes, responseDisplayFormat } =
actionResponseDisplayDataFormats(actionData);
const responseTypes = getPluginResponseTypes(state);
const documentationLinks = getPluginDocumentationLinks(state);
const plugin = getPlugin(state, pluginId);
// State to manage the evaluations for the form
let formEvaluationState = {};
// Fetching evaluations state only once the formData is populated
if (!!props.formData) {
formEvaluationState = getFormEvaluationState(state)[props.formData.id];
}
return {
actionName,
plugin,
pluginId,
selectedDbId,
responseDataTypes,
responseDisplayFormat,
responseType: responseTypes[pluginId],
documentationLink: documentationLinks[pluginId],
formName: QUERY_EDITOR_FORM_NAME,
formEvaluationState,
};
};
export default connect(mapStateToProps)(
reduxForm<Action, EditorJSONtoFormProps>({
form: QUERY_EDITOR_FORM_NAME,
enableReinitialize: true,
})(EditorJSONtoForm),
);

View File

@ -1,89 +0,0 @@
import React from "react";
import { render } from "@testing-library/react";
import configureStore from "redux-mock-store";
import { Provider } from "react-redux";
import { ThemeProvider } from "styled-components";
import { unitTestBaseMockStore } from "layoutSystems/common/dropTarget/unitTestUtils";
import { lightTheme } from "selectors/themeSelectors";
import { BrowserRouter as Router } from "react-router-dom";
import { EditorViewMode } from "ee/entities/IDE/constants";
import "@testing-library/jest-dom/extend-expect";
import QueryDebuggerTabs from "./QueryDebuggerTabs";
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
const mockStore = configureStore([]);
const storeState = {
...unitTestBaseMockStore,
evaluations: {
tree: {},
},
entities: {
plugins: {
list: [],
},
datasources: {
structure: {},
list: [],
},
},
ui: {
...unitTestBaseMockStore.ui,
users: {
featureFlag: {
data: {},
overriddenFlags: {},
},
},
ide: {
view: EditorViewMode.FullScreen,
},
debugger: {
context: {
errorCount: 0,
},
},
pluginActionEditor: {
debugger: {
open: true,
responseTabHeight: 200,
selectedTab: "response",
},
},
},
};
describe("ApiResponseView", () => {
let store = mockStore(storeState);
beforeEach(() => {
store = mockStore(storeState);
});
it("the container should have class select-text to enable the selection of text for user", () => {
const { container } = render(
<Provider store={store}>
<ThemeProvider theme={lightTheme}>
<Router>
<QueryDebuggerTabs
actionName="Query1"
actionSource={{
id: "ID1",
name: "Query1",
type: ENTITY_TYPE.ACTION,
}}
isRunning={false}
onRunClick={() => {}}
/>
</Router>
</ThemeProvider>
</Provider>,
);
expect(
container
.querySelector(".t--query-bottom-pane-container")
?.classList.contains("select-text"),
).toBe(true);
});
});

View File

@ -1,68 +0,0 @@
import type { ReduxAction } from "actions/ReduxActionTypes";
import type { SaveActionNameParams } from "PluginActionEditor";
import React, { createContext, useMemo } from "react";
interface QueryEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
onCreateDatasourceClick?: () => void;
onEntityNotFoundBackClick?: () => void;
changeQueryPage?: (baseQueryId: string) => void;
actionRightPaneBackLink?: React.ReactNode;
saveActionName: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
actionRightPaneAdditionSections?: React.ReactNode;
showSuggestedWidgets?: boolean;
notification?: string | React.ReactNode;
}
type QueryEditorContextProviderProps =
React.PropsWithChildren<QueryEditorContextContextProps>;
export const QueryEditorContext = createContext<QueryEditorContextContextProps>(
{} as QueryEditorContextContextProps,
);
export function QueryEditorContextProvider({
actionRightPaneAdditionSections,
actionRightPaneBackLink,
changeQueryPage,
children,
moreActionsMenu,
notification,
onCreateDatasourceClick,
onEntityNotFoundBackClick,
saveActionName,
showSuggestedWidgets,
}: QueryEditorContextProviderProps) {
const value = useMemo(
() => ({
actionRightPaneBackLink,
actionRightPaneAdditionSections,
changeQueryPage,
moreActionsMenu,
onCreateDatasourceClick,
onEntityNotFoundBackClick,
saveActionName,
showSuggestedWidgets,
notification,
}),
[
actionRightPaneBackLink,
actionRightPaneAdditionSections,
changeQueryPage,
moreActionsMenu,
onCreateDatasourceClick,
onEntityNotFoundBackClick,
saveActionName,
showSuggestedWidgets,
notification,
],
);
return (
<QueryEditorContext.Provider value={value}>
{children}
</QueryEditorContext.Provider>
);
}

View File

@ -1,128 +0,0 @@
import React, { useContext } from "react";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import { Button } from "@appsmith/ads";
import { StyledFormRow } from "./EditorJSONtoForm";
import styled from "styled-components";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
import { useSelector } from "react-redux";
import { getActionByBaseId, getPlugin } from "ee/selectors/entitiesSelector";
import { QueryEditorContext } from "./QueryEditorContext";
import type { Plugin } from "entities/Plugin";
import type { Datasource } from "entities/Datasource";
import type { AppState } from "ee/reducers";
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;
justify-content: space-between;
align-items: center;
width: 50%;
input {
margin: 0;
box-sizing: border-box;
}
`;
const ActionsWrapper = styled.div`
display: flex;
align-items: center;
flex: 1 1 50%;
justify-content: flex-end;
gap: var(--ads-v2-spaces-3);
width: 50%;
`;
interface Props {
plugin?: Plugin;
formName: string;
dataSources: Datasource[];
onCreateDatasourceClick: () => void;
isRunDisabled?: boolean;
isRunning: boolean;
onRunClick: () => void;
}
const QueryEditorHeader = (props: Props) => {
const {
dataSources,
formName,
isRunDisabled = false,
isRunning,
onCreateDatasourceClick,
onRunClick,
plugin,
} = props;
const { moreActionsMenu, saveActionName } = useContext(QueryEditorContext);
const activeActionBaseId = useActiveActionBaseId();
const currentActionConfig = useSelector((state) =>
activeActionBaseId
? getActionByBaseId(state, activeActionBaseId)
: undefined,
);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const isDatasourceSelectorEnabled = useFeatureFlag(
FEATURE_FLAG.release_ide_datasource_selector_enabled,
);
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);
return (
<StyledFormRow>
<NameWrapper>
<ActionNameEditor
actionConfig={currentActionConfig}
disabled={!isChangePermitted}
icon={icon}
saveActionName={saveActionName}
saveStatus={saveStatus}
/>
</NameWrapper>
<ActionsWrapper>
{moreActionsMenu}
{isDatasourceSelectorEnabled && (
<DatasourceSelector
currentActionConfig={currentActionConfig}
dataSources={dataSources}
formName={formName}
onCreateDatasourceClick={onCreateDatasourceClick}
plugin={plugin}
/>
)}
<Button
className="t--run-query"
data-guided-tour-iid="run-query"
isDisabled={isRunDisabled}
isLoading={isRunning}
onClick={onRunClick}
size="md"
>
Run
</Button>
</ActionsWrapper>
</StyledFormRow>
);
};
export default QueryEditorHeader;

View File

@ -1,210 +0,0 @@
import React, { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import Editor from "./Editor";
import history from "utils/history";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import BackToCanvas from "components/common/BackToCanvas";
import { INTEGRATION_TABS } from "constants/routes";
import {
getCurrentApplicationId,
getIsEditorInitialized,
getPagePermissions,
} from "selectors/editorSelectors";
import { changeQuery } from "PluginActionEditor/store";
import { DatasourceCreateEntryPoints } from "constants/Datasource";
import {
getActionByBaseId,
getIsActionConverting,
getPluginImages,
getPluginSettingConfigs,
} from "ee/selectors/entitiesSelector";
import { integrationEditorURL } from "ee/RouteBuilder";
import { QueryEditorContextProvider } from "./QueryEditorContext";
import type { QueryEditorRouteParams } from "constants/routes";
import {
getHasCreateActionPermission,
getHasDeleteActionPermission,
getHasManageActionPermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import Disabler from "pages/common/Disabler";
import ConvertToModuleInstanceCTA from "ee/pages/Editor/EntityEditor/ConvertToModuleInstanceCTA";
import { MODULE_TYPE } from "ee/constants/ModuleConstants";
import ConvertEntityNotification from "ee/pages/common/ConvertEntityNotification";
import { PluginType } from "entities/Plugin";
import { Icon } from "@appsmith/ads";
import { resolveIcon } from "../utils";
import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import { saveActionName } from "actions/pluginActionActions";
type QueryEditorProps = RouteComponentProps<QueryEditorRouteParams>;
function QueryEditor(props: QueryEditorProps) {
const { baseApiId, basePageId, baseQueryId } = props.match.params;
const baseActionId = baseQueryId || baseApiId;
const dispatch = useDispatch();
const action = useSelector((state) =>
getActionByBaseId(state, baseActionId || ""),
);
const pluginId = action?.pluginId || "";
const isEditorInitialized = useSelector(getIsEditorInitialized);
const applicationId: string = useSelector(getCurrentApplicationId);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const settingsConfig = useSelector((state) =>
getPluginSettingConfigs(state, pluginId),
);
const pagePermissions = useSelector(getPagePermissions);
const isConverting = useSelector((state) =>
getIsActionConverting(state, action?.id || ""),
);
const pluginImages = useSelector(getPluginImages);
const editorMode = useSelector(getIDEViewMode);
const icon = resolveIcon({
iconLocation: pluginImages[pluginId] || "",
pluginType: action?.pluginType || "",
moduleType: action?.actionConfiguration?.body?.moduleType,
}) || (
<EntityIcon
height={`${ENTITY_ICON_SIZE}px`}
width={`${ENTITY_ICON_SIZE}px`}
>
<Icon name="module" />
</EntityIcon>
);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
const isCreatePermitted = getHasCreateActionPermission(
isFeatureEnabled,
pagePermissions,
);
const moreActionsMenu = useMemo(() => {
const convertToModuleProps = {
canCreateModuleInstance: isCreatePermitted,
canDeleteEntity: isDeletePermitted,
entityId: action?.id || "",
moduleType: MODULE_TYPE.QUERY,
};
return (
<>
<MoreActionsMenu
basePageId={basePageId}
className="t--more-action-menu"
id={action?.id || ""}
isChangePermitted={isChangePermitted}
isDeletePermitted={isDeletePermitted}
name={action?.name || ""}
prefixAdditionalMenus={
editorMode === EditorViewMode.SplitScreen && (
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
)
}
/>
{action?.pluginType !== PluginType.INTERNAL &&
editorMode !== EditorViewMode.SplitScreen && (
// Need to remove this check once workflow query is supported in module
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
)}
</>
);
}, [
action?.id,
action?.name,
action?.pluginType,
isChangePermitted,
isDeletePermitted,
basePageId,
isCreatePermitted,
editorMode,
]);
const actionRightPaneBackLink = useMemo(() => {
return <BackToCanvas basePageId={basePageId} />;
}, [basePageId]);
const changeQueryPage = useCallback(
(baseQueryId: string) => {
dispatch(
changeQuery({ baseQueryId: baseQueryId, basePageId, applicationId }),
);
},
[basePageId, applicationId, dispatch],
);
const onCreateDatasourceClick = useCallback(() => {
history.push(
integrationEditorURL({
basePageId: basePageId,
selectedTab: INTEGRATION_TABS.NEW,
}),
);
// Event for datasource creation click
const entryPoint = DatasourceCreateEntryPoints.QUERY_EDITOR;
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
entryPoint,
});
}, [basePageId]);
// custom function to return user to integrations page if action is not found
const onEntityNotFoundBackClick = useCallback(
() =>
history.push(
integrationEditorURL({
basePageId: basePageId,
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
),
[basePageId],
);
const notification = useMemo(() => {
if (!isConverting) return null;
return (
<ConvertEntityNotification
icon={icon}
name={action?.name || ""}
withPadding
/>
);
}, [action?.name, isConverting, icon]);
return (
<QueryEditorContextProvider
actionRightPaneBackLink={actionRightPaneBackLink}
changeQueryPage={changeQueryPage}
moreActionsMenu={moreActionsMenu}
notification={notification}
onCreateDatasourceClick={onCreateDatasourceClick}
onEntityNotFoundBackClick={onEntityNotFoundBackClick}
saveActionName={saveActionName}
>
<Disabler isDisabled={isConverting}>
<Editor
{...props}
isEditorInitialized={isEditorInitialized}
settingsConfig={settingsConfig}
/>
</Disabler>
</QueryEditorContextProvider>
);
}
export default QueryEditor;

View File

@ -45,6 +45,7 @@ import {
import type {
Action,
ApiAction,
AutoGeneratedHeader,
CreateApiActionDefaultsParams,
} from "entities/Action";
import { type Plugin, PluginPackageName, PluginType } from "entities/Plugin";
@ -60,8 +61,7 @@ import { getCurrentBasePageId } from "selectors/editorSelectors";
import { validateResponse } from "./ErrorSagas";
import type { CreateDatasourceSuccessAction } from "actions/datasourceActions";
import { removeTempDatasource } from "actions/datasourceActions";
import type { AutoGeneratedHeader } from "pages/Editor/APIEditor/helpers";
import { deriveAutoGeneratedHeaderState } from "pages/Editor/APIEditor/helpers";
import { deriveAutoGeneratedHeaderState } from "../PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/utils/autoGeneratedHeaders";
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import type { FeatureFlags } from "ee/entities/FeatureFlag";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";

View File

@ -9,7 +9,6 @@ import { toast } from "@appsmith/ads";
export const LOCAL_STORAGE_KEYS = {
CANVAS_CARDS_STATE: "CANVAS_CARDS_STATE",
SPLITPANE_ANNOUNCEMENT: "SPLITPANE_ANNOUNCEMENT",
NUDGE_SHOWN_SPLIT_PANE: "NUDGE_SHOWN_SPLIT_PANE",
};