feat: dynamic post body in api form

- add info icon
- display helper text and placeholders dynamically
This commit is contained in:
Hetu Nandu 2020-05-05 07:50:30 +00:00
parent 6856571643
commit 0bdb9bea4c
35 changed files with 2188 additions and 41 deletions

View File

@ -0,0 +1,53 @@
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { RestAction } from "api/ActionAPI";
export const createQueryRequest = (payload: Partial<RestAction>) => {
return {
type: ReduxActionTypes.CREATE_QUERY_INIT,
payload,
};
};
export const deleteQuery = (payload: { id: string }) => {
return {
type: ReduxActionTypes.DELETE_QUERY_INIT,
payload,
};
};
export const deleteQuerySuccess = (payload: { id: string }) => {
return {
type: ReduxActionTypes.DELETE_QUERY_SUCCESS,
payload,
};
};
export const executeQuery = (payload: {
action: RestAction;
actionId: string;
}) => {
return {
type: ReduxActionTypes.EXECUTE_QUERY_REQUEST,
payload,
};
};
export const initQueryPane = (
pluginType: string,
urlId?: string,
): ReduxAction<{ pluginType: string; id?: string }> => {
return {
type: ReduxActionTypes.INIT_QUERY_PANE,
payload: { id: urlId, pluginType },
};
};
export const changeQuery = (
id: string,
pluginType: string,
): ReduxAction<{ id: string; pluginType: string }> => {
return {
type: ReduxActionTypes.QUERY_PANE_CHANGE,
payload: { id, pluginType },
};
};

View File

@ -108,6 +108,10 @@ export interface ExecuteActionRequest extends APIRequest {
paginationField?: PaginationField;
}
export interface ExecuteQueryRequest extends APIRequest {
action: Pick<RestAction, "id"> | Omit<RestAction, "id">;
}
export interface ExecuteActionResponse extends ApiResponse {
actionId: string;
data: any;
@ -200,6 +204,10 @@ class ActionAPI extends API {
static moveAction(moveRequest: MoveActionRequest) {
return API.put(ActionAPI.url + "/move", moveRequest);
}
static executeQuery(executeAction: any): AxiosPromise<ActionApiResponse> {
return API.post(ActionAPI.url + "/execute", executeAction);
}
}
export default ActionAPI;

View File

@ -0,0 +1,3 @@
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C12.418 0 16 1.79 16 4C16 6.21 12.418 8 8 8C3.582 8 0 6.21 0 4C0 1.79 3.582 0 8 0ZM16 6V9C16 11.21 12.418 13 8 13C3.582 13 0 11.21 0 9V6C0 8.21 3.582 10 8 10C12.418 10 16 8.21 16 6ZM16 11V14C16 16.21 12.418 18 8 18C3.582 18 0 16.21 0 14V11C0 13.21 3.582 15 8 15C12.418 15 16 13.21 16 11Z" fill="#2E3D49"/>
</svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@ -0,0 +1,3 @@
<svg width="15" height="19" viewBox="0 0 15 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="1.19091" y1="18.263" x2="13.4305" y2="1.4123" stroke="#D0D7DD" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -76,7 +76,7 @@ export interface TextInputProps extends IInputGroupProps {
/** Additional classname */
className?: string;
type?: string;
refHandler?: (ref: HTMLInputElement | null) => void;
refHandler?: any;
noValidate?: boolean;
readonly?: boolean;
}

View File

@ -3,6 +3,7 @@ import cm from "codemirror";
import styled from "styled-components";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/monokai.css";
require("codemirror/mode/javascript/javascript");
const Wrapper = styled.div<{ height: number }>`
@ -24,13 +25,14 @@ class CodeEditor extends React.Component<Props> {
componentDidMount(): void {
if (this.textArea.current) {
const readOnly = !this.props.input.onChange;
this.editor = cm.fromTextArea(this.textArea.current, {
mode: { name: "javascript", json: true },
value: this.props.input.value,
readOnly,
lineNumbers: true,
tabSize: 2,
indentWithTabs: true,
lineNumbers: true,
lineWrapping: true,
});
this.editor.setSize(null, this.props.height);

View File

@ -10,6 +10,7 @@ import "codemirror/addon/hint/javascript-hint";
import "codemirror/addon/display/placeholder";
import "codemirror/addon/edit/closebrackets";
import "codemirror/addon/display/autorefresh";
import "codemirror/addon/mode/multiplex";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants";
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
@ -21,6 +22,42 @@ import { DataTree } from "entities/DataTree/dataTreeFactory";
import { Theme } from "constants/DefaultTheme";
import AnalyticsUtil from "utils/AnalyticsUtil";
require("codemirror/mode/javascript/javascript");
require("codemirror/mode/sql/sql");
require("codemirror/addon/hint/sql-hint");
CodeMirror.defineMode("sql-js", function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, "text/x-sql"),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
globalVars: true,
}),
},
// .. more multiplexed styles can follow here
);
});
CodeMirror.defineMode("js-js", function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, { name: "javascript", json: true }),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
globalVars: true,
}),
},
// .. more multiplexed styles can follow here
);
});
const getBorderStyle = (
props: { theme: Theme } & {
@ -208,9 +245,12 @@ export type DynamicAutocompleteInputProps = {
showLineNumbers?: boolean;
allowTabIndent?: boolean;
singleLine: boolean;
disabled?: boolean;
mode?: string | object;
className?: string;
leftImage?: string;
disabled?: boolean;
link?: string;
baseMode?: string | object;
};
type Props = ReduxStateProps &
@ -249,7 +289,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
};
if (!this.props.allowTabIndent) extraKeys["Tab"] = false;
this.editor = CodeMirror.fromTextArea(this.textArea.current, {
mode: { name: "javascript", globalVars: true },
mode: this.props.mode || { name: "javascript", globalVars: true },
viewportMargin: 10,
tabSize: 2,
indentWithTabs: true,
@ -259,6 +299,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
autoCloseBrackets: true,
...options,
});
this.editor.on("change", _.debounce(this.handleChange, 300));
this.editor.on("keyup", this.handleAutocompleteVisibility);
this.editor.on("focus", this.handleEditorFocus);
@ -286,6 +327,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
if (this.editor) {
this.editor.refresh();
if (!this.state.isFocused) {
const currentMode = this.editor.getOption("mode");
const editorValue = this.editor.getValue();
let inputValue = this.props.input.value;
// Safe update of value of the editor when value updated outside the editor
@ -295,6 +337,10 @@ class DynamicAutocompleteInput extends Component<Props, State> {
if ((!!inputValue || inputValue === "") && inputValue !== editorValue) {
this.editor.setValue(inputValue);
}
if (currentMode !== this.props.mode) {
this.editor.setOption("mode", this.props?.mode);
}
} else {
// Update the dynamic bindings for autocomplete
if (prevProps.dynamicData !== this.props.dynamicData) {
@ -367,6 +413,11 @@ class DynamicAutocompleteInput extends Component<Props, State> {
!cm.state.completionActive &&
AUTOCOMPLETE_CLOSE_KEY_CODES.indexOf(event.code) === -1;
if (this.props.baseMode) {
// https://github.com/codemirror/CodeMirror/issues/5249#issue-295565980
cm.doc.modeOption = this.props.baseMode;
}
if (shouldShow) {
AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
this.setState({
@ -398,7 +449,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
};
render() {
const { input, meta, theme, singleLine, disabled } = this.props;
const { input, meta, theme, singleLine, disabled, className } = this.props;
const hasError = !!(meta && meta.error);
let showError = false;
if (this.editor) {
@ -413,6 +464,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
singleLine={singleLine}
isFocused={this.state.isFocused}
disabled={disabled}
className={className}
>
<HintStyles />
<IconContainer>

View File

@ -8,12 +8,15 @@ import {
PAGE_LIST_EDITOR_URL,
DATA_SOURCES_EDITOR_URL,
DATA_SOURCES_EDITOR_ID_URL,
QUERIES_EDITOR_URL,
QUERIES_EDITOR_ID_URL,
getCurlImportPageURL,
API_EDITOR_URL_WITH_SELECTED_PAGE_ID,
getProviderTemplatesURL,
} from "constants/routes";
import WidgetSidebar from "pages/Editor/WidgetSidebar";
import QuerySidebar from "pages/Editor/QuerySidebar";
import DataSourceSidebar from "pages/Editor/DataSourceSidebar";
import ApiSidebar from "pages/Editor/ApiSidebar";
import PageListSidebar from "pages/Editor/PageListSidebar";
@ -73,11 +76,23 @@ export const Sidebar = () => {
component={ApiSidebar}
name={"ApiSidebar"}
/>
<AppRoute
exact
path={QUERIES_EDITOR_URL()}
component={QuerySidebar}
name={"QuerySidebar"}
/>
<AppRoute
exact
path={QUERIES_EDITOR_ID_URL()}
component={QuerySidebar}
name={"QuerySidebar"}
/>
<AppRoute
exact
path={DATA_SOURCES_EDITOR_URL()}
name="DataSourceSidebar"
component={DataSourceSidebar}
name="DataSourceSidebar"
/>
<AppRoute
exact

View File

@ -8,6 +8,10 @@ export const API_REQUEST_HEADERS: APIHeaders = {
"Content-Type": "application/json",
};
export const POSTMAN = "POSTMAN";
export const CURL = "CURL";
export const Swagger = "Swagger";
export const OAuthURL = "/oauth2/authorization";
export const GoogleOAuthURL = `${OAuthURL}/google`;
export const GithubOAuthURL = `${OAuthURL}/github`;

View File

@ -15,6 +15,7 @@ export const DEFAULT_API_ACTION: Partial<RestAction> = {
},
};
export const API_CONSTANT = "API";
export const DEFAULT_PROVIDER_OPTION = "Business Software";
export const POST_BODY_FORMATS = ["application/json", "x-www-form-urlencoded"];

View File

@ -28,6 +28,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
LOAD_API_RESPONSE: "LOAD_API_RESPONSE",
LOAD_QUERY_RESPONSE: "LOAD_QUERY_RESPONSE",
RUN_API_REQUEST: "RUN_API_REQUEST",
INIT_API_PANE: "INIT_API_PANE",
API_PANE_CHANGE_API: "API_PANE_CHANGE_API",
RUN_API_SUCCESS: "RUN_API_SUCCESS",
EXECUTE_ACTION: "EXECUTE_ACTION",
EXECUTE_ACTION_SUCCESS: "EXECUTE_ACTION_SUCCESS",
@ -57,6 +59,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
UPDATE_ACTION_SUCCESS: "UPDATE_ACTION_SUCCESS",
DELETE_ACTION_INIT: "DELETE_ACTION_INIT",
DELETE_ACTION_SUCCESS: "DELETE_ACTION_SUCCESS",
CREATE_QUERY_INIT: "CREATE_QUERY_INIT",
FETCH_DATASOURCES_INIT: "FETCH_DATASOURCES_INIT",
FETCH_DATASOURCES_SUCCESS: "FETCH_DATASOURCES_SUCCESS",
CREATE_DATASOURCE_INIT: "CREATE_DATASOURCE_INIT",
@ -86,11 +89,11 @@ export const ReduxActionTypes: { [key: string]: string } = {
CREATE_APPLICATION_SUCCESS: "CREATE_APPLICATION_SUCCESS",
UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION",
HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE",
INIT_API_PANE: "INIT_API_PANE",
API_PANE_CHANGE_API: "API_PANE_CHANGE_API",
INIT_DATASOURCE_PANE: "INIT_DATASOURCE_PANE",
INIT_QUERY_PANE: "INIT_QUERY_PANE",
UPDATE_API_DRAFT: "UPDATE_API_DRAFT",
DELETE_API_DRAFT: "DELETE_API_DRAFT",
QUERY_PANE_CHANGE: "QUERY_PANE_CHANGE",
UPDATE_ROUTES_PARAMS: "UPDATE_ROUTES_PARAMS",
PERSIST_USER_SESSION: "PERSIST_USER_SESSION",
LOGIN_USER_INIT: "LOGIN_USER_INIT",
@ -175,6 +178,11 @@ export const ReduxActionTypes: { [key: string]: string } = {
FETCH_PROVIDER_TEMPLATES_SUCCESS: "FETCH_PROVIDER_TEMPLATES_SUCCESS",
ADD_API_TO_PAGE_INIT: "ADD_API_TO_PAGE_INIT",
ADD_API_TO_PAGE_SUCCESS: "ADD_API_TO_PAGE_SUCCESS",
DELETE_QUERY_INIT: "DELETE_QUERY_INIT",
DELETE_QUERY_SUCCESS: "DELETE_QUERY_SUCCESS",
EXECUTE_QUERY_REQUEST: "EXECUTE_QUERY_REQUEST",
RUN_QUERY_SUCCESS: "RUN_QUERY_SUCCESS",
CLEAR_PREVIOUSLY_EXECUTED_QUERY: "CLEAR_PREVIOUSLY_EXECUTED_QUERY",
FETCH_PROVIDERS_CATEGORIES_INIT: "FETCH_PROVIDERS_CATEGORIES_INIT",
FETCH_PROVIDERS_CATEGORIES_SUCCESS: "FETCH_PROVIDERS_CATEGORIES_SUCCESS",
FETCH_PROVIDERS_WITH_CATEGORY_INIT: "FETCH_PROVIDERS_WITH_CATEGORY_INIT",
@ -260,6 +268,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
MOVE_ACTION_ERROR: "MOVE_ACTION_ERROR",
COPY_ACTION_ERROR: "COPY_ACTION_ERROR",
DELETE_PAGE_ERROR: "DELETE_PAGE_ERROR",
RUN_QUERY_ERROR: "RUN_QUERY_ERROR",
DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR",
SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR",
CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR",

View File

@ -13,6 +13,8 @@ export const CREATE_PASSWORD_FORM_NAME = "CreatePasswordForm";
export const CREATE_ORGANIZATION_FORM_NAME = "CreateOrganizationForm";
export const CURL_IMPORT_FORM = "CurlImportForm";
export const QUERY_EDITOR_FORM_NAME = "QueryEditorForm";
export const API_HOME_SCREEN_FORM = "APIHomeScreenForm";
export const DATASOURCE_DB_FORM = "DatasourceDBForm";

View File

@ -1,5 +1,6 @@
import { MenuIcons } from "icons/MenuIcons";
import { FeatureFlagEnum } from "utils/featureFlags";
export const BASE_URL = "/";
export const ORG_URL = "/org";
export const APPLICATIONS_URL = `/applications`;
@ -29,6 +30,12 @@ export type ProviderViewerRouteParams = {
providerId: string;
};
export type QueryEditorRouteParams = {
applicationId: string;
pageId: string;
queryId: string;
};
export const BUILDER_BASE_URL = (applicationId = ":applicationId"): string =>
`/applications/${applicationId}`;
@ -66,6 +73,17 @@ export const DATA_SOURCES_EDITOR_ID_URL = (
): string =>
`${DATA_SOURCES_EDITOR_URL(applicationId, pageId)}/${datasourceId}`;
export const QUERIES_EDITOR_URL = (
applicationId = ":applicationId",
pageId = ":pageId",
): string => `${BUILDER_PAGE_URL(applicationId, pageId)}/queries`;
export const QUERIES_EDITOR_ID_URL = (
applicationId = ":applicationId",
pageId = ":pageId",
queryId = ":queryId",
): string => `${QUERIES_EDITOR_URL(applicationId, pageId)}/${queryId}`;
export const API_EDITOR_ID_URL = (
applicationId = ":applicationId",
pageId = ":pageId",
@ -124,6 +142,17 @@ export const getProviderTemplatesURL = (
providerId = ":providerId",
): string => `${API_EDITOR_URL(applicationId, pageId)}/provider/${providerId}`;
export const QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID = (
applicationId = ":applicationId",
pageId = ":pageId",
selectedPageId = ":importTo",
): string => {
return `${BUILDER_PAGE_URL(
applicationId,
pageId,
)}/queries?importTo=${selectedPageId}`;
};
export const EDITOR_ROUTES = [
{
icon: MenuIcons.WIDGETS_ICON,
@ -139,6 +168,14 @@ export const EDITOR_ROUTES = [
title: "APIs",
exact: false,
},
{
icon: MenuIcons.QUERIES_ICON,
className: "t--nav-link-query-editor",
path: QUERIES_EDITOR_URL,
title: "Queries",
exact: false,
flag: FeatureFlagEnum.QueryPane,
},
{
icon: MenuIcons.DATASOURCES_ICON,
className: "t--nav-link-datasource-editor",

View File

@ -5,6 +5,7 @@ import { ReactComponent as ApisIcon } from "assets/icons/menu/api.svg";
import { ReactComponent as OrgIcon } from "assets/icons/menu/org.svg";
import { ReactComponent as PagesIcon } from "assets/icons/menu/pages.svg";
import { ReactComponent as DataSourcesIcon } from "assets/icons/menu/data-sources.svg";
import { ReactComponent as QueriesIcon } from "assets/icons/menu/queries.svg";
import { ReactComponent as HomepageIcon } from "assets/icons/menu/homepage.svg";
/* eslint-disable react/display-name */
@ -36,6 +37,11 @@ export const MenuIcons: {
<DataSourcesIcon />
</IconWrapper>
),
QUERIES_ICON: (props: IconProps) => (
<IconWrapper {...props}>
<QueriesIcon />
</IconWrapper>
),
HOMEPAGE_ICON: (props: IconProps) => (
<IconWrapper {...props}>
<HomepageIcon />

View File

@ -0,0 +1,455 @@
import React, { useState, useRef, useEffect } from "react";
import {
reduxForm,
InjectedFormProps,
Field,
FormSubmitHandler,
} from "redux-form";
import {
GridComponent,
ColumnsDirective,
ColumnDirective,
} from "@syncfusion/ej2-react-grids";
import ReactJson from "react-json-view";
import styled, { createGlobalStyle } from "styled-components";
import { Popover } from "@blueprintjs/core";
import history from "utils/history";
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
import { DATA_SOURCES_EDITOR_URL } from "constants/routes";
import TemplateMenu from "./TemplateMenu";
import Spinner from "components/editorComponents/Spinner";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import Button from "components/editorComponents/Button";
import FormRow from "components/editorComponents/FormRow";
import TextField from "components/editorComponents/form/fields/TextField";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { Datasource } from "api/DatasourcesApi";
import { RestAction } from "api/ActionAPI";
import { QUERY_EDITOR_FORM_NAME } from "constants/forms";
import { PLUGIN_PACKAGE_POSTGRES } from "constants/QueryEditorConstants";
import "@syncfusion/ej2-react-grids/styles/material.css";
const QueryFormContainer = styled.div`
font-size: 20px;
padding: 20px 32px;
width: 100%;
max-height: 93vh;
a {
font-size: 14px;
line-height: 20px;
margin-top: 15px;
}
.textAreaStyles {
border-radius: 4px;
border: 1px solid #d0d7dd;
font-size: 14px;
height: calc(100vh / 3);
}
.statementTextArea {
font-size: 14px;
line-height: 20px;
color: #2e3d49;
margin-top: 15px;
}
&& {
.CodeMirror-lines {
padding: 16px 20px;
}
}
.queryInput {
max-width: 30%;
padding-right: 10px;
}
span.bp3-popover-target {
display: initial !important;
}
`;
const ActionButtons = styled.div`
flex: 1;
margin-left: 10px;
`;
const ActionButton = styled(BaseButton)`
&&& {
max-width: 72px;
margin: 0 5px;
min-height: 30px;
}
`;
const ResponseContainer = styled.div`
margin-top: 20px;
`;
const ResponseContent = styled.div`
height: calc(
100vh - (100vh / 3) - 150px - ${props => props.theme.headerHeight}
);
overflow: auto;
`;
const DropdownSelect = styled.div`
font-size: 14px;
`;
const NoDataSourceContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
margin-top: 62px;
flex: 1;
.font18 {
width: 50%;
text-align: center;
margin-bottom: 23px;
font-size: 18px;
color: #2e3d49;
}
`;
const TooltipStyles = createGlobalStyle`
.helper-tooltip{
width: 378px;
.bp3-popover {
height: 137px;
max-width: 378px;
box-shadow: none;
display: inherit !important;
.bp3-popover-arrow {
display: block;
fill: none;
}
.bp3-popover-arrow-fill {
fill: #23292E;
}
.bp3-popover-content {
padding: 15px;
background-color: #23292E;
color: #fff;
text-align: left;
border-radius: 4px;
text-transform: initial;
font-weight: 500;
font-size: 16px;
line-height: 20px;
}
.popoverBtn {
float: right;
margin-top: 25px;
}
.popuptext {
padding-right: 30px;
}
}
}
`;
const TableHeader = styled.div`
font-weight: 500;
font-size: 14px;
font-family: "DM Sans";
color: #2e3d49;
`;
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;
const StyledGridComponent = styled(GridComponent)`
&&& {
.e-altrow {
background-color: #fafafa;
}
.e-active {
background: #cccccc;
}
.e-gridcontent {
max-height: calc(
100vh - (100vh / 3) - 150px - 49px -
${props => props.theme.headerHeight}
);
overflow: auto;
}
}
`;
type QueryFormProps = {
isCreating: boolean;
onDeleteClick: () => void;
onSaveClick: () => void;
onRunClick: () => void;
createTemplate: (template: any, name: string) => void;
onSubmit: FormSubmitHandler<RestAction>;
isDeleting: boolean;
allowSave: boolean;
isSaving: boolean;
isRunning: boolean;
dataSources: Datasource[];
DATASOURCES_OPTIONS: any;
executedQueryData: any;
applicationId: string;
selectedPluginPackage: string;
pageId: string;
location: {
state: any;
};
};
export type StateAndRouteProps = QueryFormProps;
type Props = StateAndRouteProps &
InjectedFormProps<RestAction, StateAndRouteProps>;
const QueryEditorForm: React.FC<Props> = (props: Props) => {
const {
handleSubmit,
allowSave,
isDeleting,
isSaving,
isRunning,
onSaveClick,
onRunClick,
onDeleteClick,
DATASOURCES_OPTIONS,
pageId,
applicationId,
dataSources,
executedQueryData,
selectedPluginPackage,
createTemplate,
isCreating,
} = props;
const [showTemplateMenu, setMenuVisibility] = useState(true);
const isSQL = selectedPluginPackage === PLUGIN_PACKAGE_POSTGRES;
const isNewQuery = props.location.state?.newQuery ?? false;
let queryOutput = { body: [{ "": "" }] };
const inputEl = useRef<HTMLInputElement>();
if (executedQueryData) {
if (isSQL && executedQueryData.body.length) {
queryOutput = executedQueryData;
}
}
useEffect(() => {
if (isNewQuery) {
inputEl.current?.select();
}
}, [isNewQuery]);
if (isCreating) {
return (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
);
}
return (
<QueryFormContainer>
<form onSubmit={handleSubmit}>
<FormRow>
<TextField
name="name"
placeholder="Query"
className="queryInput"
refHandler={inputEl}
/>
<DropdownSelect>
<DropdownField
placeholder="Datasource"
name="datasource.id"
options={DATASOURCES_OPTIONS}
width={200}
maxMenuHeight={200}
/>
</DropdownSelect>
<ActionButtons>
<ActionButton
text="Delete"
accent="error"
loading={isDeleting}
onClick={onDeleteClick}
/>
{dataSources.length === 0 ? (
<>
<TooltipStyles />
<Popover
autoFocus={true}
canEscapeKeyClose={true}
content="You dont have a Data Source to run this query
"
position="bottom"
defaultIsOpen={false}
usePortal
portalClassName="helper-tooltip"
>
<ActionButton
text="Run"
accent="primary"
loading={isRunning}
/>
<div>
<p className="popuptext">
You dont have a Data Source to run this query
</p>
<Button
onClick={() =>
history.push(
DATA_SOURCES_EDITOR_URL(applicationId, pageId),
)
}
text="Add Datasource"
intent="primary"
filled
size="small"
className="popoverBtn"
/>
</div>
</Popover>
</>
) : (
<ActionButton
text="Run"
loading={isRunning}
accent="secondary"
onClick={onRunClick}
/>
)}
<ActionButton
text="Save"
accent="primary"
filled
onClick={onSaveClick}
loading={isSaving}
disabled={!allowSave}
/>
</ActionButtons>
</FormRow>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p className="statementTextArea">Query Statement</p>
{isSQL ? (
<a
href="https://www.postgresql.org/docs/12/index.html"
target="_blank"
rel="noopener noreferrer"
>
PostgreSQL docs
</a>
) : (
<a
href="https://docs.mongodb.com/manual/reference/command/nav-crud/"
target="_blank"
rel="noopener noreferrer"
>
Mongo docs
</a>
)}
</div>
{isNewQuery && showTemplateMenu ? (
<TemplateMenu
createTemplate={templateString => {
const name = isSQL
? "actionConfiguration.query.cmd"
: "actionConfiguration.query";
setMenuVisibility(false);
createTemplate(templateString, name);
}}
selectedPluginPackage={selectedPluginPackage}
/>
) : isSQL ? (
<Field
name="actionConfiguration.query.cmd"
component={DynamicAutocompleteInput}
className="textAreaStyles"
mode="sql-js"
baseMode="text/x-sql"
/>
) : (
<Field
name="actionConfiguration.query"
component={DynamicAutocompleteInput}
className="textAreaStyles"
mode="js-js"
normalize={(value: any) => {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}}
/>
)}
</form>
{dataSources.length === 0 && (
<NoDataSourceContainer>
<p className="font18">
Seems like you dont have any Datasouces to create a query
</p>
<Button
onClick={() =>
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId))
}
text="Add a Datasource"
intent="primary"
filled
size="small"
icon="plus"
/>
</NoDataSourceContainer>
)}
{executedQueryData && dataSources.length && (
<ResponseContainer>
<p className="statementTextArea">Query response</p>
<ResponseContent>
{isSQL ? (
<StyledGridComponent dataSource={queryOutput.body}>
<ColumnsDirective>
{Object.keys(queryOutput.body[0]).map((key: string) => {
return (
<ColumnDirective
headerTemplate={(props: { headerText: any }) => {
const { headerText } = props;
return <TableHeader>{headerText}</TableHeader>;
}}
key={key}
field={key}
width="200"
/>
);
})}
</ColumnsDirective>
</StyledGridComponent>
) : (
<ReactJson
src={executedQueryData.body}
name={null}
enableClipboard={false}
displayObjectSize={false}
displayDataTypes={false}
style={{
fontSize: "14px",
}}
/>
)}
</ResponseContent>
</ResponseContainer>
)}
</QueryFormContainer>
);
};
export default reduxForm<RestAction, StateAndRouteProps>({
form: QUERY_EDITOR_FORM_NAME,
enableReinitialize: true,
})(QueryEditorForm);

View File

@ -0,0 +1,290 @@
import React from "react";
import styled from "styled-components";
import { Icon, Card } from "@blueprintjs/core";
import { connect } from "react-redux";
import { AppState } from "reducers";
import ImageAlt from "assets/images/placeholder-image.svg";
import Postgres from "assets/images/Postgress.png";
import MongoDB from "assets/images/MongoDB.png";
import { createNewQueryName } from "utils/AppsmithUtils";
import { Plugin } from "api/PluginApi";
import {
getPlugins,
getPluginIdsOfPackageNames,
} from "selectors/entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { Datasource } from "api/DatasourcesApi";
import { RestAction } from "api/ActionAPI";
import history from "utils/history";
import { createActionRequest } from "actions/actionActions";
import { BaseTextInput } from "components/designSystems/appsmith/TextInputComponent";
import { Colors } from "constants/Colors";
import {
PLUGIN_PACKAGE_MONGO,
PLUGIN_PACKAGE_POSTGRES,
PLUGIN_PACKAGE_DBS,
} from "constants/QueryEditorConstants";
import { Page } from "constants/ReduxActionConstants";
import {
QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID,
QUERIES_EDITOR_ID_URL,
} from "constants/routes";
import AnalyticsUtil from "utils/AnalyticsUtil";
const QueryHomePage = styled.div`
font-size: 20px;
padding: 20px;
max-height: 95vh;
overflow: auto;
.addIcon {
align-items: center;
margin-top: 15px;
margin-bottom: 20px;
}
.createText {
font-size: 14px;
justify-content: center;
text-align: center;
letter-spacing: -0.17px;
color: #2e3d49;
font-weight: 500;
text-decoration: none !important;
}
.textBtn {
font-size: 14px;
justify-content: center;
text-align: center;
letter-spacing: -0.17px;
color: #2e3d49;
font-weight: 500;
text-decoration: none !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const SearchContainer = styled.div`
display: flex;
width: 40%;
`;
const SearchBar = styled(BaseTextInput)`
margin-bottom: 10px;
input {
background-color: ${Colors.WHITE};
1px solid ${Colors.GEYSER};
}
`;
const CardsWrapper = styled.div`
flex: 1;
display: -webkit-box;
flex-wrap: wrap;
-webkit-box-pack: start !important;
justify-content: center;
text-align: center;
min-width: 140px;
border-radius: 4px;
.createCard {
margin-right: 20px;
margin-top: 10px;
margin-bottom: 10px;
width: 140px;
height: 110px;
padding-bottom: 0px;
cursor: pointer !important;
}
`;
const DatasourceCardsContainer = styled.div`
flex: 1;
display: inline-flex;
flex-wrap: wrap;
margin-left: -10px;
justify-content: flex-start;
text-align: center;
min-width: 150px;
border-radius: 4px;
.eachDatasourceCard {
margin: 10px;
width: 140px;
height: 110px;
padding-bottom: 0px;
cursor: pointer;
}
.dataSourceImage {
height: 52px;
width: auto;
margin-top: -5px;
max-width: 100%;
margin-bottom: 7px;
}
`;
type QueryHomeScreenProps = {
dataSources: Datasource[];
applicationId: string;
pageId: string;
createAction: (data: Partial<RestAction>) => void;
actions: ActionDataState;
pluginIds: Array<string> | undefined;
isCreating: boolean;
location: {
search: string;
};
history: {
replace: (data: string) => void;
push: (data: string) => void;
};
plugins: Plugin[];
pages: Page[];
};
class QueryHomeScreen extends React.Component<QueryHomeScreenProps> {
handleCreateNewQuery = (dataSourceId: string, params: string) => {
const { actions, pages, applicationId } = this.props;
const pageId = new URLSearchParams(params).get("importTo");
const page = pages.find(page => page.pageId === pageId);
AnalyticsUtil.logEvent("CREATE_QUERY_CLICK", {
pageName: page?.pageName ?? "",
});
if (pageId) {
const newQueryName = createNewQueryName(actions, pageId);
history.push(QUERIES_EDITOR_ID_URL(applicationId, pageId));
this.props.createAction({
name: newQueryName,
pageId,
datasource: {
id: dataSourceId,
},
});
}
};
getImageSrc = (dataSource: Datasource) => {
const { plugins } = this.props;
const { pluginId } = dataSource;
const plugin = plugins.find(
(plugin: { id: string }) => plugin.id === pluginId,
);
switch (plugin?.packageName) {
case PLUGIN_PACKAGE_MONGO:
return MongoDB;
case PLUGIN_PACKAGE_POSTGRES:
return Postgres;
default:
return ImageAlt;
}
};
render() {
const {
dataSources,
pluginIds,
applicationId,
pageId,
history,
location,
} = this.props;
const validDataSources: Array<Datasource> = [];
dataSources.forEach(dataSource => {
if (pluginIds?.includes(dataSource.pluginId)) {
validDataSources.push(dataSource);
}
});
const queryParams: string = location.search;
const destinationPageId = new URLSearchParams(location.search).get(
"importTo",
);
if (!destinationPageId) {
history.push(
QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID(applicationId, pageId, pageId),
);
}
return (
<QueryHomePage>
<SearchContainer>
<SearchBar
icon="search"
input={{
onChange: () => null,
}}
placeholder="Search"
/>
</SearchContainer>
<p style={{ fontSize: "14px" }}>Create Query</p>
<CardsWrapper>
{dataSources.length > 0 && (
<>
<DatasourceCardsContainer>
<Card
interactive={false}
className="eachDatasourceCard"
onClick={() =>
this.handleCreateNewQuery(
validDataSources[0].id,
queryParams,
)
}
>
<Icon icon="plus" iconSize={25} className="addIcon" />
<p className="createText">Blank Query</p>
</Card>
{validDataSources.map(dataSource => {
return (
<Card
interactive={false}
className="eachDatasourceCard"
key={dataSource.id}
onClick={() =>
this.handleCreateNewQuery(dataSource.id, queryParams)
}
>
<img
src={this.getImageSrc(dataSource)}
className="dataSourceImage"
alt="Datasource"
></img>
<p className="textBtn" title={dataSource.name}>
{dataSource.name}
</p>
</Card>
);
})}
</DatasourceCardsContainer>
</>
)}
</CardsWrapper>
</QueryHomePage>
);
}
}
const mapStateToProps = (state: AppState) => ({
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
plugins: getPlugins(state),
actions: state.entities.actions,
pages: state.entities.pageList.pages,
});
const mapDispatchToProps = (dispatch: any) => ({
createAction: (data: Partial<RestAction>) => {
dispatch(createActionRequest(data));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(QueryHomeScreen);

View File

@ -0,0 +1,134 @@
import React from "react";
import styled from "styled-components";
import Templates from "./Templates";
const Container = styled.div`
display: flex;
height: 185px;
padding: 16px 24px;
flex: 1;
border-radius: 4px;
border: 1px solid #d0d7dd;
flex-direction: column;
color: #4e5d78;
`;
const BulletPoint = styled.div`
height: 4px;
width: 4px;
border-radius: 2px;
background-color: #c4c4c4;
`;
const Item = styled.div`
font-size: 14px;
line-height: 11px;
margin-left: 6px;
`;
const Row = styled.div`
display: flex;
align-items: center;
flex-direction: row;
padding: 9px;
width: 108px;
cursor: pointer;
:hover {
background-color: #ebeff2;
border-radius: 4px;
}
`;
interface TemplateMenuProps {
createTemplate: (template: any) => void;
selectedPluginPackage: string;
}
type Props = TemplateMenuProps;
class TemplateMenu extends React.Component<Props> {
nameInput!: HTMLDivElement | null;
componentDidMount() {
this.nameInput?.focus();
}
fetchTemplate = (queryType: React.ReactText) => {
const { selectedPluginPackage } = this.props;
const allTemplates = Templates[selectedPluginPackage];
if (allTemplates) {
return allTemplates[queryType];
}
};
render() {
const { createTemplate } = this.props;
return (
<Container
ref={input => {
this.nameInput = input;
}}
tabIndex={0}
onKeyPress={e => {
e.preventDefault();
if (e.key === "Enter") {
createTemplate("");
}
}}
onClick={() => createTemplate("")}
>
<div style={{ fontSize: 14 }}>
Press enter to start with a blank state or select a template.
</div>
<div style={{ marginTop: "6px" }}>
<Row
onClick={e => {
const template = this.fetchTemplate("create");
createTemplate(template);
e.stopPropagation();
}}
>
<BulletPoint />
<Item>Create</Item>
</Row>
<Row
onClick={e => {
const template = this.fetchTemplate("read");
createTemplate(template);
e.stopPropagation();
}}
>
<BulletPoint />
<Item>Read</Item>
</Row>
<Row
onClick={e => {
const template = this.fetchTemplate("delete");
createTemplate(template);
e.stopPropagation();
}}
>
<BulletPoint />
<Item>Delete</Item>
</Row>
<Row
onClick={e => {
const template = this.fetchTemplate("update");
createTemplate(template);
e.stopPropagation();
}}
>
<BulletPoint />
<Item>Update</Item>
</Row>
</div>
</Container>
);
}
}
export default TemplateMenu;

View File

@ -0,0 +1,53 @@
import {
PLUGIN_PACKAGE_MONGO,
PLUGIN_PACKAGE_POSTGRES,
} from "constants/QueryEditorConstants";
const Templates: Record<string, any> = {
[PLUGIN_PACKAGE_MONGO]: {
create: {
insert: "users",
documents: [
{
name: "John Smith",
email: ["john@appsmith.com](mailto:%22john@appsmith.com)"],
gender: "M",
},
],
},
read: {
find: "users",
filter: { id: { $gte: 10 } },
sort: { id: 1 },
limit: 10,
},
delete: {
delete: "users",
deletes: [{ q: { id: 10 } }],
},
update: {
update: "users",
updates: [
{
q: { id: 10 },
u: {
name: "Updated Sam",
email: ["updates@appsmith.com](mailto:%22updates@appsmith.com)"],
},
},
],
},
},
[PLUGIN_PACKAGE_POSTGRES]: {
create: `INSERT INTO users(
id, name, gender, avatar, email, address, role)
VALUES (?, ?, ?, ?, ?, ?, ?);`,
read: "SELECT * FROM users LIMIT 10 ORDER BY id",
delete: `DELETE FROM users WHERE id=?`,
update: `UPDATE users
Set status='APPROVED'
WHERE id=1;`,
},
};
export default Templates;

View File

@ -0,0 +1,194 @@
import React from "react";
import { RouteComponentProps } from "react-router";
import { connect } from "react-redux";
import {
submit,
getFormValues,
getFormInitialValues,
change,
} from "redux-form";
import _ from "lodash";
import styled from "styled-components";
import { QueryEditorRouteParams } from "constants/routes";
import QueryEditorForm from "./Form";
import QueryHomeScreen from "./QueryHomeScreen";
import { updateAction } from "actions/actionActions";
import { deleteQuery, executeQuery } from "actions/queryPaneActions";
import { AppState } from "reducers";
import { getDataSources } from "selectors/editorSelectors";
import { QUERY_EDITOR_FORM_NAME } from "constants/forms";
import { Datasource } from "api/DatasourcesApi";
import { RestAction } from "api/ActionAPI";
import { QueryPaneReduxState } from "reducers/uiReducers/queryPaneReducer";
import {
getPluginIdsOfPackageNames,
getPluginPackageFromDatasourceId,
} from "selectors/entitiesSelector";
import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants";
import { getCurrentApplication } from "selectors/applicationSelectors";
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
const EmptyStateContainer = styled.div`
display: flex;
height: 100%;
font-size: 20px;
`;
type QueryPageProps = {
dataSources: Datasource[];
queryPane: QueryPaneReduxState;
formData: RestAction;
isCreating: boolean;
apiPane: ApiPaneReduxState;
initialValues: RestAction;
pluginIds: Array<string> | undefined;
submitForm: (name: string) => void;
createAction: (values: RestAction) => void;
runAction: (action: RestAction, actionId: string) => void;
deleteAction: (id: string) => void;
updateAction: (data: RestAction) => void;
createTemplate: (template: string) => void;
executedQueryData: any;
selectedPluginPackage: string;
};
type StateAndRouteProps = RouteComponentProps<QueryEditorRouteParams>;
type Props = QueryPageProps & StateAndRouteProps;
class QueryEditor extends React.Component<Props> {
handleSubmit = () => {
const { formData } = this.props;
this.props.updateAction(formData);
};
handleSaveClick = () => {
this.props.submitForm(QUERY_EDITOR_FORM_NAME);
};
handleDeleteClick = () => {
const { queryId } = this.props.match.params;
this.props.deleteAction(queryId);
};
handleRunClick = () => {
const { formData, match } = this.props;
const payload = { ...formData };
this.props.runAction(payload, match.params.queryId);
};
render() {
const {
dataSources,
queryPane,
createTemplate,
match: {
params: { queryId },
},
pluginIds,
executedQueryData,
selectedPluginPackage,
apiPane,
isCreating,
} = this.props;
const { applicationId, pageId } = this.props.match.params;
if (!pluginIds?.length) {
return (
<EmptyStateContainer>{"Plugin is not installed"}</EmptyStateContainer>
);
}
const { drafts } = apiPane;
const { isSaving, isRunning, isDeleting } = queryPane;
const validDataSources: Array<Datasource> = [];
dataSources.forEach(dataSource => {
if (pluginIds?.includes(dataSource.pluginId)) {
validDataSources.push(dataSource);
}
});
const DATASOURCES_OPTIONS = validDataSources.map(dataSource => ({
label: dataSource.name,
value: dataSource.id,
}));
DATASOURCES_OPTIONS.push({
label: "Create new Datasource",
value: "createNew",
});
return (
<React.Fragment>
{queryId ? (
<QueryEditorForm
isCreating={isCreating}
location={this.props.location}
applicationId={applicationId}
pageId={pageId}
allowSave={queryId in drafts}
isSaving={isSaving[queryId]}
isRunning={isRunning[queryId]}
isDeleting={isDeleting[queryId]}
onSubmit={this.handleSubmit}
onSaveClick={this.handleSaveClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
dataSources={dataSources}
createTemplate={createTemplate}
DATASOURCES_OPTIONS={DATASOURCES_OPTIONS}
selectedPluginPackage={selectedPluginPackage}
executedQueryData={executedQueryData[queryId]}
/>
) : (
<QueryHomeScreen
dataSources={dataSources}
applicationId={applicationId}
pageId={pageId}
history={this.props.history}
location={this.props.location}
isCreating={isCreating}
/>
)}
</React.Fragment>
);
}
}
const mapStateToProps = (state: AppState): any => {
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as RestAction;
const initialValues = getFormInitialValues(QUERY_EDITOR_FORM_NAME)(
state,
) as RestAction;
const datasourceId = _.get(formData, "datasource.id");
const selectedPluginPackage = getPluginPackageFromDatasourceId(
state,
datasourceId,
);
return {
apiPane: state.ui.apiPane,
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
dataSources: getDataSources(state),
executedQueryData: state.ui.queryPane.runQuerySuccessData,
queryPane: state.ui.queryPane,
currentApplication: getCurrentApplication(state),
formData: getFormValues(QUERY_EDITOR_FORM_NAME)(state) as RestAction,
selectedPluginPackage,
initialValues,
isCreating: state.ui.queryPane.isCreating,
};
};
const mapDispatchToProps = (dispatch: any): any => ({
submitForm: (name: string) => dispatch(submit(name)),
updateAction: (data: RestAction) => dispatch(updateAction({ data })),
deleteAction: (id: string) => dispatch(deleteQuery({ id })),
runAction: (action: RestAction, actionId: string) =>
dispatch(executeQuery({ action, actionId })),
createTemplate: (template: any, name: string) => {
dispatch(change(QUERY_EDITOR_FORM_NAME, name, template));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(QueryEditor);

View File

@ -0,0 +1,222 @@
import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps } from "react-router";
import styled from "styled-components";
import { AppState } from "reducers";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
import EditorSidebar from "pages/Editor/EditorSidebar";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
import { QueryEditorRouteParams } from "constants/routes";
import { Datasource } from "api/DatasourcesApi";
import {
createActionRequest,
moveActionRequest,
copyActionRequest,
} from "actions/actionActions";
import { deleteQuery } from "actions/queryPaneActions";
import { RestAction } from "api/ActionAPI";
import { changeQuery, initQueryPane } from "actions/queryPaneActions";
import { getQueryActions } from "selectors/entitiesSelector";
import { getNextEntityName } from "utils/AppsmithUtils";
import { getDataSources } from "selectors/editorSelectors";
import { QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID } from "constants/routes";
const ActionItem = styled.div`
flex: 1;
display: flex;
align-items: center;
`;
const ActionName = styled.span`
flex: 3;
padding: 0 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
interface ReduxStateProps {
queries: ActionDataState;
apiPane: ApiPaneReduxState;
actions: ActionDataState;
dataSources: Datasource[];
}
interface ReduxDispatchProps {
createAction: (data: Partial<RestAction>) => void;
onQueryChange: (id: string, pluginType: string) => void;
initQueryPane: (pluginType: string, urlId?: string) => void;
moveAction: (
id: string,
pageId: string,
name: string,
originalPageId: string,
) => void;
copyAction: (id: string, pageId: string, name: string) => void;
deleteAction: (id: string) => void;
}
type Props = ReduxStateProps &
ReduxDispatchProps &
RouteComponentProps<QueryEditorRouteParams>;
class QuerySidebar extends React.Component<Props> {
componentDidMount(): void {
this.props.initQueryPane(QUERY_CONSTANT, this.props.match.params.queryId);
}
shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
if (
Object.keys(nextProps.apiPane.drafts) !==
Object.keys(this.props.apiPane.drafts)
) {
return true;
}
return nextProps.actions !== this.props.actions;
}
handleCreateNew = () => {
const { actions } = this.props;
const { pageId } = this.props.match.params;
const pageApiNames = actions
.filter(a => a.config.pageId === pageId)
.map(a => a.config.name);
const newName = getNextEntityName("Query", pageApiNames);
this.props.createAction({ name: newName, pageId });
};
handleCreateNewQuery = (dataSourceId: string, pageId: string) => {
const { actions } = this.props;
const pageApiNames = actions
.filter(a => a.config.pageId === pageId)
.map(a => a.config.name);
const newQueryName = getNextEntityName("Query", pageApiNames);
this.props.createAction({
name: newQueryName,
pageId,
datasource: {
id: dataSourceId,
},
});
};
handleCreateNewQueryClick = (selectedPageId: string) => {
const { history } = this.props;
const { pageId, applicationId } = this.props.match.params;
history.push(
QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID(
applicationId,
pageId,
selectedPageId,
),
);
};
handleQueryChange = (queryId: string) => {
this.props.onQueryChange(queryId, QUERY_CONSTANT);
};
handleMove = (itemId: string, destinationPageId: string) => {
const query = this.props.queries.filter(a => a.config.id === itemId)[0];
const pageApiNames = this.props.queries
.filter(a => a.config.pageId === destinationPageId)
.map(a => a.config.name);
let name = query.config.name;
if (pageApiNames.indexOf(query.config.name) > -1) {
name = getNextEntityName(name, pageApiNames);
}
this.props.moveAction(itemId, destinationPageId, name, query.config.pageId);
};
handleCopy = (itemId: string, destinationPageId: string) => {
const query = this.props.queries.filter(a => a.config.id === itemId)[0];
const pageApiNames = this.props.queries
.filter(a => a.config.pageId === destinationPageId)
.map(a => a.config.name);
let name = `${query.config.name}Copy`;
if (pageApiNames.indexOf(name) > -1) {
name = getNextEntityName(name, pageApiNames);
}
this.props.copyAction(itemId, destinationPageId, name);
};
handleDelete = (itemId: string) => {
this.props.deleteAction(itemId);
};
renderItem = (query: RestAction) => {
return (
<ActionItem>
<ActionName>{query.name}</ActionName>
</ActionItem>
);
};
render() {
const {
apiPane: { drafts },
apiPane: { isFetching },
match: {
params: { queryId },
},
queries,
dataSources,
} = this.props;
const data = queries.map(a => a.config);
const validDataSources: Array<Datasource> = [];
dataSources.forEach(dataSource => {
if (dataSource.isValid) {
validDataSources.push(dataSource);
}
});
return (
<EditorSidebar
isLoading={isFetching}
list={data}
selectedItemId={queryId}
draftIds={Object.keys(drafts)}
itemRender={this.renderItem}
onItemCreateClick={this.handleCreateNewQueryClick}
onItemSelected={this.handleQueryChange}
moveItem={this.handleMove}
copyItem={this.handleCopy}
deleteItem={this.handleDelete}
createButtonTitle="Create new query"
/>
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
queries: getQueryActions(state),
apiPane: state.ui.apiPane,
actions: state.entities.actions,
dataSources: getDataSources(state),
});
const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({
createAction: (data: Partial<RestAction>) =>
dispatch(createActionRequest(data)),
onQueryChange: (queryId: string, pluginType: string) => {
dispatch(changeQuery(queryId, pluginType));
},
initQueryPane: (pluginType: string, urlId?: string) =>
dispatch(initQueryPane(pluginType, urlId)),
moveAction: (
id: string,
destinationPageId: string,
name: string,
originalPageId: string,
) =>
dispatch(
moveActionRequest({ id, destinationPageId, originalPageId, name }),
),
copyAction: (id: string, destinationPageId: string, name: string) =>
dispatch(copyActionRequest({ id, destinationPageId, name })),
deleteAction: (id: string) => dispatch(deleteQuery({ id })),
});
export default connect(mapStateToProps, mapDispatchToProps)(QuerySidebar);

View File

@ -1,6 +1,7 @@
import React from "react";
import { Switch, withRouter, RouteComponentProps } from "react-router-dom";
import ApiEditor from "./APIEditor";
import QueryEditor from "./QueryEditor";
import DataSourceEditor from "./DataSourceEditor";
import CurlImportForm from "./APIEditor/CurlImportForm";
@ -8,6 +9,8 @@ import ProviderTemplates from "./APIEditor/ProviderTemplates";
import {
API_EDITOR_ID_URL,
API_EDITOR_URL,
QUERIES_EDITOR_URL,
QUERIES_EDITOR_ID_URL,
DATA_SOURCES_EDITOR_URL,
DATA_SOURCES_EDITOR_ID_URL,
BUILDER_PAGE_URL,
@ -66,6 +69,9 @@ class EditorsRouter extends React.Component<
) !== -1 ||
this.props.location.pathname.indexOf(
API_EDITOR_URL(applicationId, pageId),
) !== -1 ||
this.props.location.pathname.indexOf(
QUERIES_EDITOR_URL(applicationId, pageId),
) !== -1
),
};
@ -87,6 +93,9 @@ class EditorsRouter extends React.Component<
) !== -1 ||
this.props.location.pathname.indexOf(
API_EDITOR_URL(applicationId, pageId),
) !== -1 ||
this.props.location.pathname.indexOf(
QUERIES_EDITOR_URL(applicationId, pageId),
) !== -1
),
});
@ -139,6 +148,19 @@ class EditorsRouter extends React.Component<
component={ApiEditor}
name={"ApiEditor"}
/>
<AppRoute
exact
path={QUERIES_EDITOR_URL()}
component={QueryEditor}
name={"QueryEditor"}
/>
<AppRoute
exact
path={QUERIES_EDITOR_ID_URL()}
component={QueryEditor}
name={"QueryEditor"}
/>
<AppRoute
exact
path={getCurlImportPageURL()}

View File

@ -90,6 +90,10 @@ const actionsReducer = createReducer(initialState, {
state: ActionDataState,
action: ReduxAction<{ id: string }>,
): ActionDataState => state.filter(a => a.config.id !== action.payload.id),
[ReduxActionTypes.DELETE_QUERY_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
): ActionDataState => state.filter(a => a.config.id !== action.payload.id),
[ReduxActionTypes.EXECUTE_API_ACTION_REQUEST]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
@ -111,6 +115,7 @@ const actionsReducer = createReducer(initialState, {
if (a.config.id === action.payload.id) {
return { ...a, isLoading: false, data: action.payload.response };
}
return a;
});
},
@ -122,6 +127,7 @@ const actionsReducer = createReducer(initialState, {
if (a.config.id === action.payload.actionId) {
return { ...a, isLoading: false };
}
return a;
}),
[ReduxActionTypes.RUN_API_REQUEST]: (
@ -135,6 +141,7 @@ const actionsReducer = createReducer(initialState, {
isLoading: true,
};
}
return a;
}),
[ReduxActionTypes.RUN_API_SUCCESS]: (
@ -149,6 +156,19 @@ const actionsReducer = createReducer(initialState, {
return a;
});
},
[ReduxActionTypes.RUN_QUERY_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ actionId: string; data: ActionResponse }>,
): ActionDataState => {
const actionId: string = action.payload.actionId;
return state.map(a => {
if (a.config.id === actionId) {
return { ...a, isLoading: false, data: action.payload.data };
}
return a;
});
},
[ReduxActionErrorTypes.RUN_API_ERROR]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
@ -157,6 +177,7 @@ const actionsReducer = createReducer(initialState, {
if (a.config.id === action.payload.id) {
return { ...a, isLoading: false };
}
return a;
}),
[ReduxActionTypes.MOVE_ACTION_INIT]: (
@ -178,6 +199,7 @@ const actionsReducer = createReducer(initialState, {
},
};
}
return a;
}),
[ReduxActionTypes.MOVE_ACTION_SUCCESS]: (
@ -188,6 +210,7 @@ const actionsReducer = createReducer(initialState, {
if (a.config.id === action.payload.id) {
return { ...a, config: action.payload };
}
return a;
}),
[ReduxActionErrorTypes.MOVE_ACTION_ERROR]: (
@ -204,6 +227,7 @@ const actionsReducer = createReducer(initialState, {
},
};
}
return a;
}),
[ReduxActionTypes.COPY_ACTION_INIT]: (
@ -240,6 +264,7 @@ const actionsReducer = createReducer(initialState, {
config: action.payload,
};
}
return a;
}),
[ReduxActionErrorTypes.COPY_ACTION_ERROR]: (
@ -257,6 +282,7 @@ const actionsReducer = createReducer(initialState, {
}
return true;
}
return true;
}),
});

View File

@ -17,6 +17,7 @@ import { DatasourcePaneReduxState } from "./uiReducers/datasourcePaneReducer";
import { ApplicationsReduxState } from "./uiReducers/applicationsReducer";
import { PageListReduxState } from "./entityReducers/pageListReducer";
import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
import { QueryPaneReduxState } from "./uiReducers/queryPaneReducer";
import { PluginDataState } from "reducers/entityReducers/pluginsReducer";
import { AuthState } from "reducers/uiReducers/authReducer";
import { OrgReduxState } from "reducers/uiReducers/orgReducer";
@ -51,6 +52,7 @@ export interface AppState {
importedCollections: ImportedCollectionsReduxState;
providers: ProvidersReduxState;
imports: ImportReduxState;
queryPane: QueryPaneReduxState;
datasourcePane: DatasourcePaneReduxState;
};
entities: {

View File

@ -14,6 +14,7 @@ import { widgetDraggingReducer } from "./dragResizeReducer";
import importedCollectionsReducer from "./importedCollectionsReducer";
import providersReducer from "./providerReducer";
import importReducer from "./importReducer";
import queryPaneReducer from "./queryPaneReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@ -30,6 +31,7 @@ const uiReducer = combineReducers({
importedCollections: importedCollectionsReducer,
providers: providersReducer,
imports: importReducer,
queryPane: queryPaneReducer,
datasourcePane: datasourcePaneReducer,
});
export default uiReducer;

View File

@ -0,0 +1,163 @@
import { createReducer } from "utils/AppsmithUtils";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
ReduxAction,
} from "constants/ReduxActionConstants";
import { RestAction } from "api/ActionAPI";
const initialState: QueryPaneReduxState = {
isFetching: false,
isCreating: false,
isRunning: {},
isSaving: {},
isDeleting: {},
runQuerySuccessData: {},
lastUsed: "",
};
export interface QueryPaneReduxState {
isFetching: boolean;
isRunning: Record<string, boolean>;
isSaving: Record<string, boolean>;
isDeleting: Record<string, boolean>;
runQuerySuccessData: {};
lastUsed: string;
isCreating: boolean;
}
const queryPaneReducer = createReducer(initialState, {
[ReduxActionTypes.CREATE_ACTION_INIT]: (state: QueryPaneReduxState) => {
return {
...state,
isCreating: true,
};
},
[ReduxActionTypes.CREATE_ACTION_SUCCESS]: (state: QueryPaneReduxState) => {
return {
...state,
isCreating: false,
};
},
[ReduxActionTypes.CREATE_ACTION_ERROR]: (state: QueryPaneReduxState) => {
return {
...state,
isCreating: false,
};
},
[ReduxActionTypes.QUERY_PANE_CHANGE]: (
state: QueryPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
lastUsed: action.payload.id,
}),
[ReduxActionTypes.UPDATE_ACTION_INIT]: (
state: QueryPaneReduxState,
action: ReduxAction<{ data: RestAction }>,
) => ({
...state,
isSaving: {
...state.isSaving,
[action.payload.data.id]: true,
},
}),
[ReduxActionTypes.UPDATE_ACTION_SUCCESS]: (
state: QueryPaneReduxState,
action: ReduxAction<{ data: RestAction }>,
) => ({
...state,
isSaving: {
...state.isSaving,
[action.payload.data.id]: false,
},
}),
[ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: (
state: QueryPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
isSaving: {
...state.isSaving,
[action.payload.id]: false,
},
}),
[ReduxActionTypes.DELETE_QUERY_INIT]: (
state: QueryPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
isDeleting: {
...state.isDeleting,
[action.payload.id]: true,
},
}),
[ReduxActionTypes.DELETE_QUERY_SUCCESS]: (
state: QueryPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
isDeleting: {
...state.isDeleting,
[action.payload.id]: false,
},
}),
[ReduxActionErrorTypes.DELETE_QUERY_ERROR]: (
state: QueryPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
isDeleting: {
...state.isDeleting,
[action.payload.id]: false,
},
}),
[ReduxActionTypes.EXECUTE_QUERY_REQUEST]: (
state: any,
action: ReduxAction<{ action: RestAction; actionId: string }>,
) => {
return {
...state,
isRunning: {
...state.isRunning,
[action.payload.actionId]: true,
},
runQuerySuccessData: [],
};
},
[ReduxActionTypes.CLEAR_PREVIOUSLY_EXECUTED_QUERY]: (state: any) => ({
...state,
runQuerySuccessData: [],
}),
[ReduxActionTypes.RUN_QUERY_SUCCESS]: (
state: any,
action: ReduxAction<{ actionId: string; data: object }>,
) => {
return {
...state,
isRunning: {
...state.isRunning,
[action.payload.actionId]: false,
},
runQuerySuccessData: {
...state.runQuerySuccessData,
[action.payload.actionId]: action.payload.data,
},
};
},
[ReduxActionErrorTypes.RUN_QUERY_ERROR]: (
state: any,
action: ReduxAction<{ actionId: string }>,
) => {
return {
...state,
isRunning: {
...state.isRunning,
[action.payload.actionId]: false,
},
};
},
});
export default queryPaneReducer;

View File

@ -73,6 +73,7 @@ import {
import { ToastType } from "react-toastify";
import AnalyticsUtil from "utils/AnalyticsUtil";
import * as log from "loglevel";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
export const getAction = (
state: AppState,
@ -427,30 +428,42 @@ export function* updateActionSaga(
actionPayload: ReduxAction<{ data: RestAction }>,
) {
try {
const isApi = actionPayload.payload.data.pluginType !== "DB";
const { data } = actionPayload.payload;
const action = transformRestAction(data);
let action = data;
if (isApi) {
action = transformRestAction(data);
}
const response: GenericApiResponse<RestAction> = yield ActionAPI.updateAPI(
action,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
AppToaster.show({
message: `${actionPayload.payload.data.name} Action updated`,
type: ToastType.SUCCESS,
});
const pageName = yield select(
getCurrentPageNameByActionId,
response.data.id,
);
if (action.pluginType === QUERY_CONSTANT) {
AnalyticsUtil.logEvent("SAVE_QUERY", {
queryName: action.name,
pageName,
});
}
AppToaster.show({
message: `${actionPayload.payload.data.name} Action updated`,
type: ToastType.SUCCESS,
});
AnalyticsUtil.logEvent("SAVE_API", {
apiId: response.data.id,
apiName: response.data.name,
pageName: pageName,
});
yield put(updateActionSuccess({ data: response.data }));
yield put(runApiAction(data.id));
if (actionPayload.payload.data.pluginType !== "DB") {
yield put(runApiAction(data.id));
}
}
} catch (error) {
yield put({

View File

@ -361,13 +361,16 @@ function* formValueChangeSaga(
}
function* handleActionCreatedSaga(actionPayload: ReduxAction<RestAction>) {
const { id } = actionPayload.payload;
const { id, pluginType } = actionPayload.payload;
const action = yield select(getAction, id);
const data = { ...action };
yield put(initialize(API_EDITOR_FORM_NAME, data));
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
if (pluginType === "API") {
yield put(initialize(API_EDITOR_FORM_NAME, data));
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
}
}
function* handleActionUpdatedSaga(
@ -394,14 +397,18 @@ function* handleActionDeletedSaga(actionPayload: ReduxAction<{ id: string }>) {
function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
const action = yield select(getAction, id);
const { values }: { values: RestAction } = yield select(
getFormData,
API_EDITOR_FORM_NAME,
);
if (values.id === id) {
yield put(initialize(API_EDITOR_FORM_NAME, action));
} else {
yield put(changeApi(id));
const pluginType = action?.pluginType ?? "";
if (pluginType === "API") {
const { values }: { values: RestAction } = yield select(
getFormData,
API_EDITOR_FORM_NAME,
);
if (values.id === id) {
yield put(initialize(API_EDITOR_FORM_NAME, action));
} else {
yield put(changeApi(id));
}
}
}

View File

@ -9,6 +9,7 @@ import { API_EDITOR_FORM_NAME } from "constants/forms";
import { validateResponse } from "sagas/ErrorSagas";
import CurlImportApi, { CurlImportRequest } from "api/ImportApi";
import { ApiResponse } from "api/ApiResponses";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { AppToaster } from "components/editorComponents/ToastComponent";
import { ToastType } from "react-toastify";
import { CURL_IMPORT_SUCCESS } from "constants/messages";
@ -19,6 +20,7 @@ import {
getCurrentPageId,
} from "selectors/editorSelectors";
import { fetchActions } from "actions/actionActions";
import { CURL } from "constants/ApiConstants";
export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
const { type, pageId, name } = action.payload;
@ -36,6 +38,9 @@ export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
const currentPageId = yield select(getCurrentPageId);
if (isValidResponse) {
AnalyticsUtil.logEvent("IMPORT_API", {
importSource: CURL,
});
AppToaster.show({
message: CURL_IMPORT_SUCCESS,
type: ToastType.SUCCESS,

View File

@ -0,0 +1,296 @@
import _, { take } from "lodash";
import {
all,
select,
put,
takeEvery,
call,
takeLatest,
} from "redux-saga/effects";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionWithMeta,
ReduxFormActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { getFormData } from "selectors/formSelectors";
import { QUERY_EDITOR_FORM_NAME } from "constants/forms";
import history from "utils/history";
import {
QUERIES_EDITOR_URL,
QUERIES_EDITOR_ID_URL,
APPLICATIONS_URL,
} from "constants/routes";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "selectors/editorSelectors";
import { initialize } from "redux-form";
import { getAction, getActionParams } from "./ActionSagas";
import { AppState } from "reducers";
import ActionAPI, {
RestAction,
PaginationField,
ExecuteActionRequest,
ActionApiResponse,
} from "api/ActionAPI";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
import { changeQuery, deleteQuerySuccess } from "actions/queryPaneActions";
import { AppToaster } from "components/editorComponents/ToastComponent";
import { ToastType } from "react-toastify";
import { PageAction } from "constants/ActionConstants";
import { transformRestAction } from "transformers/RestActionTransformer";
import { isDynamicValue, getDynamicBindings } from "utils/DynamicBindingUtils";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { GenericApiResponse } from "api/ApiResponses";
import { validateResponse } from "./ErrorSagas";
import { getQueryName } from "selectors/entitiesSelector";
const getQueryDraft = (state: AppState, id: string) => {
const drafts = state.ui.apiPane.drafts;
if (id in drafts) return drafts[id];
return {};
};
const getActions = (state: AppState) =>
state.entities.actions.map(a => a.config);
const getLastUsedAction = (state: AppState) => state.ui.queryPane.lastUsed;
function* initQueryPaneSaga(
actionPayload: ReduxAction<{
pluginType: string;
id?: string;
}>,
) {
let actions = yield select(getActions);
while (!actions.length) {
yield take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS);
actions = yield select(getActions);
}
const urlId = actionPayload.payload.id;
const lastUsedId = yield select(getLastUsedAction);
let id = "";
if (urlId) {
id = urlId;
} else if (lastUsedId) {
id = lastUsedId;
}
yield put(changeQuery(id, QUERY_CONSTANT));
}
function* changeQuerySaga(
actionPayload: ReduxAction<{ id: string; pluginType: string }>,
) {
const { id } = actionPayload.payload;
// Typescript says Element does not have blur function but it does;
document.activeElement &&
"blur" in document.activeElement &&
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
document.activeElement.blur();
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
if (!applicationId || !pageId) {
history.push(APPLICATIONS_URL);
return;
}
const action = yield select(getAction, id);
if (!action) {
history.push(QUERIES_EDITOR_URL(applicationId, pageId));
return;
}
const draft = yield select(getQueryDraft, id);
const data = _.isEmpty(draft) ? action : draft;
const URL = QUERIES_EDITOR_ID_URL(applicationId, pageId, id);
yield put(initialize(QUERY_EDITOR_FORM_NAME, data));
history.push(URL);
}
function* updateDraftsSaga() {
const { values } = yield select(getFormData, QUERY_EDITOR_FORM_NAME);
if (!values.id) return;
const action = yield select(getAction, values.id);
if (_.isEqual(values, action)) {
yield put({
type: ReduxActionTypes.DELETE_API_DRAFT,
payload: { id: values.id },
});
} else {
yield put({
type: ReduxActionTypes.UPDATE_API_DRAFT,
payload: { id: values.id, draft: values },
});
}
}
function* formValueChangeSaga(
actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>,
) {
const { form } = actionPayload.meta;
if (form !== QUERY_EDITOR_FORM_NAME) return;
yield all([call(updateDraftsSaga)]);
}
function* handleQueryCreatedSaga(actionPayload: ReduxAction<RestAction>) {
const { id, pluginType } = actionPayload.payload;
const action = yield select(getAction, id);
const data = { ...action };
if (pluginType === "DB") {
yield put(initialize(QUERY_EDITOR_FORM_NAME, data));
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
history.replace(QUERIES_EDITOR_ID_URL(applicationId, pageId, id), {
newQuery: true,
});
}
}
function* handleQueryDeletedSaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
history.push(QUERIES_EDITOR_URL(applicationId, pageId));
yield put({
type: ReduxActionTypes.DELETE_API_DRAFT,
payload: { id },
});
}
function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
const action = yield select(getAction, id);
const pluginType = action?.pluginType ?? "";
if (pluginType === "DB") {
const { values }: { values: RestAction } = yield select(
getFormData,
QUERY_EDITOR_FORM_NAME,
);
if (values.id === id) {
yield put(initialize(QUERY_EDITOR_FORM_NAME, action));
}
}
}
export function* executeQuerySaga(
actionPayload: ReduxAction<{
action: RestAction;
actionId: string;
paginationField: PaginationField;
}>,
) {
try {
const {
values,
dirty,
}: {
values: RestAction;
dirty: boolean;
valid: boolean;
} = yield select(getFormData, QUERY_EDITOR_FORM_NAME);
const actionObject: PageAction = yield select(getAction, values.id);
let action: ExecuteActionRequest["action"] = { id: values.id };
let jsonPathKeys = actionObject.jsonPathKeys;
if (dirty) {
action = _.omit(transformRestAction(values), "id") as RestAction;
const actionString = JSON.stringify(action);
if (isDynamicValue(actionString)) {
const { jsSnippets } = getDynamicBindings(actionString);
// Replace cause the existing keys could have been updated
jsonPathKeys = jsSnippets.filter(jsSnippet => !!jsSnippet);
} else {
jsonPathKeys = [];
}
}
const { paginationField } = actionPayload.payload;
const params = yield call(getActionParams, jsonPathKeys);
const response: ActionApiResponse = yield ActionAPI.executeAction({
action,
params,
paginationField,
});
if (response.responseMeta && response.responseMeta.error) {
throw response.responseMeta.error;
}
yield put({
type: ReduxActionTypes.RUN_QUERY_SUCCESS,
payload: {
data: response.data,
actionId: actionPayload.payload.actionId,
},
});
AppToaster.show({
message: "Query ran successfully",
type: ToastType.SUCCESS,
});
AnalyticsUtil.logEvent("RUN_QUERY", {
queryName: actionPayload.payload.action.name,
});
} catch (error) {
yield put({
type: ReduxActionErrorTypes.RUN_QUERY_ERROR,
payload: {
actionId: actionPayload.payload.actionId,
show: false,
},
});
AppToaster.show({
message: "Query is invalid. Please edit to make it valid",
type: ToastType.ERROR,
});
}
}
export function* deleteQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
try {
const id = actionPayload.payload.id;
const response: GenericApiResponse<RestAction> = yield ActionAPI.deleteAction(
id,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
const queryName = yield select(getQueryName, id);
AnalyticsUtil.logEvent("DELETE_QUERY", {
queryName,
});
AppToaster.show({
message: `${response.data.name} Action deleted`,
type: ToastType.SUCCESS,
});
yield put(deleteQuerySuccess({ id }));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.DELETE_QUERY_ERROR,
payload: { error, id: actionPayload.payload.id },
});
}
}
export default function* root() {
yield all([
takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleQueryCreatedSaga),
takeLatest(ReduxActionTypes.DELETE_QUERY_INIT, deleteQuerySaga),
takeEvery(ReduxActionTypes.DELETE_QUERY_SUCCESS, handleQueryDeletedSaga),
takeEvery(ReduxActionTypes.MOVE_ACTION_SUCCESS, handleMoveOrCopySaga),
takeEvery(ReduxActionTypes.COPY_ACTION_SUCCESS, handleMoveOrCopySaga),
takeLatest(ReduxActionTypes.EXECUTE_QUERY_REQUEST, executeQuerySaga),
takeEvery(ReduxActionTypes.QUERY_PANE_CHANGE, changeQuerySaga),
takeEvery(ReduxActionTypes.INIT_QUERY_PANE, initQueryPaneSaga),
// Intercepting the redux-form change actionType
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),
]);
}

View File

@ -15,6 +15,7 @@ import orgSagas from "./OrgSagas";
import importedCollectionsSagas from "./CollectionSagas";
import providersSagas from "./ProvidersSaga";
import curlImportSagas from "./CurlImportSagas";
import queryPaneSagas from "./QueryPaneSagas";
import modalSagas from "./ModalSagas";
import batchSagas from "./BatchSagas";
@ -36,6 +37,7 @@ export function* rootSaga() {
spawn(importedCollectionsSagas),
spawn(providersSagas),
spawn(curlImportSagas),
spawn(queryPaneSagas),
spawn(modalSagas),
spawn(batchSagas),
]);

View File

@ -24,14 +24,14 @@ import * as log from "loglevel";
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
const getWidgetSideBar = (state: AppState) => state.ui.widgetSidebar;
const getPageListState = (state: AppState) => state.entities.pageList;
export const getDataSources = (state: AppState) =>
state.entities.datasources.list;
export const getLastSelectedPage = (state: AppState) =>
state.ui.apiPane.lastSelectedPage;
export const getProviderCategories = (state: AppState) =>
state.ui.providers.providerCategories;
export const getDataSources = (state: AppState) =>
state.entities.datasources.list;
const getWidgets = (state: AppState): CanvasWidgetsReduxState =>
state.entities.canvasWidgets;

View File

@ -1,6 +1,11 @@
import { AppState } from "reducers";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import {
ActionDataState,
ActionData,
} from "reducers/entityReducers/actionsReducer";
import { ActionResponse } from "api/ActionAPI";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
import { API_CONSTANT } from "constants/ApiEditorConstants";
import { createSelector } from "reselect";
import { Page } from "constants/ReduxActionConstants";
@ -31,6 +36,19 @@ export const getPluginIdsOfNames = (
return pluginIds;
};
export const getPluginIdsOfPackageNames = (
state: AppState,
names: Array<string>,
): Array<string> | undefined => {
const plugins = state.entities.plugins.list.filter(plugin =>
names.includes(plugin.packageName),
);
const pluginIds = plugins.map(plugin => plugin.id);
if (!pluginIds.length) return undefined;
return pluginIds;
};
export const getPluginNameFromDatasourceId = (
state: AppState,
datasourceId: string,
@ -46,15 +64,6 @@ export const getPluginNameFromDatasourceId = (
return plugin.name;
};
export const getPluginNameFromId = (state: AppState, pluginId: string) => {
const plugin = state.entities.plugins.list.find(
plugin => plugin.id === pluginId,
);
if (!plugin) return "";
return plugin.name;
};
export const getPluginPackageFromId = (state: AppState, pluginId: string) => {
const plugin = state.entities.plugins.list.find(
plugin => plugin.id === pluginId,
@ -79,6 +88,15 @@ export const getPluginPackageFromDatasourceId = (
return plugin.packageName;
};
export const getPluginNameFromId = (state: AppState, pluginId: string) => {
const plugin = state.entities.plugins.list.find(
plugin => plugin.id === pluginId,
);
if (!plugin) return "";
return plugin.name;
};
export const getPluginForm = (state: AppState, pluginId: string): [] => {
return state.entities.plugins.formConfigs[pluginId];
};
@ -93,6 +111,34 @@ export const getDatasourceNames = (state: AppState): any =>
state.entities.datasources.list.map(datasource => datasource.name);
export const getPlugins = (state: AppState) => state.entities.plugins.list;
export const getApiActions = (state: AppState): ActionDataState => {
return state.entities.actions.filter((action: ActionData) => {
return action.config.pluginType === API_CONSTANT;
});
};
export const getQueryName = (state: AppState, actionId: string): string => {
const action = state.entities.actions.find((action: ActionData) => {
return action.config.id === actionId;
});
return action?.config.name ?? "";
};
export const getPageName = (state: AppState, pageId: string): string => {
const page = state.entities.pageList.pages.find((page: Page) => {
return page.pageId === pageId;
});
return page?.pageName ?? "";
};
export const getQueryActions = (state: AppState): ActionDataState => {
return state.entities.actions.filter((action: ActionData) => {
return action.config.pluginType === QUERY_CONSTANT;
});
};
const getCurrentPageId = (state: AppState) =>
state.entities.pageList.currentPageId;
@ -112,5 +158,6 @@ export const getActionResponses = (
state.entities.actions.forEach(a => {
responses[a.config.id] = a.data;
});
return responses;
};

View File

@ -26,6 +26,8 @@ export type EventName =
| "PREVIEW_APP"
| "EDITOR_OPEN"
| "CREATE_API"
| "IMPORT_API"
| "IMPORT_API_CLICK"
| "SAVE_API"
| "SAVE_API_CLICK"
| "RUN_API"
@ -33,7 +35,12 @@ export type EventName =
| "DELETE_API"
| "DELETE_API_CLICK"
| "DUPLICATE_API"
| "DUPLICATE_API_CLICK"
| "RUN_QUERY"
| "DELETE_QUERY"
| "SAVE_QUERY"
| "MOVE_API"
| "MOVE_API_CLICK"
| "API_SELECT"
| "CREATE_API_CLICK"
| "AUTO_COMPELTE_SHOW"
@ -42,6 +49,7 @@ export type EventName =
| "CREATE_APP"
| "CREATE_DATA_SOURCE_CLICK"
| "SAVE_DATA_SOURCE"
| "CREATE_QUERY_CLICK"
| "NAVIGATE"
| "PAGE_LOAD"
| "NAVIGATE_EDITOR"

View File

@ -3,7 +3,6 @@ import { getAppsmithConfigs } from "configs";
import * as Sentry from "@sentry/browser";
import AnalyticsUtil from "./AnalyticsUtil";
import FontFaceObserver from "fontfaceobserver";
import FormControlRegistry from "./FormControlRegistry";
import { Property } from "api/ActionAPI";
import _ from "lodash";
@ -105,6 +104,17 @@ export const noop = () => {
console.log("noop");
};
export const createNewQueryName = (
queries: ActionDataState,
pageId: string,
) => {
const pageApiNames = queries
.filter(a => a.config.pageId === pageId)
.map(a => a.config.name);
const newName = getNextEntityName("Query", pageApiNames);
return newName;
};
export const convertToString = (value: any): string => {
if (_.isUndefined(value)) {
return "";

View File

@ -0,0 +1 @@
declare module "react-json-view";