chore: action editors refactor (#27972)

## Description
The aim of this PR is to make the editors reusable in the Module editor.

Changes
1. A wrapper is introduced for API, Query and Curl editors which passes
differentiating functions and make the form editors agnostic of pageId
and applicationId
2. In order to pass down function, react contexts are added to avoid
prop drilling

#### PR fixes following issue(s)
Fixes #26160 

#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
- Chore (housekeeping or task changes that don't impact user perception)

## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
ashit-rath 2023-10-17 10:53:55 +05:30 committed by GitHub
parent 20463ee449
commit d48ac4fd81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1166 additions and 811 deletions

View File

@ -2,18 +2,20 @@ import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { Action } from "entities/Action";
export const changeQuery = (
id: string,
newQuery?: boolean,
action?: Action,
): ReduxAction<{
export interface ChangeQueryPayload {
id: string;
packageId?: string;
applicationId?: string;
pageId?: string;
moduleId?: string;
newQuery?: boolean;
action?: any;
}> => {
action?: Action;
}
export const changeQuery = (payload: ChangeQueryPayload) => {
return {
type: ReduxActionTypes.QUERY_PANE_CHANGE,
payload: { id, newQuery, action },
payload,
};
};

View File

@ -0,0 +1,46 @@
import React, { useCallback, useContext } from "react";
import styled from "styled-components";
import { Link } from "design-system";
import history from "utils/history";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { BACK_TO_CANVAS, createMessage } from "@appsmith/constants/messages";
import { builderURL } from "@appsmith/RouteBuilder";
const BackToCanvasLink = styled(Link)`
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
margin-top: ${(props) => props.theme.spaces[11]}px;
margin-bottom: ${(props) => props.theme.spaces[11]}px;
`;
interface BackToCanvasProps {
pageId: string;
}
function BackToCanvas({ pageId }: BackToCanvasProps) {
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
const handleCloseWalkthrough = useCallback(() => {
if (isWalkthroughOpened && popFeature) {
popFeature();
}
}, [isWalkthroughOpened, popFeature]);
return (
<BackToCanvasLink
id="back-to-canvas"
kind="secondary"
onClick={() => {
history.push(builderURL({ pageId }));
handleCloseWalkthrough();
}}
startIcon="arrow-left-line"
>
{createMessage(BACK_TO_CANVAS)}
</BackToCanvasLink>
);
}
export default BackToCanvas;

View File

@ -21,6 +21,7 @@ import {
createMessage,
} from "@appsmith/constants/messages";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
const ApiNameWrapper = styled.div<{ page?: string }>`
min-width: 50%;
@ -58,6 +59,11 @@ const ApiIconBox = styled.div`
margin-right: 8px;
flex-shrink: 0;
`;
interface SaveActionNameParams {
id: string;
name: string;
}
interface ActionNameEditorProps {
/*
This prop checks if page is API Pane or Query Pane or Curl Pane
@ -67,6 +73,9 @@ interface ActionNameEditorProps {
*/
page?: string;
disabled?: boolean;
saveActionName?: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
}
function ActionNameEditor(props: ActionNameEditorProps) {
@ -84,7 +93,13 @@ function ActionNameEditor(props: ActionNameEditorProps) {
<NameEditorComponent
checkForGuidedTour
currentActionConfig={currentActionConfig}
dispatchAction={saveActionName}
/**
* This component is used by module editor in EE which uses a different
* action to save the name of an action. The current callers of this component
* pass the existing saveAction action but as fallback the saveActionName is used here
* as a guard.
*/
dispatchAction={props.saveActionName || saveActionName}
>
{({
forceUpdate,

View File

@ -2,7 +2,7 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { Collapse, Classes as BPClasses } from "@blueprintjs/core";
import { Classes, getTypographyByKey } from "design-system-old";
import { Divider, Icon, Link, Text } from "design-system";
import { Divider, Icon, Text } from "design-system";
import SuggestedWidgets from "./SuggestedWidgets";
import type { ReactNode, MutableRefObject } from "react";
import { useParams } from "react-router";
@ -11,7 +11,6 @@ import { getWidgets } from "sagas/selectors";
import type { AppState } from "@appsmith/reducers";
import { getDependenciesFromInverseDependencies } from "../Debugger/helpers";
import {
BACK_TO_CANVAS,
BINDINGS_DISABLED_TOOLTIP,
BINDING_SECTION_LABEL,
createMessage,
@ -23,11 +22,7 @@ import type {
SuggestedWidget,
SuggestedWidget as SuggestedWidgetsType,
} from "api/ActionAPI";
import {
getCurrentPageId,
getPagePermissions,
} from "selectors/editorSelectors";
import { builderURL } from "@appsmith/RouteBuilder";
import { getPagePermissions } from "selectors/editorSelectors";
import DatasourceStructureHeader from "pages/Editor/Explorer/Datasources/DatasourceStructureHeader";
import {
DatasourceStructureContainer as DataStructureList,
@ -55,7 +50,6 @@ import { Tooltip } from "design-system";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
import { getIsFirstTimeUserOnboardingEnabled } from "selectors/onboardingSelectors";
import history from "utils/history";
import { SignpostingWalkthroughConfig } from "pages/Editor/FirstTimeUserOnboarding/Utils";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
@ -113,12 +107,6 @@ const SideBar = styled.div`
}
`;
const BackToCanvasLink = styled(Link)`
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
margin-top: ${(props) => props.theme.spaces[11]}px;
margin-bottom: ${(props) => props.theme.spaces[11]}px;
`;
const Label = styled.span`
cursor: pointer;
`;
@ -291,6 +279,7 @@ export function useEntityDependencies(actionName: string) {
function ActionSidebar({
actionName,
actionRightPaneBackLink,
context,
datasourceId,
hasConnections,
@ -305,16 +294,12 @@ function ActionSidebar({
datasourceId: string;
pluginId: string;
context: DatasourceStructureContext;
actionRightPaneBackLink: React.ReactNode;
}) {
const dispatch = useDispatch();
const widgets = useSelector(getWidgets);
const pageId = useSelector(getCurrentPageId);
const user = useSelector(getCurrentUser);
const {
isOpened: isWalkthroughOpened,
popFeature,
pushFeature,
} = useContext(WalkthroughContext) || {};
const { pushFeature } = useContext(WalkthroughContext) || {};
const schemaRef = useRef(null);
const params = useParams<{
pageId: string;
@ -444,26 +429,9 @@ function ActionSidebar({
return <Placeholder>{createMessage(NO_CONNECTIONS)}</Placeholder>;
}
const handleCloseWalkthrough = () => {
if (isWalkthroughOpened && popFeature) {
popFeature();
}
};
return (
<SideBar>
<BackToCanvasLink
id="back-to-canvas"
kind="secondary"
onClick={() => {
history.push(builderURL({ pageId }));
handleCloseWalkthrough();
}}
startIcon="arrow-left-line"
>
{createMessage(BACK_TO_CANVAS)}
</BackToCanvasLink>
{actionRightPaneBackLink}
{showSchema && (
<CollapsibleSection

View File

@ -0,0 +1,65 @@
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import type { PaginationField } from "api/ActionAPI";
import React, { createContext, useMemo } from "react";
interface SaveActionNameParams {
id: string;
name: string;
}
interface ApiEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
handleDeleteClick: () => void;
handleRunClick: (paginationField?: PaginationField) => void;
actionRightPaneBackLink?: React.ReactNode;
settingsConfig: any;
saveActionName?: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
closeEditorLink?: React.ReactNode;
}
type ApiEditorContextProviderProps =
React.PropsWithChildren<ApiEditorContextContextProps>;
export const ApiEditorContext = createContext<ApiEditorContextContextProps>(
{} as ApiEditorContextContextProps,
);
export function ApiEditorContextProvider({
actionRightPaneBackLink,
children,
closeEditorLink,
handleDeleteClick,
handleRunClick,
moreActionsMenu,
saveActionName,
settingsConfig,
}: ApiEditorContextProviderProps) {
const value = useMemo(
() => ({
actionRightPaneBackLink,
closeEditorLink,
handleDeleteClick,
handleRunClick,
moreActionsMenu,
saveActionName,
settingsConfig,
}),
[
actionRightPaneBackLink,
closeEditorLink,
handleDeleteClick,
handleRunClick,
moreActionsMenu,
saveActionName,
settingsConfig,
],
);
return (
<ApiEditorContext.Provider value={value}>
{children}
</ApiEditorContext.Provider>
);
}

View File

@ -310,6 +310,7 @@ function ApiRightPane(props: any) {
<SomeWrapper>
<ActionRightPane
actionName={props.actionName}
actionRightPaneBackLink={props.actionRightPaneBackLink}
context={DatasourceStructureContext.API_EDITOR}
datasourceId={props.datasourceId}
hasConnections={hasDependencies}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
API_EDITOR_TABS,
@ -18,8 +18,6 @@ import type { AppState } from "@appsmith/reducers";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import ActionSettings from "pages/Editor/ActionSettings";
import RequestDropdownField from "components/editorComponents/form/fields/RequestDropdownField";
import type { ExplorerURLParams } from "@appsmith/pages/Editor/Explorer/helpers";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { Classes } from "design-system-old";
import {
@ -39,7 +37,6 @@ import {
API_PANE_DUPLICATE_HEADER,
createMessage,
} from "@appsmith/constants/messages";
import CloseEditor from "components/editorComponents/CloseEditor";
import { useParams } from "react-router";
import DataSourceList from "./ApiRightPane";
import type { Datasource } from "entities/Datasource";
@ -57,10 +54,10 @@ import { DEFAULT_DATASOURCE_NAME } from "constants/ApiEditorConstants/ApiEditorC
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import {
getHasDeleteActionPermission,
getHasExecuteActionPermission,
getHasManageActionPermission,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { ApiEditorContext } from "./ApiEditorContext";
const Form = styled.form`
position: relative;
@ -206,6 +203,7 @@ type CommonFormPropsWithExtraParams = CommonFormProps & {
handleSubmit: any;
// defaultSelectedTabIndex
defaultTabSelected?: number;
closeEditorLink?: React.ReactNode;
};
export const NameWrapper = styled.div`
@ -498,12 +496,15 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
const index = Object.values(API_EDITOR_TABS).indexOf(value);
dispatch(setApiPaneConfigSelectedTabIndex(index));
};
const { actionRightPaneBackLink, moreActionsMenu, saveActionName } =
useContext(ApiEditorContext);
const {
actionConfigurationHeaders,
actionConfigurationParams,
actionName,
autoGeneratedActionConfigHeaders,
closeEditorLink,
currentActionDatasourceId,
formName,
handleSubmit,
@ -532,7 +533,6 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
const currentActionConfig: Action | undefined = actions.find(
(action) => action.id === params.apiId || action.id === params.queryId,
);
const { pageId } = useParams<ExplorerURLParams>();
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
@ -542,10 +542,6 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const plugin = useSelector((state: AppState) =>
getPlugin(state, pluginId ?? ""),
@ -577,22 +573,19 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
return (
<MainContainer>
<CloseEditor />
{closeEditorLink}
<Form onSubmit={handleSubmit(noop)}>
<MainConfiguration>
<FormRow className="form-row-header">
<NameWrapper className="t--nameOfApi">
<ActionNameEditor disabled={!isChangePermitted} page="API_PANE" />
<ActionNameEditor
disabled={!isChangePermitted}
page="API_PANE"
saveActionName={saveActionName}
/>
</NameWrapper>
<ActionButtons className="t--formActionButtons">
<MoreActionsMenu
className="t--more-action-menu"
id={currentActionConfig ? currentActionConfig.id : ""}
isChangePermitted={isChangePermitted}
isDeletePermitted={isDeletePermitted}
name={currentActionConfig ? currentActionConfig.name : ""}
pageId={pageId}
/>
{moreActionsMenu}
<Button
className="t--apiFormRunBtn"
isDisabled={blockExecution}
@ -742,6 +735,7 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
</SecondaryWrapper>
<DataSourceList
actionName={actionName}
actionRightPaneBackLink={actionRightPaneBackLink}
applicationId={props.applicationId}
currentActionDatasourceId={currentActionDatasourceId}
currentPageId={props.currentPageId}

View File

@ -0,0 +1,41 @@
import React, { useMemo } from "react";
import CurlImportForm from "./CurlImportForm";
import { createNewApiName } from "utils/AppsmithUtils";
import { curlImportSubmitHandler } from "./helpers";
import { getActions } from "@appsmith/selectors/entitiesSelector";
import { getIsImportingCurl } from "selectors/ui";
import { showDebuggerFlag } from "selectors/debuggerSelectors";
import { useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import type { BuilderRouteParams } from "constants/routes";
import CloseEditor from "components/editorComponents/CloseEditor";
type CurlImportEditorProps = RouteComponentProps<BuilderRouteParams>;
function CurlImportEditor(props: CurlImportEditorProps) {
const actions = useSelector(getActions);
const { pageId } = props.match.params;
const showDebugger = useSelector(showDebuggerFlag);
const isImportingCurl = useSelector(getIsImportingCurl);
const initialFormValues = {
pageId,
name: createNewApiName(actions, pageId),
};
const closeEditorLink = useMemo(() => <CloseEditor />, []);
return (
<CurlImportForm
closeEditorLink={closeEditorLink}
curlImportSubmitHandler={curlImportSubmitHandler}
initialValues={initialFormValues}
isImportingCurl={isImportingCurl}
showDebugger={showDebugger}
/>
);
}
export default CurlImportEditor;

View File

@ -1,26 +1,17 @@
import React from "react";
import type { InjectedFormProps } from "redux-form";
import { reduxForm, Form, Field } from "redux-form";
import { connect } from "react-redux";
import type { RouteComponentProps } from "react-router";
import { withRouter } from "react-router";
import styled from "styled-components";
import type { AppState } from "@appsmith/reducers";
import type { ActionDataState } from "@appsmith/reducers/entityReducers/actionsReducer";
import { CURL_IMPORT_FORM } from "@appsmith/constants/forms";
import type { BuilderRouteParams } from "constants/routes";
import type { curlImportFormValues } from "./helpers";
import { curlImportSubmitHandler } from "./helpers";
import { createNewApiName } from "utils/AppsmithUtils";
import CurlLogo from "assets/images/Curl-logo.svg";
import CloseEditor from "components/editorComponents/CloseEditor";
import { Button } from "design-system";
import FormRow from "components/editorComponents/FormRow";
import Debugger, {
ResizerContentContainer,
ResizerMainContainer,
} from "../DataSourceEditor/Debugger";
import { showDebuggerFlag } from "selectors/debuggerSelectors";
const MainConfiguration = styled.div`
padding: var(--ads-v2-spaces-4) var(--ads-v2-spaces-7);
@ -99,25 +90,28 @@ const MainContainer = styled.div`
padding: 0px var(--ads-v2-spaces-7);
}
`;
interface ReduxStateProps {
actions: ActionDataState;
initialValues: Record<string, unknown>;
interface OwnProps {
isImportingCurl: boolean;
showDebugger: boolean;
curlImportSubmitHandler: (
values: curlImportFormValues,
dispatch: any,
) => void;
initialValues: Record<string, unknown>;
closeEditorLink?: React.ReactNode;
}
export type StateAndRouteProps = ReduxStateProps &
RouteComponentProps<BuilderRouteParams>;
type Props = StateAndRouteProps &
InjectedFormProps<curlImportFormValues, StateAndRouteProps>;
type Props = OwnProps & InjectedFormProps<curlImportFormValues, OwnProps>;
class CurlImportForm extends React.Component<Props> {
render() {
const { handleSubmit, isImportingCurl, showDebugger } = this.props;
const { closeEditorLink, handleSubmit, isImportingCurl, showDebugger } =
this.props;
return (
<MainContainer>
<CloseEditor />
{closeEditorLink}
<MainConfiguration>
<FormRow className="form-row-header">
<div
@ -171,35 +165,6 @@ class CurlImportForm extends React.Component<Props> {
}
}
const mapStateToProps = (state: AppState, props: Props): ReduxStateProps => {
const { pageId: destinationPageId } = props.match.params;
// Debugger render flag
const showDebugger = showDebuggerFlag(state);
if (destinationPageId) {
return {
actions: state.entities.actions,
initialValues: {
pageId: destinationPageId,
name: createNewApiName(state.entities.actions, destinationPageId),
},
isImportingCurl: state.ui.imports.isImportingCurl,
showDebugger,
};
}
return {
actions: state.entities.actions,
initialValues: {},
isImportingCurl: state.ui.imports.isImportingCurl,
showDebugger,
};
};
export default withRouter(
connect(mapStateToProps)(
reduxForm<curlImportFormValues, StateAndRouteProps>({
form: CURL_IMPORT_FORM,
})(CurlImportForm),
),
);
export default reduxForm<curlImportFormValues, OwnProps>({
form: CURL_IMPORT_FORM,
})(CurlImportForm);

View File

@ -0,0 +1,276 @@
import React from "react";
import { connect } from "react-redux";
import { submit } from "redux-form";
import RestApiEditorForm from "./RestAPIForm";
import RapidApiEditorForm from "./RapidApiEditorForm";
import type { AppState } from "@appsmith/reducers";
import type { RouteComponentProps } from "react-router";
import type {
ActionData,
ActionDataState,
} from "@appsmith/reducers/entityReducers/actionsReducer";
import _ from "lodash";
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
import {
getActionById,
getCurrentApplicationId,
getCurrentPageName,
} from "selectors/editorSelectors";
import type { Plugin } from "api/PluginApi";
import type { Action, PaginationType, RapidApiAction } from "entities/Action";
import { PluginPackageName } from "entities/Action";
import { getApiName } from "selectors/formSelectors";
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 } from "actions/apiPaneActions";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import * as Sentry from "@sentry/react";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
import { getPageList, getPlugins } from "@appsmith/selectors/entitiesSelector";
import history from "utils/history";
import { saasEditorApiIdURL } from "@appsmith/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;
apiName: string;
currentApplication?: ApplicationPayload;
currentPageName: string | undefined;
pages: any;
plugins: Plugin[];
pluginId: any;
apiAction: Action | ActionData | RapidApiAction | 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() {
PerformanceTracker.stopTracking(PerformanceTransactionName.OPEN_ACTION, {
actionType: "API",
});
const type = this.getFormName();
if (this.props.match.params.apiId) {
this.props.changeAPIPage(this.props.match.params.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.isRunning && !this.props.isRunning) {
PerformanceTracker.stopTracking(PerformanceTransactionName.RUN_API_CLICK);
}
if (prevProps.match.params.apiId !== this.props.match.params.apiId) {
const type = this.getFormName();
this.props.changeAPIPage(
this.props.match.params.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: { apiId },
},
paginationType,
pluginId,
plugins,
} = this.props;
if (!pluginId && apiId) {
return <EntityNotFoundPane />;
}
if (isCreating || !isEditorInitialized) {
return (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
);
}
let formUiComponent: string | undefined;
if (apiId) {
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}
onDeleteClick={this.context.handleDeleteClick}
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}
onDeleteClick={this.context.handleDeleteClick}
onRunClick={this.context.handleRunClick}
paginationType={paginationType}
pluginId={pluginId}
settingsConfig={this.context.settingsConfig}
/>
)}
{formUiComponent === "RapidApiEditorForm" && (
<RapidApiEditorForm
apiId={this.props.match.params.apiId || ""}
apiName={this.props.apiName}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
isDeleting={isDeleting}
isRunning={isRunning}
location={this.props.location}
onDeleteClick={this.context.handleDeleteClick}
onRunClick={this.context.handleRunClick}
paginationType={paginationType}
/>
)}
{formUiComponent === "SaaSEditorForm" &&
history.push(
saasEditorApiIdURL({
pageId: this.props.match.params.pageId,
pluginPackageName:
getPackageNameFromPluginId(
this.props.pluginId,
this.props.plugins,
) ?? "",
apiId: this.props.match.params.apiId || "",
}),
)}
</div>
);
}
}
const formStyles: CSSProperties = {
position: "relative",
height: "100%",
display: "flex",
flexDirection: "column",
};
const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const apiAction = getActionById(state, props);
const apiName = getApiName(state, props.match.params.apiId);
const { isCreating, isDeleting, isRunning } = state.ui.apiPane;
const pluginId = _.get(apiAction, "pluginId", "");
return {
actions: state.entities.actions,
currentApplication: getCurrentApplication(state),
currentPageName: getCurrentPageName(state),
pages: getPageList(state),
apiName: apiName || "",
plugins: getPlugins(state),
pluginId,
paginationType: _.get(apiAction, "actionConfiguration.paginationType"),
apiAction,
isRunning: isRunning[props.match.params.apiId],
isDeleting: isDeleting[props.match.params.apiId],
isCreating: isCreating,
applicationId: getCurrentApplicationId(state),
};
};
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,4 +1,4 @@
import React, { useCallback, useRef } from "react";
import React, { useCallback, useContext, useRef } from "react";
import { connect } from "react-redux";
import type { InjectedFormProps } from "redux-form";
import { change, formValueSelector, reduxForm } from "redux-form";
@ -21,6 +21,7 @@ import QueryEditor from "./QueryEditor";
import { tailwindLayers } from "constants/Layers";
import VariableEditor from "./VariableEditor";
import Pagination from "./Pagination";
import { ApiEditorContext } from "../ApiEditorContext";
const ResizeableDiv = styled.div`
display: flex;
@ -81,6 +82,8 @@ function GraphQLEditorForm(props: Props) {
DEFAULT_GRAPHQL_VARIABLE_WIDTH,
);
const { closeEditorLink } = useContext(ApiEditorContext);
/**
* Variable Editor's resizeable handler for the changing of width
*/
@ -131,6 +134,7 @@ function GraphQLEditorForm(props: Props) {
</ResizeableDiv>
</BodyWrapper>
}
closeEditorLink={closeEditorLink}
defaultTabSelected={2}
formName={API_EDITOR_FORM_NAME}
paginationUIComponent={

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useContext } from "react";
import { connect } from "react-redux";
import type { InjectedFormProps } from "redux-form";
import { reduxForm, formValueSelector } from "redux-form";
@ -22,6 +22,7 @@ import { getActionData } from "@appsmith/selectors/entitiesSelector";
import type { AppState } from "@appsmith/reducers";
import { Icon } from "design-system";
import { showDebuggerFlag } from "selectors/debuggerSelectors";
import { ApiEditorContext } from "./ApiEditorContext";
const Form = styled.form`
display: flex;
@ -142,6 +143,8 @@ function RapidApiEditorForm(props: Props) {
templateId,
} = props;
const { saveActionName } = useContext(ApiEditorContext);
const postbodyResponsePresent =
templateId &&
actionConfiguration &&
@ -158,7 +161,7 @@ function RapidApiEditorForm(props: Props) {
<MainConfiguration>
<FormRow>
<NameWrapper>
<ActionNameEditor />
<ActionNameEditor saveActionName={saveActionName} />
<a
className="t--apiDocumentationLink"
href={providerURL && `http://${providerURL}`}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useContext } from "react";
import { connect } from "react-redux";
import type { InjectedFormProps } from "redux-form";
import { change, formValueSelector, reduxForm } from "redux-form";
@ -24,6 +24,7 @@ import type { CommonFormProps } from "./CommonEditorForm";
import CommonEditorForm from "./CommonEditorForm";
import Pagination from "./Pagination";
import { getCurrentEnvironmentId } from "@appsmith/selectors/environmentSelectors";
import { ApiEditorContext } from "./ApiEditorContext";
const NoBodyMessage = styled.div`
margin-top: 20px;
@ -42,6 +43,7 @@ type APIFormProps = {
type Props = APIFormProps & InjectedFormProps<Action, APIFormProps>;
function ApiEditorForm(props: Props) {
const { closeEditorLink } = useContext(ApiEditorContext);
const { actionName, httpMethodFromForm } = props;
const allowPostBody = httpMethodFromForm;
const theme = EditorTheme.LIGHT;
@ -58,6 +60,7 @@ function ApiEditorForm(props: Props) {
</NoBodyMessage>
)
}
closeEditorLink={closeEditorLink}
formName={API_EDITOR_FORM_NAME}
paginationUIComponent={
<Pagination

View File

@ -1,318 +1,129 @@
import React from "react";
import { connect } from "react-redux";
import { submit } from "redux-form";
import RestApiEditorForm from "./RestAPIForm";
import RapidApiEditorForm from "./RapidApiEditorForm";
import { deleteAction, runAction } from "actions/pluginActionActions";
import type { PaginationField } from "api/ActionAPI";
import type { AppState } from "@appsmith/reducers";
import React, { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import type {
ActionData,
ActionDataState,
} from "@appsmith/reducers/entityReducers/actionsReducer";
import _ from "lodash";
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import {
getActionById,
getCurrentApplicationId,
getCurrentPageName,
getIsEditorInitialized,
} from "selectors/editorSelectors";
import type { Plugin } from "api/PluginApi";
import type { Action, PaginationType, RapidApiAction } from "entities/Action";
import { PluginPackageName } from "entities/Action";
import { getApiName } from "selectors/formSelectors";
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 } from "actions/apiPaneActions";
getPageList,
getPluginSettingConfigs,
getPlugins,
} from "@appsmith/selectors/entitiesSelector";
import { deleteAction, runAction } from "actions/pluginActionActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import Editor from "./Editor";
import BackToCanvas from "components/common/BackToCanvas";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import { getIsEditorInitialized } from "selectors/editorSelectors";
import { getAction } from "@appsmith/selectors/entitiesSelector";
import type { APIEditorRouteParams } from "constants/routes";
import {
getHasDeleteActionPermission,
getHasManageActionPermission,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { ApiEditorContextProvider } from "./ApiEditorContext";
import type { PaginationField } from "api/ActionAPI";
import { get } from "lodash";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import * as Sentry from "@sentry/react";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
import {
getPageList,
getPlugins,
getPluginSettingConfigs,
} from "@appsmith/selectors/entitiesSelector";
import history from "utils/history";
import { saasEditorApiIdURL } from "@appsmith/RouteBuilder";
import GraphQLEditorForm from "./GraphQL/GraphQLEditorForm";
import CloseEditor from "components/editorComponents/CloseEditor";
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;
interface ReduxStateProps {
actions: ActionDataState;
isRunning: boolean;
isDeleting: boolean;
isCreating: boolean;
apiName: string;
currentApplication?: ApplicationPayload;
currentPageName: string | undefined;
pages: any;
plugins: Plugin[];
pluginId: any;
settingsConfig: any;
apiAction: Action | ActionData | RapidApiAction | undefined;
paginationType: PaginationType;
isEditorInitialized: boolean;
applicationId: string;
}
interface ReduxActionProps {
submitForm: (name: string) => void;
runAction: (id: string, paginationField?: PaginationField) => void;
deleteAction: (id: string, name: string) => void;
changeAPIPage: (apiId: string, isSaas: boolean) => void;
}
type ApiEditorWrapperProps = RouteComponentProps<APIEditorRouteParams>;
function getPageName(pages: any, pageId: string) {
const page = pages.find((page: any) => page.pageId === pageId);
return page ? page.pageName : "";
}
function getPackageNameFromPluginId(pluginId: string, plugins: Plugin[]) {
const plugin = plugins.find((plugin: Plugin) => plugin.id === pluginId);
return plugin?.packageName;
}
function ApiEditorWrapper(props: ApiEditorWrapperProps) {
const { apiId = "", pageId } = props.match.params;
const dispatch = useDispatch();
const isEditorInitialized = useSelector(getIsEditorInitialized);
const action = useSelector((state) => getAction(state, apiId));
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, pageId);
const settingsConfig = useSelector((state) =>
getPluginSettingConfigs(state, pluginId),
);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
type Props = ReduxActionProps &
ReduxStateProps &
RouteComponentProps<{ apiId: string; pageId: string }>;
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
class ApiEditor extends React.Component<Props> {
componentDidMount() {
PerformanceTracker.stopTracking(PerformanceTransactionName.OPEN_ACTION, {
actionType: "API",
});
const type = this.getFormName();
this.props.changeAPIPage(this.props.match.params.apiId, type === "SAAS");
}
handleDeleteClick = () => {
const pageName = getPageName(
this.props.pages,
this.props.match.params.pageId,
);
AnalyticsUtil.logEvent("DELETE_API_CLICK", {
apiName: this.props.apiName,
apiID: this.props.match.params.apiId,
pageName: pageName,
});
this.props.deleteAction(this.props.match.params.apiId, this.props.apiName);
};
const moreActionsMenu = useMemo(
() => (
<MoreActionsMenu
className="t--more-action-menu"
id={action ? action.id : ""}
isChangePermitted={isChangePermitted}
isDeletePermitted={isDeletePermitted}
name={action ? action.name : ""}
pageId={pageId}
/>
),
[action?.id, action?.name, isChangePermitted, isDeletePermitted, pageId],
);
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.isRunning && !this.props.isRunning) {
PerformanceTracker.stopTracking(PerformanceTransactionName.RUN_API_CLICK);
}
if (prevProps.match.params.apiId !== this.props.match.params.apiId) {
const type = this.getFormName();
this.props.changeAPIPage(this.props.match.params.apiId, type === "SAAS");
}
}
handleRunClick = (paginationField?: PaginationField) => {
const pageName = getPageName(
this.props.pages,
this.props.match.params.pageId,
);
const pluginName = this.props.plugins.find(
(plugin) => plugin.id === this.props.pluginId,
)?.name;
PerformanceTracker.startTracking(PerformanceTransactionName.RUN_API_CLICK, {
apiId: this.props.match.params.apiId,
});
AnalyticsUtil.logEvent("RUN_API_CLICK", {
apiName: this.props.apiName,
apiID: this.props.match.params.apiId,
pageName: pageName,
datasourceId: (this.props?.apiAction as any)?.datasource?.id,
pluginName: pluginName,
isMock: false, // as mock db exists only for postgres and mongo plugins
});
this.props.runAction(this.props.match.params.apiId, paginationField);
};
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: { apiId },
},
paginationType,
pluginId,
plugins,
} = this.props;
if (!pluginId && apiId) {
return <EntityNotFoundPane />;
}
if (isCreating || !isEditorInitialized) {
return (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
const handleRunClick = useCallback(
(paginationField?: PaginationField) => {
const pluginName = plugins.find((plugin) => plugin.id === pluginId)?.name;
PerformanceTracker.startTracking(
PerformanceTransactionName.RUN_API_CLICK,
{
apiId,
},
);
}
AnalyticsUtil.logEvent("RUN_API_CLICK", {
apiName,
apiID: apiId,
pageName: pageName,
datasourceId,
pluginName: pluginName,
isMock: false, // as mock db exists only for postgres and mongo plugins
});
dispatch(runAction(apiId, paginationField));
},
[apiId, apiName, pageName, getPageName, plugins, pluginId, datasourceId],
);
let formUiComponent: string | undefined;
if (apiId) {
if (pluginId) {
formUiComponent = this.getPluginUiComponentOfId(pluginId, plugins);
} else {
formUiComponent = this.getPluginUiComponentOfName(plugins);
}
}
const actionRightPaneBackLink = useMemo(() => {
return <BackToCanvas pageId={pageId} />;
}, [pageId]);
return (
<div style={formStyles}>
{formUiComponent === "ApiEditorForm" && (
<RestApiEditorForm
apiName={this.props.apiName}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
isDeleting={isDeleting}
isRunning={isRunning}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
paginationType={paginationType}
pluginId={pluginId}
settingsConfig={this.props.settingsConfig}
/>
)}
{formUiComponent === "GraphQLEditorForm" && (
<GraphQLEditorForm
apiName={this.props.apiName}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
isDeleting={isDeleting}
isRunning={isRunning}
match={this.props.match}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
paginationType={paginationType}
pluginId={pluginId}
settingsConfig={this.props.settingsConfig}
/>
)}
{formUiComponent === "RapidApiEditorForm" && (
<RapidApiEditorForm
apiId={this.props.match.params.apiId}
apiName={this.props.apiName}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
isDeleting={isDeleting}
isRunning={isRunning}
location={this.props.location}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
paginationType={paginationType}
/>
)}
{formUiComponent === "SaaSEditorForm" &&
history.push(
saasEditorApiIdURL({
pageId: this.props.match.params.pageId,
pluginPackageName:
getPackageNameFromPluginId(
this.props.pluginId,
this.props.plugins,
) ?? "",
apiId: this.props.match.params.apiId,
}),
)}
</div>
);
}
const handleDeleteClick = useCallback(() => {
AnalyticsUtil.logEvent("DELETE_API_CLICK", {
apiName,
apiID: apiId,
pageName,
});
dispatch(deleteAction({ id: apiId, name: apiName }));
}, [getPageName, pages, pageId, apiName]);
const closeEditorLink = useMemo(() => <CloseEditor />, []);
return (
<ApiEditorContextProvider
actionRightPaneBackLink={actionRightPaneBackLink}
closeEditorLink={closeEditorLink}
handleDeleteClick={handleDeleteClick}
handleRunClick={handleRunClick}
moreActionsMenu={moreActionsMenu}
settingsConfig={settingsConfig}
>
<Editor {...props} isEditorInitialized={isEditorInitialized} />
</ApiEditorContextProvider>
);
}
const formStyles: CSSProperties = {
position: "relative",
height: "100%",
display: "flex",
flexDirection: "column",
};
const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const apiAction = getActionById(state, props);
const apiName = getApiName(state, props.match.params.apiId);
const { isCreating, isDeleting, isRunning } = state.ui.apiPane;
const pluginId = _.get(apiAction, "pluginId", "");
const settingsConfig = getPluginSettingConfigs(state, pluginId);
return {
actions: state.entities.actions,
currentApplication: getCurrentApplication(state),
currentPageName: getCurrentPageName(state),
pages: getPageList(state),
apiName: apiName || "",
plugins: getPlugins(state),
pluginId,
settingsConfig,
paginationType: _.get(apiAction, "actionConfiguration.paginationType"),
apiAction,
isRunning: isRunning[props.match.params.apiId],
isDeleting: isDeleting[props.match.params.apiId],
isCreating: isCreating,
isEditorInitialized: getIsEditorInitialized(state),
applicationId: getCurrentApplicationId(state),
};
};
const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({
submitForm: (name: string) => dispatch(submit(name)),
runAction: (id: string, paginationField?: PaginationField) =>
dispatch(runAction(id, paginationField)),
deleteAction: (id: string, name: string) =>
dispatch(deleteAction({ id, name })),
changeAPIPage: (actionId: string, isSaas: boolean) =>
dispatch(changeApi(actionId, isSaas)),
});
export default Sentry.withProfiler(
connect(mapStateToProps, mapDispatchToProps)(ApiEditor),
);
export default ApiEditorWrapper;

View File

@ -0,0 +1,352 @@
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 "@appsmith/reducers";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { QUERY_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
import type { Plugin } from "api/PluginApi";
import { UIComponentTypes } from "api/PluginApi";
import type { Datasource } from "entities/Datasource";
import {
getPluginIdsOfPackageNames,
getPlugins,
getAction,
getActionResponses,
getDatasourceByPluginId,
getDBAndRemoteDatasources,
} from "@appsmith/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 PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { initFormEvaluations } from "@appsmith/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 "@appsmith/selectors/environmentSelectors";
import { QueryEditorContext } from "./QueryEditorContext";
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: (
editorConfig: 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;
responses: any;
isCreating: boolean;
editorConfig: any;
uiComponent: UIComponentTypes;
applicationId: string;
actionId: string;
actionObjectDiff?: any;
isSaas: boolean;
datasourceId?: string;
currentEnvironmentId: string;
currentEnvironmentName: string;
}
type StateAndRouteProps = RouteComponentProps<QueryEditorRouteParams>;
type OwnProps = StateAndRouteProps & {
isEditorInitialized: boolean;
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.queryId) {
this.props.initFormEvaluation(
this.props.editorConfig,
this.props.settingsConfig,
this.props.match.params.queryId,
);
}
}
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.actionId);
// 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);
}
}
PerformanceTracker.stopTracking(PerformanceTransactionName.OPEN_ACTION, {
actionType: "QUERY",
});
}
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;
PerformanceTracker.startTracking(
PerformanceTransactionName.RUN_QUERY_CLICK,
{ actionId: this.props.actionId },
);
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) {
if (prevProps.isRunning === true && this.props.isRunning === false) {
PerformanceTracker.stopTracking(
PerformanceTransactionName.RUN_QUERY_CLICK,
);
}
// 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.actionId !== this.props.actionId ||
prevProps.pluginId !== this.props.pluginId
) {
this.context?.changeQueryPage?.(this.props.actionId);
}
}
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
dataSources={dataSources}
datasourceId={this.props.datasourceId}
editorConfig={editorConfig}
executedQueryData={responses[actionId]}
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 { apiId, queryId } = props.match.params;
const actionId = queryId || apiId || "";
const { runErrorMessage } = state.ui.queryPane;
const { plugins } = state.entities;
const { editorConfigs } = plugins;
const action = getAction(state, actionId) as QueryAction | SaaSAction;
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as
| QueryAction
| SaaSAction;
let pluginId;
if (action) {
pluginId = action.pluginId;
}
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,
currentEnvironmentId: currentEnvDetails?.id || "",
currentEnvironmentName: currentEnvDetails?.name || "",
pluginId,
plugins: allPlugins,
runErrorMessage,
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
dataSources: !!apiId
? getDatasourceByPluginId(state, action?.pluginId)
: getDBAndRemoteDatasources(state),
responses: getActionResponses(state),
isRunning: state.ui.queryPane.isRunning[actionId],
isDeleting: state.ui.queryPane.isDeleting[actionId],
isSaas: !!apiId,
formData,
editorConfig,
isCreating: state.ui.apiPane.isCreating,
uiComponent,
applicationId: getCurrentApplicationId(state),
actionObjectDiff,
datasourceId: action?.datasource?.id,
};
};
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
deleteAction: (id: string, name: string) =>
dispatch(deleteAction({ id, name })),
runAction: (actionId: string) => dispatch(runAction(actionId)),
initFormEvaluation: (
editorConfig: 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,3 +1,4 @@
import { useContext } from "react";
import type { RefObject } from "react";
import React, { useCallback, useRef, useState } from "react";
import type { InjectedFormProps } from "redux-form";
@ -42,7 +43,6 @@ import Resizable, {
ResizerCSS,
} from "components/editorComponents/Debugger/Resizer";
import AnalyticsUtil from "utils/AnalyticsUtil";
import CloseEditor from "components/editorComponents/CloseEditor";
import EntityDeps from "components/editorComponents/Debugger/EntityDependecies";
import {
checkIfSectionCanRender,
@ -67,8 +67,6 @@ import {
} from "@appsmith/constants/messages";
import { useParams } from "react-router";
import type { AppState } from "@appsmith/reducers";
import type { ExplorerURLParams } from "@appsmith/pages/Editor/Explorer/helpers";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import { thinScrollbar } from "constants/DefaultTheme";
import ActionRightPane, {
useEntityDependencies,
@ -132,12 +130,12 @@ import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceSt
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
import {
getHasCreateDatasourcePermission,
getHasDeleteActionPermission,
getHasExecuteActionPermission,
getHasManageActionPermission,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { QueryEditorContext } from "./QueryEditorContext";
const QueryFormContainer = styled.form`
flex: 1;
@ -368,6 +366,7 @@ interface QueryFormProps {
value,
}: UpdateActionPropertyActionPayload) => void;
datasourceId: string;
showCloseEditor: boolean;
}
interface ReduxProps {
@ -404,6 +403,13 @@ export function EditorJSONtoForm(props: Props) {
updateActionResponseDisplayFormat,
} = props;
const {
actionRightPaneBackLink,
closeEditorLink,
moreActionsMenu,
saveActionName,
} = useContext(QueryEditorContext);
let error = runErrorMessage;
let output: Record<string, any>[] | null = null;
let hintMessages: Array<string> = [];
@ -421,8 +427,6 @@ export function EditorJSONtoForm(props: Props) {
const currentActionConfig: Action | undefined = actions.find(
(action) => action.id === params.apiId || action.id === params.queryId,
);
const { pageId } = useParams<ExplorerURLParams>();
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isChangePermitted = getHasManageActionPermission(
@ -433,10 +437,6 @@ export function EditorJSONtoForm(props: Props) {
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
currentActionConfig?.userPermissions,
);
const userWorkspacePermissions = useSelector(
(state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [],
@ -913,22 +913,18 @@ export function EditorJSONtoForm(props: Props) {
return (
<>
{!guidedTourEnabled && <CloseEditor />}
{!guidedTourEnabled && closeEditorLink}
{guidedTourEnabled && <Guide className="query-page" />}
<QueryFormContainer onSubmit={handleSubmit(noop)}>
<StyledFormRow>
<NameWrapper>
<ActionNameEditor disabled={!isChangePermitted} />
<ActionNameEditor
disabled={!isChangePermitted}
saveActionName={saveActionName}
/>
</NameWrapper>
<ActionsWrapper>
<MoreActionsMenu
className="t--more-action-menu"
id={currentActionConfig ? currentActionConfig.id : ""}
isChangePermitted={isChangePermitted}
isDeletePermitted={isDeletePermitted}
name={currentActionConfig ? currentActionConfig.name : ""}
pageId={pageId}
/>
{moreActionsMenu}
<DropdownSelect>
<DropdownField
className={"t--switch-datasource"}
@ -1113,6 +1109,7 @@ export function EditorJSONtoForm(props: Props) {
<SidebarWrapper show={shouldOpenActionPaneByDefault}>
<ActionRightPane
actionName={actionName}
actionRightPaneBackLink={actionRightPaneBackLink}
context={DatasourceStructureContext.QUERY_EDITOR}
datasourceId={props.datasourceId}
hasConnections={hasDependencies}

View File

@ -0,0 +1,64 @@
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import React, { createContext, useMemo } from "react";
interface SaveActionNameParams {
id: string;
name: string;
}
interface QueryEditorContextContextProps {
moreActionsMenu?: React.ReactNode;
onCreateDatasourceClick?: () => void;
onEntityNotFoundBackClick?: () => void;
changeQueryPage?: (queryId: string) => void;
actionRightPaneBackLink?: React.ReactNode;
saveActionName?: (
params: SaveActionNameParams,
) => ReduxAction<SaveActionNameParams>;
closeEditorLink?: React.ReactNode;
}
type QueryEditorContextProviderProps =
React.PropsWithChildren<QueryEditorContextContextProps>;
export const QueryEditorContext = createContext<QueryEditorContextContextProps>(
{} as QueryEditorContextContextProps,
);
export function QueryEditorContextProvider({
actionRightPaneBackLink,
changeQueryPage,
children,
closeEditorLink,
moreActionsMenu,
onCreateDatasourceClick,
onEntityNotFoundBackClick,
saveActionName,
}: QueryEditorContextProviderProps) {
const value = useMemo(
() => ({
actionRightPaneBackLink,
changeQueryPage,
closeEditorLink,
moreActionsMenu,
onCreateDatasourceClick,
onEntityNotFoundBackClick,
saveActionName,
}),
[
actionRightPaneBackLink,
changeQueryPage,
closeEditorLink,
moreActionsMenu,
onCreateDatasourceClick,
onEntityNotFoundBackClick,
saveActionName,
],
);
return (
<QueryEditorContext.Provider value={value}>
{children}
</QueryEditorContext.Provider>
);
}

View File

@ -1,199 +1,87 @@
import React from "react";
import React, { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
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 { INTEGRATION_TABS } from "constants/routes";
import AnalyticsUtil from "utils/AnalyticsUtil";
import Editor from "./Editor";
import history from "utils/history";
import QueryEditorForm from "./Form";
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions";
import {
deleteAction,
runAction,
setActionResponseDisplayFormat,
setActionProperty,
} from "actions/pluginActionActions";
import type { AppState } from "@appsmith/reducers";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import BackToCanvas from "components/common/BackToCanvas";
import { INTEGRATION_TABS } from "constants/routes";
import {
getCurrentApplicationId,
getCurrentPageId,
getIsEditorInitialized,
} from "selectors/editorSelectors";
import { QUERY_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
import type { Plugin } from "api/PluginApi";
import { UIComponentTypes } from "api/PluginApi";
import type { Datasource } from "entities/Datasource";
import {
getPluginIdsOfPackageNames,
getPlugins,
getAction,
getActionResponses,
getDatasourceByPluginId,
getDBAndRemoteDatasources,
} from "@appsmith/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 { changeQuery } from "actions/queryPaneActions";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { initFormEvaluations } from "@appsmith/actions/evaluationActions";
import { getUIComponent } from "./helpers";
import type { Diff } from "deep-diff";
import { diff } from "deep-diff";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import { integrationEditorURL } from "@appsmith/RouteBuilder";
import { getConfigInitialValues } from "components/formControls/utils";
import { merge } from "lodash";
import { getPathAndValueFromActionDiffObject } from "../../../utils/getPathAndValueFromActionDiffObject";
import { DatasourceCreateEntryPoints } from "constants/Datasource";
import { getCurrentEnvironmentDetails } from "@appsmith/selectors/environmentSelectors";
import {
getAction,
getPluginSettingConfigs,
} from "@appsmith/selectors/entitiesSelector";
import { integrationEditorURL } from "@appsmith/RouteBuilder";
import { QueryEditorContextProvider } from "./QueryEditorContext";
import type { QueryEditorRouteParams } from "constants/routes";
import {
getHasDeleteActionPermission,
getHasManageActionPermission,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import CloseEditor from "components/editorComponents/CloseEditor";
const EmptyStateContainer = styled.div`
display: flex;
height: 100%;
font-size: 20px;
`;
type QueryEditorProps = RouteComponentProps<QueryEditorRouteParams>;
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;
function QueryEditor(props: QueryEditorProps) {
const { apiId, queryId } = props.match.params;
const actionId = queryId || apiId;
const dispatch = useDispatch();
const action = useSelector((state) => getAction(state, actionId || ""));
const pluginId = action?.pluginId || "";
const isEditorInitialized = useSelector(getIsEditorInitialized);
const applicationId: string = useSelector(getCurrentApplicationId);
const pageId: string = useSelector(getCurrentPageId);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const settingsConfig = useSelector((state) =>
getPluginSettingConfigs(state, pluginId),
);
interface ReduxDispatchProps {
runAction: (actionId: string) => void;
deleteAction: (id: string, name: string) => void;
changeQueryPage: (queryId: string) => void;
initFormEvaluation: (
editorConfig: any,
settingConfig: any,
formId: string,
) => void;
updateActionResponseDisplayFormat: ({
field,
id,
value,
}: UpdateActionPropertyActionPayload) => void;
setActionProperty: (
actionId: string,
propertyName: string,
value: string,
) => void;
}
const isDeletePermitted = getHasDeleteActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
interface ReduxStateProps {
plugins: Plugin[];
dataSources: Datasource[];
isRunning: boolean;
isDeleting: boolean;
formData: QueryAction | SaaSAction;
runErrorMessage: Record<string, string>;
pluginId: string | undefined;
pluginIds: Array<string> | undefined;
responses: any;
isCreating: boolean;
editorConfig: any;
settingConfig: any;
isEditorInitialized: boolean;
uiComponent: UIComponentTypes;
applicationId: string;
actionId: string;
actionObjectDiff?: any;
isSaas: boolean;
datasourceId?: string;
currentEnvironmentId: string;
currentEnvironmentName: string;
}
const isChangePermitted = getHasManageActionPermission(
isFeatureEnabled,
action?.userPermissions,
);
type StateAndRouteProps = RouteComponentProps<QueryEditorRouteParams>;
const moreActionsMenu = useMemo(
() => (
<MoreActionsMenu
className="t--more-action-menu"
id={action ? action.id : ""}
isChangePermitted={isChangePermitted}
isDeletePermitted={isDeletePermitted}
name={action ? action.name : ""}
pageId={pageId}
/>
),
[action?.id, action?.name, isChangePermitted, isDeletePermitted, pageId],
);
type Props = StateAndRouteProps & ReduxDispatchProps & ReduxStateProps;
const actionRightPaneBackLink = useMemo(() => {
return <BackToCanvas pageId={pageId} />;
}, [pageId]);
class QueryEditor extends React.Component<Props> {
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.queryId) {
this.props.initFormEvaluation(
this.props.editorConfig,
this.props.settingConfig,
this.props.match.params.queryId,
);
}
}
const changeQueryPage = useCallback(
(queryId: string) => {
dispatch(changeQuery({ id: queryId, pageId, applicationId }));
},
[pageId, applicationId],
);
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.props.changeQueryPage(this.props.actionId);
// 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);
}
}
PerformanceTracker.stopTracking(PerformanceTransactionName.OPEN_ACTION, {
actionType: "QUERY",
});
}
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;
PerformanceTracker.startTracking(
PerformanceTransactionName.RUN_QUERY_CLICK,
{ actionId: this.props.actionId },
);
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) {
if (prevProps.isRunning === true && this.props.isRunning === false) {
PerformanceTracker.stopTracking(
PerformanceTransactionName.RUN_QUERY_CLICK,
);
}
// 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.actionId !== this.props.actionId ||
prevProps.pluginId !== this.props.pluginId
) {
this.props.changeQueryPage(this.props.actionId);
}
}
onCreateDatasourceClick = () => {
const { pageId } = this.props.match.params;
const onCreateDatasourceClick = useCallback(() => {
history.push(
integrationEditorURL({
pageId,
@ -205,187 +93,44 @@ class QueryEditor extends React.Component<Props> {
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
entryPoint,
});
};
}, [
pageId,
history,
integrationEditorURL,
DatasourceCreateEntryPoints,
AnalyticsUtil,
]);
render() {
const {
actionId,
dataSources,
editorConfig,
isCreating,
isDeleting,
isEditorInitialized,
isRunning,
pluginId,
pluginIds,
responses,
runErrorMessage,
settingConfig,
uiComponent,
updateActionResponseDisplayFormat,
} = this.props;
const { pageId } = this.props.match.params;
// custom function to return user to integrations page if action is not found
const goToDatasourcePage = () =>
// custom function to return user to integrations page if action is not found
const onEntityNotFoundBackClick = useCallback(
() =>
history.push(
integrationEditorURL({
pageId,
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
);
// if the action can not be found, generate a entity not found page
if (!pluginId && actionId) {
return <EntityNotFoundPane goBackFn={goToDatasourcePage} />;
}
if (!pluginIds?.length) {
return (
<EmptyStateContainer>{"Plugin is not installed"}</EmptyStateContainer>
);
}
if (isCreating || !isEditorInitialized) {
return (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
);
}
return (
<QueryEditorForm
dataSources={dataSources}
datasourceId={this.props.datasourceId}
editorConfig={editorConfig}
executedQueryData={responses[actionId]}
formData={this.props.formData}
isDeleting={isDeleting}
isRunning={isRunning}
location={this.props.location}
onCreateDatasourceClick={this.onCreateDatasourceClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
pluginId={this.props.pluginId}
runErrorMessage={runErrorMessage[actionId]}
settingConfig={settingConfig}
uiComponent={uiComponent}
updateActionResponseDisplayFormat={updateActionResponseDisplayFormat}
/>
);
}
}
const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const { apiId, queryId } = props.match.params;
const actionId = queryId || apiId;
const { runErrorMessage } = state.ui.queryPane;
const { plugins } = state.entities;
const { editorConfigs, settingConfigs } = plugins;
const action = getAction(state, actionId) as QueryAction | SaaSAction;
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as
| QueryAction
| SaaSAction;
let pluginId;
if (action) {
pluginId = action.pluginId;
}
let editorConfig: any;
if (editorConfigs && pluginId) {
editorConfig = editorConfigs[pluginId];
}
let settingConfig: any;
if (settingConfigs && pluginId) {
settingConfig = settingConfigs[pluginId];
}
const initialValues = {};
if (editorConfig) {
merge(initialValues, getConfigInitialValues(editorConfig));
}
if (settingConfig) {
merge(initialValues, getConfigInitialValues(settingConfig));
}
// 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,
),
[pageId, history, integrationEditorURL],
);
const allPlugins = getPlugins(state);
let uiComponent = UIComponentTypes.DbEditorForm;
if (!!pluginId) uiComponent = getUIComponent(pluginId, allPlugins);
const closeEditorLink = useMemo(() => <CloseEditor />, []);
const currentEnvDetails = getCurrentEnvironmentDetails(state);
return (
<QueryEditorContextProvider
actionRightPaneBackLink={actionRightPaneBackLink}
changeQueryPage={changeQueryPage}
closeEditorLink={closeEditorLink}
moreActionsMenu={moreActionsMenu}
onCreateDatasourceClick={onCreateDatasourceClick}
onEntityNotFoundBackClick={onEntityNotFoundBackClick}
>
<Editor
{...props}
isEditorInitialized={isEditorInitialized}
settingsConfig={settingsConfig}
/>
</QueryEditorContextProvider>
);
}
return {
actionId,
currentEnvironmentId: currentEnvDetails?.id || "",
currentEnvironmentName: currentEnvDetails?.name || "",
pluginId,
plugins: allPlugins,
runErrorMessage,
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
dataSources: !!apiId
? getDatasourceByPluginId(state, action?.pluginId)
: getDBAndRemoteDatasources(state),
responses: getActionResponses(state),
isRunning: state.ui.queryPane.isRunning[actionId],
isDeleting: state.ui.queryPane.isDeleting[actionId],
isSaas: !!apiId,
formData,
editorConfig,
settingConfig,
isCreating: state.ui.apiPane.isCreating,
isEditorInitialized: getIsEditorInitialized(state),
uiComponent,
applicationId: getCurrentApplicationId(state),
actionObjectDiff,
datasourceId: action?.datasource?.id,
};
};
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
deleteAction: (id: string, name: string) =>
dispatch(deleteAction({ id, name })),
runAction: (actionId: string) => dispatch(runAction(actionId)),
changeQueryPage: (queryId: string) => {
dispatch(changeQuery(queryId));
},
initFormEvaluation: (
editorConfig: 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);
export default QueryEditor;

View File

@ -6,7 +6,6 @@ import IntegrationEditor from "./IntegrationEditor";
import QueryEditor from "./QueryEditor";
import JSEditor from "./JSEditor";
import GeneratePage from "./GeneratePage";
import CurlImportForm from "./APIEditor/CurlImportForm";
import ProviderTemplates from "./APIEditor/ProviderTemplates";
import {
API_EDITOR_ID_PATH,
@ -27,6 +26,7 @@ import * as Sentry from "@sentry/react";
import { SaaSEditorRoutes } from "./SaaSEditor/routes";
import OnboardingChecklist from "./FirstTimeUserOnboarding/Checklist";
import { DatasourceEditorRoutes } from "pages/routes";
import CurlImportEditor from "./APIEditor/CurlImportEditor";
const SentryRoute = Sentry.withSentryRouting(Route);
@ -91,7 +91,7 @@ function EditorsRouter() {
/>
<SentryRoute
component={CurlImportForm}
component={CurlImportEditor}
exact
path={`${path}${CURL_IMPORT_PAGE_PATH}`}
/>

View File

@ -24,10 +24,7 @@ import {
} from "@appsmith/constants/forms";
import history from "utils/history";
import { APPLICATIONS_URL, INTEGRATION_TABS } from "constants/routes";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "selectors/editorSelectors";
import { getCurrentPageId } from "selectors/editorSelectors";
import { autofill, change, initialize, reset } from "redux-form";
import {
getAction,
@ -86,25 +83,28 @@ import type { FeatureFlags } from "@appsmith/entities/FeatureFlag";
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
import { isGACEnabled } from "@appsmith/utils/planHelpers";
import { getHasManageActionPermission } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import type { ChangeQueryPayload } from "actions/queryPaneActions";
// Called whenever the query being edited is changed via the URL or query pane
function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
function* changeQuerySaga(actionPayload: ReduxAction<ChangeQueryPayload>) {
const { applicationId, id, moduleId, packageId, pageId } =
actionPayload.payload;
let configInitialValues = {};
const applicationId: string = yield select(getCurrentApplicationId);
const pageId: string = yield select(getCurrentPageId);
if (!applicationId || !pageId) {
if (!(packageId && moduleId) && !(applicationId && pageId)) {
history.push(APPLICATIONS_URL);
return;
}
const action: Action | undefined = yield select(getAction, id);
if (!action) {
history.push(
integrationEditorURL({
pageId,
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
);
if (pageId) {
history.push(
integrationEditorURL({
pageId,
selectedTab: INTEGRATION_TABS.ACTIVE,
}),
);
}
return;
}

View File

@ -54,3 +54,6 @@ export const getDatasourceCollapsibleState = createSelector(
return datasourceCollapsibleState[key];
},
);
export const getIsImportingCurl = (state: AppState) =>
state.ui.imports.isImportingCurl;