chore: Move action redesign to GA (#38659)
This commit is contained in:
parent
5a98f55024
commit
1963a9a27d
|
|
@ -5,7 +5,6 @@ import produce from "immer";
|
|||
const defaultFlags = {
|
||||
release_side_by_side_ide_enabled: true,
|
||||
rollout_remove_feature_walkthrough_enabled: false, // remove this flag from here when it's removed from code
|
||||
release_actions_redesign_enabled: true,
|
||||
release_git_modularisation_enabled: true,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
import { Tab, TabPanel, Tabs, TabsList } from "@appsmith/ads";
|
||||
import FormLabel from "components/editorComponents/FormLabel";
|
||||
import type { AutoGeneratedHeader } from "pages/Editor/APIEditor/helpers";
|
||||
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import React from "react";
|
||||
import { API_EDITOR_TABS } from "../../../../constants/CommonApiConstants";
|
||||
import { DatasourceConfig } from "./components/DatasourceConfig";
|
||||
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
|
||||
import ApiAuthentication from "./components/ApiAuthentication";
|
||||
import ActionSettings from "pages/Editor/ActionSettings";
|
||||
import { API_EDITOR_TAB_TITLES, createMessage } from "ee/constants/messages";
|
||||
import { useSelectedFormTab } from "./hooks/useSelectedFormTab";
|
||||
import { getHeadersCount, getParamsCount } from "./utils";
|
||||
import type { Property } from "entities/Action";
|
||||
|
||||
const SettingsWrapper = styled.div`
|
||||
padding: var(--ads-v2-spaces-4) 0;
|
||||
height: 100%;
|
||||
|
||||
${FormLabel} {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
const StyledTabPanel = styled(TabPanel)`
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
/**
|
||||
* @deprecated This component will be deleted along with APIEditor/CommonEditorForm.
|
||||
*/
|
||||
export function RequestTabs(props: {
|
||||
autogeneratedHeaders: AutoGeneratedHeader[] | undefined;
|
||||
datasourceHeaders: Property[];
|
||||
actionConfigurationHeaders: Property[];
|
||||
actionName: string;
|
||||
pushFields: boolean;
|
||||
theme: EditorTheme.LIGHT;
|
||||
datasourceParams: Property[];
|
||||
actionConfigurationParams: Property[];
|
||||
bodyUIComponent: React.ReactNode;
|
||||
paginationUiComponent: React.ReactNode;
|
||||
formName: string;
|
||||
showSettings: boolean;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actionSettingsConfig?: any;
|
||||
}) {
|
||||
const [value, onValueChange] = useSelectedFormTab();
|
||||
const headersCount = getHeadersCount(
|
||||
props.actionConfigurationHeaders,
|
||||
props.datasourceHeaders,
|
||||
props.autogeneratedHeaders,
|
||||
);
|
||||
|
||||
const paramsCount = getParamsCount(
|
||||
props.actionConfigurationParams,
|
||||
props.datasourceHeaders,
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
onValueChange={onValueChange}
|
||||
style={{
|
||||
height: "calc(100% - 36px)",
|
||||
overflow: "hidden",
|
||||
maxHeight: "unset",
|
||||
}}
|
||||
value={value}
|
||||
>
|
||||
<TabsList>
|
||||
{Object.values(API_EDITOR_TABS)
|
||||
.filter((tab) => {
|
||||
return !(!props.showSettings && tab === API_EDITOR_TABS.SETTINGS);
|
||||
})
|
||||
.map((tab) => (
|
||||
<Tab
|
||||
data-testid={`t--api-editor-${tab}`}
|
||||
key={tab}
|
||||
notificationCount={
|
||||
tab == "HEADERS"
|
||||
? headersCount
|
||||
: tab == "PARAMS"
|
||||
? paramsCount
|
||||
: undefined
|
||||
}
|
||||
value={tab}
|
||||
>
|
||||
{createMessage(API_EDITOR_TAB_TITLES[tab])}
|
||||
</Tab>
|
||||
))}
|
||||
</TabsList>
|
||||
<StyledTabPanel value={API_EDITOR_TABS.HEADERS}>
|
||||
<DatasourceConfig
|
||||
attributeName="header"
|
||||
autogeneratedHeaders={props.autogeneratedHeaders}
|
||||
data={props.datasourceHeaders}
|
||||
/>
|
||||
<KeyValueFieldArray
|
||||
actionConfig={props.actionConfigurationHeaders}
|
||||
dataTreePath={`${props.actionName}.config.headers`}
|
||||
hideHeader
|
||||
label="Headers"
|
||||
name="actionConfiguration.headers"
|
||||
placeholder="Value"
|
||||
pushFields={props.pushFields}
|
||||
theme={props.theme}
|
||||
/>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value={API_EDITOR_TABS.PARAMS}>
|
||||
<DatasourceConfig
|
||||
attributeName={"param"}
|
||||
data={props.datasourceParams}
|
||||
/>
|
||||
<KeyValueFieldArray
|
||||
actionConfig={props.actionConfigurationParams}
|
||||
dataTreePath={`${props.actionName}.config.queryParameters`}
|
||||
hideHeader
|
||||
label="Params"
|
||||
name="actionConfiguration.queryParameters"
|
||||
pushFields={props.pushFields}
|
||||
theme={props.theme}
|
||||
/>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel className="h-full" value={API_EDITOR_TABS.BODY}>
|
||||
{props.bodyUIComponent}
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value={API_EDITOR_TABS.PAGINATION}>
|
||||
{props.paginationUiComponent}
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value={API_EDITOR_TABS.AUTHENTICATION}>
|
||||
<ApiAuthentication formName={props.formName} />
|
||||
</StyledTabPanel>
|
||||
{props.showSettings ? (
|
||||
<StyledTabPanel value={API_EDITOR_TABS.SETTINGS}>
|
||||
<SettingsWrapper>
|
||||
<ActionSettings
|
||||
actionSettingsConfig={props.actionSettingsConfig}
|
||||
formName={props.formName}
|
||||
theme={props.theme}
|
||||
/>
|
||||
</SettingsWrapper>
|
||||
</StyledTabPanel>
|
||||
) : null}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +1,4 @@
|
|||
export const sortedDatasourcesHandler = (
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
datasources: Record<string, any>,
|
||||
currentDatasourceId: string,
|
||||
) => {
|
||||
// this function sorts the datasources list, with the current action's datasource first, followed by others.
|
||||
let sortedArr = [];
|
||||
|
||||
sortedArr = datasources.filter(
|
||||
(d: { id: string }) => d?.id === currentDatasourceId,
|
||||
);
|
||||
|
||||
sortedArr = [
|
||||
...sortedArr,
|
||||
...datasources.filter((d: { id: string }) => d?.id !== currentDatasourceId),
|
||||
];
|
||||
|
||||
return sortedArr;
|
||||
};
|
||||
|
||||
export interface AutoGeneratedHeader {
|
||||
key: string;
|
||||
value: string;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
import type { AutoGeneratedHeader } from "entities/Action";
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState } from "react";
|
|||
import { isArray, isString } from "lodash";
|
||||
import { isHtml } from "../utils";
|
||||
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
|
||||
import { SegmentedControlContainer } from "pages/Editor/QueryEditor/EditorJSONtoForm";
|
||||
import { Flex, SegmentedControl } from "@appsmith/ads";
|
||||
import type { ActionResponse } from "api/ActionAPI";
|
||||
import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
|
||||
|
|
@ -12,6 +11,16 @@ import { useDispatch } from "react-redux";
|
|||
import styled from "styled-components";
|
||||
import { ResponseFormatTabs } from "./ResponseFormatTabs";
|
||||
|
||||
const SegmentedControlContainer = styled.div`
|
||||
padding: 0 var(--ads-v2-spaces-7);
|
||||
padding-top: var(--ads-v2-spaces-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ads-v2-spaces-4);
|
||||
overflow-y: clip;
|
||||
overflow-x: scroll;
|
||||
`;
|
||||
|
||||
const ResponseBodyContainer = styled.div`
|
||||
overflow-y: clip;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import pluralize from "pluralize";
|
|||
import { Callout, Tooltip, type CalloutLinkProps } from "@appsmith/ads";
|
||||
|
||||
import type { ActionResponse } from "api/ActionAPI";
|
||||
import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView";
|
||||
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { type Action } from "entities/Action";
|
||||
import { PluginType } from "entities/Plugin";
|
||||
|
|
@ -16,14 +15,7 @@ import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
|
|||
import { actionResponseDisplayDataFormats } from "pages/Editor/utils";
|
||||
import { scrollbarWidth } from "utils/helpers";
|
||||
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import {
|
||||
openPluginActionSettings,
|
||||
setPluginActionEditorSelectedTab,
|
||||
} from "PluginActionEditor/store";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
|
||||
import { EDITOR_TABS } from "constants/QueryEditorConstants";
|
||||
import { openPluginActionSettings } from "../../../../store";
|
||||
import {
|
||||
createMessage,
|
||||
PREPARED_STATEMENT_WARNING,
|
||||
|
|
@ -39,6 +31,7 @@ import { RESPONSE_TABLE_HEIGHT_OFFSET } from "./constants";
|
|||
|
||||
import * as Styled from "./styles";
|
||||
import { checkForPreparedStatement, parseActionResponse } from "./utils";
|
||||
import ActionExecutionInProgressView from "./components/ActionExecutionInProgressView";
|
||||
|
||||
interface ResponseProps {
|
||||
action: Action;
|
||||
|
|
@ -51,10 +44,6 @@ interface ResponseProps {
|
|||
}
|
||||
|
||||
export function Response(props: ResponseProps) {
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
const {
|
||||
action,
|
||||
actionResponse,
|
||||
|
|
@ -148,11 +137,7 @@ export function Response(props: ResponseProps) {
|
|||
|
||||
const preparedStatementCalloutLinks: CalloutLinkProps[] = useMemo(() => {
|
||||
const navigateToSettings = () => {
|
||||
if (isActionRedesignEnabled) {
|
||||
dispatch(openPluginActionSettings(true));
|
||||
} else {
|
||||
dispatch(setPluginActionEditorSelectedTab(EDITOR_TABS.SETTINGS));
|
||||
}
|
||||
dispatch(openPluginActionSettings(true));
|
||||
};
|
||||
|
||||
return [
|
||||
|
|
@ -161,7 +146,7 @@ export function Response(props: ResponseProps) {
|
|||
children: createMessage(PREPARED_STATEMENT_WARNING.LINK),
|
||||
},
|
||||
];
|
||||
}, [dispatch, isActionRedesignEnabled]);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleContentTypeChange = useEventCallback((e?: Event) => {
|
||||
if (e?.target && e.target instanceof HTMLElement) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import {
|
|||
import ActionAPI from "api/ActionAPI";
|
||||
import { Button, Spinner, Text } from "@appsmith/ads";
|
||||
import styled from "styled-components";
|
||||
import type { EditorTheme } from "./CodeEditor/EditorConfig";
|
||||
import LoadingOverlayScreen from "./LoadingOverlayScreen";
|
||||
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "@appsmith/ads";
|
||||
import ActionSettings from "pages/Editor/ActionSettings";
|
||||
import ActionSettings from "../PluginActionToolbar/components/ActionSettings";
|
||||
import { usePluginActionContext } from "../../PluginActionContext";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React from "react";
|
||||
import type { ControlProps } from "components/formControls/BaseControl";
|
||||
import FormControl from "./FormControl";
|
||||
import FormControl from "pages/Editor/FormControl";
|
||||
import log from "loglevel";
|
||||
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import styled from "styled-components";
|
||||
import { Text } from "@appsmith/ads";
|
||||
import CenteredWrapper from "../../components/designSystems/appsmith/CenteredWrapper";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
|
||||
interface ActionSettingsProps {
|
||||
// TODO: Fix this the next time the file is edited
|
||||
|
|
@ -26,7 +26,6 @@ export const FEATURE_FLAG = {
|
|||
"ab_one_click_learning_popover_enabled",
|
||||
release_side_by_side_ide_enabled: "release_side_by_side_ide_enabled",
|
||||
ab_appsmith_ai_query: "ab_appsmith_ai_query",
|
||||
release_actions_redesign_enabled: "release_actions_redesign_enabled",
|
||||
rollout_remove_feature_walkthrough_enabled:
|
||||
"rollout_remove_feature_walkthrough_enabled",
|
||||
rollout_eslint_enabled: "rollout_eslint_enabled",
|
||||
|
|
@ -82,7 +81,6 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
|||
ab_one_click_learning_popover_enabled: false,
|
||||
release_side_by_side_ide_enabled: false,
|
||||
ab_appsmith_ai_query: false,
|
||||
release_actions_redesign_enabled: false,
|
||||
rollout_remove_feature_walkthrough_enabled: true,
|
||||
rollout_eslint_enabled: false,
|
||||
rollout_side_by_side_enabled: false,
|
||||
|
|
|
|||
|
|
@ -154,13 +154,6 @@ const PluginActionEditor = lazy(async () =>
|
|||
),
|
||||
);
|
||||
|
||||
const ApiEditor = lazy(async () =>
|
||||
retryPromise(
|
||||
async () =>
|
||||
import(/* webpackChunkName: "APIEditor" */ "pages/Editor/APIEditor"),
|
||||
),
|
||||
);
|
||||
|
||||
const AddQuery = lazy(async () =>
|
||||
retryPromise(
|
||||
async () =>
|
||||
|
|
@ -169,30 +162,20 @@ const AddQuery = lazy(async () =>
|
|||
),
|
||||
),
|
||||
);
|
||||
const QueryEditor = lazy(async () =>
|
||||
retryPromise(
|
||||
async () =>
|
||||
import(/* webpackChunkName: "QueryEditor" */ "pages/Editor/QueryEditor"),
|
||||
),
|
||||
);
|
||||
|
||||
const QueryEmpty = lazy(async () =>
|
||||
retryPromise(
|
||||
async () =>
|
||||
import(
|
||||
/* webpackChunkName: "QueryEmpty" */ "pages/Editor/QueryEditor/QueriesBlankState"
|
||||
/* webpackChunkName: "QueryEmpty" */ "../../../../../../PluginActionEditor/components/PluginActionForm/components/UQIEditor/QueriesBlankState"
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
export const useQueryEditorRoutes = (path: string): UseRoutes => {
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
const skeleton = useMemo(() => <Skeleton />, []);
|
||||
|
||||
const newComponents = useMemo(
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "AddQuery",
|
||||
|
|
@ -237,82 +220,6 @@ export const useQueryEditorRoutes = (path: string): UseRoutes => {
|
|||
],
|
||||
[path, skeleton],
|
||||
);
|
||||
|
||||
const oldComponents = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "ApiEditor",
|
||||
component: (args: object) => {
|
||||
return (
|
||||
<Suspense fallback={skeleton}>
|
||||
<ApiEditor {...args} />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
exact: true,
|
||||
path: [
|
||||
BUILDER_PATH + API_EDITOR_ID_PATH,
|
||||
BUILDER_CUSTOM_PATH + API_EDITOR_ID_PATH,
|
||||
BUILDER_PATH_DEPRECATED + API_EDITOR_ID_PATH,
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "AddQuery",
|
||||
exact: true,
|
||||
component: () => (
|
||||
<Suspense fallback={skeleton}>
|
||||
<AddQuery />
|
||||
</Suspense>
|
||||
),
|
||||
path: [`${path}${ADD_PATH}`, `${path}/:baseQueryId${ADD_PATH}`],
|
||||
},
|
||||
{
|
||||
key: "SAASEditor",
|
||||
component: (args: object) => {
|
||||
return (
|
||||
<Suspense fallback={skeleton}>
|
||||
<QueryEditor {...args} />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
exact: true,
|
||||
path: [
|
||||
BUILDER_PATH + SAAS_EDITOR_API_ID_PATH,
|
||||
BUILDER_CUSTOM_PATH + SAAS_EDITOR_API_ID_PATH,
|
||||
BUILDER_PATH_DEPRECATED + SAAS_EDITOR_API_ID_PATH,
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "QueryEditor",
|
||||
component: (args: object) => {
|
||||
return (
|
||||
<Suspense fallback={skeleton}>
|
||||
<QueryEditor {...args} />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
exact: true,
|
||||
path: [path + "/:baseQueryId"],
|
||||
},
|
||||
{
|
||||
key: "QueryEmpty",
|
||||
component: () => (
|
||||
<Suspense fallback={skeleton}>
|
||||
<QueryEmpty />
|
||||
</Suspense>
|
||||
),
|
||||
exact: true,
|
||||
path: [path],
|
||||
},
|
||||
],
|
||||
[path, skeleton],
|
||||
);
|
||||
|
||||
if (isActionRedesignEnabled) {
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
return oldComponents;
|
||||
};
|
||||
|
||||
export const useAddQueryListItems = () => {
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
import React, { memo } from "react";
|
||||
|
||||
import EditableText, {
|
||||
EditInteractionKind,
|
||||
} from "components/editorComponents/EditableText";
|
||||
import { removeSpecialChars } from "utils/helpers";
|
||||
|
||||
import { Flex } from "@appsmith/ads";
|
||||
import NameEditorComponent, {
|
||||
IconBox,
|
||||
NameWrapper,
|
||||
} from "components/utils/NameEditorComponent";
|
||||
import {
|
||||
ACTION_ID_NOT_FOUND_IN_URL,
|
||||
ACTION_NAME_PLACEHOLDER,
|
||||
createMessage,
|
||||
} from "ee/constants/messages";
|
||||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
import type { Action } from "entities/Action";
|
||||
import type { ModuleInstance } from "ee/constants/ModuleInstanceConstants";
|
||||
|
||||
interface ActionNameEditorProps {
|
||||
/*
|
||||
This prop checks if page is API Pane or Query Pane or Curl Pane
|
||||
So, that we can toggle between ads editable-text component and existing editable-text component
|
||||
Right now, it's optional so that it doesn't impact any other pages other than API Pane.
|
||||
In future, when default component will be ads editable-text, then we can remove this prop.
|
||||
*/
|
||||
enableFontStyling?: boolean;
|
||||
disabled?: boolean;
|
||||
saveActionName: (
|
||||
params: SaveActionNameParams,
|
||||
) => ReduxAction<SaveActionNameParams>;
|
||||
actionConfig?: Action | ModuleInstance;
|
||||
icon?: JSX.Element;
|
||||
saveStatus: { isSaving: boolean; error: boolean };
|
||||
}
|
||||
|
||||
function ActionNameEditor(props: ActionNameEditorProps) {
|
||||
const {
|
||||
actionConfig,
|
||||
disabled = false,
|
||||
enableFontStyling = false,
|
||||
icon = "",
|
||||
saveActionName,
|
||||
saveStatus,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<NameEditorComponent
|
||||
id={actionConfig?.id}
|
||||
idUndefinedErrorMessage={ACTION_ID_NOT_FOUND_IN_URL}
|
||||
name={actionConfig?.name}
|
||||
onSaveName={saveActionName}
|
||||
saveStatus={saveStatus}
|
||||
>
|
||||
{({
|
||||
forceUpdate,
|
||||
handleNameChange,
|
||||
isInvalidNameForEntity,
|
||||
isNew,
|
||||
saveStatus,
|
||||
}: {
|
||||
forceUpdate: boolean;
|
||||
handleNameChange: (value: string) => void;
|
||||
isInvalidNameForEntity: (value: string) => string | boolean;
|
||||
isNew: boolean;
|
||||
saveStatus: { isSaving: boolean; error: boolean };
|
||||
}) => (
|
||||
<NameWrapper enableFontStyling={enableFontStyling}>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
gap="spaces-3"
|
||||
overflow="hidden"
|
||||
width="100%"
|
||||
>
|
||||
{icon && <IconBox className="t--plugin-icon-box">{icon}</IconBox>}
|
||||
<EditableText
|
||||
className="t--action-name-edit-field"
|
||||
defaultValue={actionConfig ? actionConfig.name : ""}
|
||||
disabled={disabled}
|
||||
editInteractionKind={EditInteractionKind.SINGLE}
|
||||
errorTooltipClass="t--action-name-edit-error"
|
||||
forceDefault={forceUpdate}
|
||||
iconSize={"md"}
|
||||
isEditingDefault={isNew}
|
||||
isInvalid={isInvalidNameForEntity}
|
||||
onTextChanged={handleNameChange}
|
||||
placeholder={createMessage(ACTION_NAME_PLACEHOLDER, "Api")}
|
||||
type="text"
|
||||
underline
|
||||
updating={saveStatus.isSaving}
|
||||
valueTransform={removeSpecialChars}
|
||||
/>
|
||||
</Flex>
|
||||
</NameWrapper>
|
||||
)}
|
||||
</NameEditorComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ActionNameEditor);
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import { getTypographyByKey } from "@appsmith/ads-old";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { getDependenciesFromInverseDependencies } from "../Debugger/helpers";
|
||||
import {
|
||||
CollapsibleGroup,
|
||||
CollapsibleGroupContainer,
|
||||
} from "components/common/Collapsible";
|
||||
|
||||
const SideBar = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& > a {
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
display: flex;
|
||||
|
||||
.connection-type {
|
||||
${getTypographyByKey("p1")}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-text:nth-child(2) {
|
||||
padding-top: ${(props) => props.theme.spaces[7]}px;
|
||||
}
|
||||
|
||||
.description {
|
||||
${getTypographyByKey("p1")}
|
||||
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
|
||||
padding-bottom: ${(props) => props.theme.spaces[7]}px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-left {
|
||||
0% {
|
||||
-webkit-transform: translateX(100%);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes slide-left {
|
||||
0% {
|
||||
-webkit-transform: translateX(100%);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border-left: 1px solid var(--ads-v2-color-border);
|
||||
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-4);
|
||||
overflow: hidden;
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
width: ${(props) => props.theme.actionSidePane.width}px;
|
||||
margin-top: 10px;
|
||||
/* margin-left: var(--ads-v2-spaces-7); */
|
||||
`;
|
||||
|
||||
export function useEntityDependencies(actionName: string) {
|
||||
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
|
||||
const entityDependencies = useMemo(
|
||||
() =>
|
||||
getDependenciesFromInverseDependencies(
|
||||
deps.inverseDependencyMap,
|
||||
actionName,
|
||||
),
|
||||
[actionName, deps.inverseDependencyMap],
|
||||
);
|
||||
const hasDependencies =
|
||||
entityDependencies &&
|
||||
(entityDependencies?.directDependencies.length > 0 ||
|
||||
entityDependencies?.inverseDependencies.length > 0);
|
||||
|
||||
return {
|
||||
hasDependencies,
|
||||
entityDependencies,
|
||||
};
|
||||
}
|
||||
|
||||
function ActionSidebar({
|
||||
additionalSections,
|
||||
}: {
|
||||
additionalSections?: React.ReactNode;
|
||||
}) {
|
||||
if (!additionalSections) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<SideBar>
|
||||
<CollapsibleGroupContainer>
|
||||
{additionalSections && (
|
||||
<CollapsibleGroup height={"100%"}>
|
||||
{additionalSections}
|
||||
</CollapsibleGroup>
|
||||
)}
|
||||
</CollapsibleGroupContainer>
|
||||
</SideBar>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionSidebar;
|
||||
|
|
@ -20,7 +20,6 @@ import {
|
|||
import { actionPathFromName } from "components/formControls/utils";
|
||||
import type { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
|
||||
import { getSqlEditorModeFromPluginName } from "components/editorComponents/CodeEditor/sql/config";
|
||||
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
|
|
@ -110,14 +109,12 @@ const mapStateToProps = (state: AppState, props: DynamicTextFieldProps) => {
|
|||
const pluginId = valueSelector(state, "datasource.pluginId");
|
||||
const responseTypes = getPluginResponseTypes(state);
|
||||
const pluginName = getPluginNameFromId(state, pluginId);
|
||||
const { release_actions_redesign_enabled } = selectFeatureFlags(state);
|
||||
|
||||
return {
|
||||
actionName,
|
||||
pluginId,
|
||||
responseType: responseTypes[pluginId],
|
||||
pluginName,
|
||||
isActionRedesignEnabled: release_actions_redesign_enabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { EmbeddedRestDatasource } from "entities/Datasource";
|
|||
import type { DynamicPath } from "utils/DynamicBindingUtils";
|
||||
import _ from "lodash";
|
||||
import type { LayoutOnLoadActionErrors } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import type { AutoGeneratedHeader } from "pages/Editor/APIEditor/helpers";
|
||||
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
|
||||
import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers";
|
||||
import {
|
||||
|
|
@ -82,6 +81,12 @@ export interface BodyFormData {
|
|||
type: string;
|
||||
}
|
||||
|
||||
export interface AutoGeneratedHeader {
|
||||
key: string;
|
||||
value: string;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
export interface ApiActionConfig extends Omit<ActionConfig, "formData"> {
|
||||
headers: Property[];
|
||||
autoGeneratedHeaders?: AutoGeneratedHeader[];
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import type { PaginationField } from "api/ActionAPI";
|
||||
import React, { createContext, useMemo } from "react";
|
||||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
|
||||
interface ApiEditorContextContextProps {
|
||||
moreActionsMenu?: React.ReactNode;
|
||||
handleRunClick: (paginationField?: PaginationField) => void;
|
||||
actionRightPaneBackLink?: React.ReactNode;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settingsConfig: any;
|
||||
saveActionName: (
|
||||
params: SaveActionNameParams,
|
||||
) => ReduxAction<SaveActionNameParams>;
|
||||
showRightPaneTabbedSection?: boolean;
|
||||
actionRightPaneAdditionSections?: React.ReactNode;
|
||||
notification?: React.ReactNode | string;
|
||||
}
|
||||
|
||||
type ApiEditorContextProviderProps =
|
||||
React.PropsWithChildren<ApiEditorContextContextProps>;
|
||||
|
||||
export const ApiEditorContext = createContext<ApiEditorContextContextProps>(
|
||||
{} as ApiEditorContextContextProps,
|
||||
);
|
||||
|
||||
export function ApiEditorContextProvider({
|
||||
actionRightPaneAdditionSections,
|
||||
actionRightPaneBackLink,
|
||||
children,
|
||||
handleRunClick,
|
||||
moreActionsMenu,
|
||||
notification,
|
||||
saveActionName,
|
||||
settingsConfig,
|
||||
showRightPaneTabbedSection,
|
||||
}: ApiEditorContextProviderProps) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
actionRightPaneAdditionSections,
|
||||
actionRightPaneBackLink,
|
||||
showRightPaneTabbedSection,
|
||||
handleRunClick,
|
||||
moreActionsMenu,
|
||||
saveActionName,
|
||||
settingsConfig,
|
||||
notification,
|
||||
}),
|
||||
[
|
||||
actionRightPaneBackLink,
|
||||
actionRightPaneAdditionSections,
|
||||
showRightPaneTabbedSection,
|
||||
handleRunClick,
|
||||
moreActionsMenu,
|
||||
saveActionName,
|
||||
settingsConfig,
|
||||
notification,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<ApiEditorContext.Provider value={value}>
|
||||
{children}
|
||||
</ApiEditorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
import React, { useContext } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import FormLabel from "components/editorComponents/FormLabel";
|
||||
import FormRow from "components/editorComponents/FormRow";
|
||||
import type { ActionResponse, PaginationField } from "api/ActionAPI";
|
||||
import type { Action, PaginationType } from "entities/Action";
|
||||
import ApiResponseView from "components/editorComponents/ApiResponseView";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
|
||||
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { Button } from "@appsmith/ads";
|
||||
import { useParams } from "react-router";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { getPlugin } from "ee/selectors/entitiesSelector";
|
||||
import type { AutoGeneratedHeader } from "./helpers";
|
||||
import { noop } from "lodash";
|
||||
import { DEFAULT_DATASOURCE_NAME } from "PluginActionEditor/constants/ApiEditorConstants";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import {
|
||||
getHasExecuteActionPermission,
|
||||
getHasManageActionPermission,
|
||||
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { ApiEditorContext } from "./ApiEditorContext";
|
||||
import RunHistory from "ee/components/RunHistory";
|
||||
import { HintMessages } from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/HintMessages";
|
||||
import { InfoFields } from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/InfoFields";
|
||||
import { RequestTabs } from "PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/RequestTabs";
|
||||
import { getSavingStatusForActionName } from "selectors/actionSelectors";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
|
||||
|
||||
const Form = styled.form`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
${FormLabel} {
|
||||
padding: ${(props) => props.theme.spaces[3]}px;
|
||||
}
|
||||
|
||||
${FormRow} {
|
||||
align-items: center;
|
||||
|
||||
${FormLabel} {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.api-info-row {
|
||||
input {
|
||||
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MainConfiguration = styled.div`
|
||||
z-index: 7;
|
||||
padding: 0 var(--ads-v2-spaces-7);
|
||||
|
||||
.api-info-row {
|
||||
padding-top: var(--ads-v2-spaces-5);
|
||||
}
|
||||
|
||||
.form-row-header {
|
||||
padding-top: var(--ads-v2-spaces-5);
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButtons = styled.div`
|
||||
justify-self: flex-end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ads-v2-spaces-3);
|
||||
`;
|
||||
|
||||
const HelpSection = styled.div`
|
||||
padding: var(--ads-v2-spaces-4) var(--ads-v2-spaces-7);
|
||||
`;
|
||||
|
||||
const SecondaryWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const TabbedViewContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 var(--ads-v2-spaces-7);
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const MainContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
/* padding: var(--ads-v2-spaces-7); */
|
||||
`;
|
||||
|
||||
export interface CommonFormProps {
|
||||
actionResponse?: ActionResponse;
|
||||
pluginId: string;
|
||||
onRunClick: (paginationField?: PaginationField) => void;
|
||||
isRunning: boolean;
|
||||
isDeleting: boolean;
|
||||
paginationType: PaginationType;
|
||||
appName: string;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actionConfigurationHeaders?: any;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actionConfigurationParams?: any;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
datasourceHeaders?: any;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
datasourceParams?: any;
|
||||
actionName: string;
|
||||
apiName: string;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settingsConfig: any;
|
||||
hintMessages?: Array<string>;
|
||||
autoGeneratedActionConfigHeaders?: AutoGeneratedHeader[];
|
||||
}
|
||||
|
||||
type CommonFormPropsWithExtraParams = CommonFormProps & {
|
||||
formName: string;
|
||||
// Body Tab Component which is passed on from the Parent Component
|
||||
bodyUIComponent: JSX.Element;
|
||||
// Pagination Tab Component which is passed on from the Parent Component
|
||||
paginationUIComponent: JSX.Element;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handleSubmit: any;
|
||||
httpsMethods: { value: string }[];
|
||||
};
|
||||
|
||||
export const NameWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNotificationWrapper = styled.div`
|
||||
padding-top: var(--ads-v2-spaces-5);
|
||||
`;
|
||||
|
||||
/**
|
||||
* Commons editor form which is being used by API and GraphQL. Since most of the things were common to both so picking out the common part was a better option. For now Body and Pagination component are being passed on by the using component.
|
||||
* @param props type CommonFormPropsWithExtraParams
|
||||
* @returns Editor with respect to which type is using it
|
||||
*/
|
||||
function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
|
||||
const {
|
||||
actionRightPaneAdditionSections,
|
||||
moreActionsMenu,
|
||||
notification,
|
||||
saveActionName,
|
||||
} = useContext(ApiEditorContext);
|
||||
|
||||
const {
|
||||
actionConfigurationHeaders,
|
||||
actionConfigurationParams,
|
||||
actionResponse,
|
||||
autoGeneratedActionConfigHeaders,
|
||||
formName,
|
||||
handleSubmit,
|
||||
hintMessages,
|
||||
isRunning,
|
||||
onRunClick,
|
||||
pluginId,
|
||||
settingsConfig,
|
||||
} = props;
|
||||
|
||||
const params = useParams<{ baseApiId?: string; baseQueryId?: string }>();
|
||||
|
||||
// passing lodash's equality function to ensure that this selector does not cause a rerender multiple times.
|
||||
// it checks each value to make sure none has changed before recomputing the actions.
|
||||
const actions: Action[] = useSelector(
|
||||
(state: AppState) => state.entities.actions.map((action) => action.config),
|
||||
equal,
|
||||
);
|
||||
|
||||
const currentActionConfig: Action | undefined = actions.find(
|
||||
(action) =>
|
||||
action.baseId === params.baseApiId || action.id === params.baseQueryId,
|
||||
);
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
isFeatureEnabled,
|
||||
currentActionConfig?.userPermissions,
|
||||
);
|
||||
const isExecutePermitted = getHasExecuteActionPermission(
|
||||
isFeatureEnabled,
|
||||
currentActionConfig?.userPermissions,
|
||||
);
|
||||
|
||||
const currentPlugin = useSelector((state: AppState) =>
|
||||
getPlugin(state, currentActionConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
const saveStatus = useSelector((state) =>
|
||||
getSavingStatusForActionName(state, currentActionConfig?.id || ""),
|
||||
);
|
||||
|
||||
const iconUrl = getAssetUrl(currentPlugin?.iconLocation) || "";
|
||||
|
||||
const icon = ActionUrlIcon(iconUrl);
|
||||
|
||||
const plugin = useSelector((state: AppState) =>
|
||||
getPlugin(state, pluginId ?? ""),
|
||||
);
|
||||
|
||||
if (!currentActionConfig) return null;
|
||||
|
||||
// this gets the url of the current action's datasource
|
||||
const actionDatasourceUrl =
|
||||
currentActionConfig?.datasource?.datasourceConfiguration?.url || "";
|
||||
const actionDatasourceUrlPath =
|
||||
currentActionConfig?.actionConfiguration?.path || "";
|
||||
// this gets the name of the current action's datasource
|
||||
const actionDatasourceName = currentActionConfig?.datasource.name || "";
|
||||
|
||||
// if the url is empty and the action's datasource name is the default datasource name (this means the api does not have a datasource attached)
|
||||
// or the user does not have permission,
|
||||
// we block action execution.
|
||||
const blockExecution =
|
||||
(!actionDatasourceUrl &&
|
||||
!actionDatasourceUrlPath &&
|
||||
actionDatasourceName === DEFAULT_DATASOURCE_NAME) ||
|
||||
!isExecutePermitted;
|
||||
|
||||
const theme = EditorTheme.LIGHT;
|
||||
|
||||
return (
|
||||
<MainContainer>
|
||||
<Form
|
||||
data-testid={`t--action-form-${plugin?.type}`}
|
||||
onSubmit={handleSubmit(noop)}
|
||||
>
|
||||
<MainConfiguration>
|
||||
<FormRow className="form-row-header">
|
||||
<NameWrapper className="t--nameOfApi">
|
||||
<ActionNameEditor
|
||||
actionConfig={currentActionConfig}
|
||||
disabled={!isChangePermitted}
|
||||
enableFontStyling
|
||||
icon={icon}
|
||||
saveActionName={saveActionName}
|
||||
saveStatus={saveStatus}
|
||||
/>
|
||||
</NameWrapper>
|
||||
<ActionButtons className="t--formActionButtons">
|
||||
{moreActionsMenu}
|
||||
<Button
|
||||
className="t--apiFormRunBtn"
|
||||
isDisabled={blockExecution}
|
||||
isLoading={isRunning}
|
||||
onClick={() => {
|
||||
onRunClick();
|
||||
}}
|
||||
size="md"
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</ActionButtons>
|
||||
</FormRow>
|
||||
{notification && (
|
||||
<StyledNotificationWrapper>
|
||||
{notification}
|
||||
</StyledNotificationWrapper>
|
||||
)}
|
||||
<FormRow className="api-info-row">
|
||||
<InfoFields
|
||||
actionName={props.actionName}
|
||||
changePermitted={isChangePermitted}
|
||||
formName={props.formName}
|
||||
options={props.httpsMethods}
|
||||
pluginId={props.pluginId}
|
||||
theme={EditorTheme.LIGHT}
|
||||
/>
|
||||
</FormRow>
|
||||
</MainConfiguration>
|
||||
{hintMessages && (
|
||||
<HelpSection>
|
||||
<HintMessages hintMessages={hintMessages} />
|
||||
</HelpSection>
|
||||
)}
|
||||
<Wrapper>
|
||||
<div className="flex flex-1">
|
||||
<SecondaryWrapper>
|
||||
<TabbedViewContainer>
|
||||
<RequestTabs
|
||||
actionConfigurationHeaders={actionConfigurationHeaders}
|
||||
actionConfigurationParams={actionConfigurationParams}
|
||||
actionName={props.actionName}
|
||||
actionSettingsConfig={settingsConfig}
|
||||
autogeneratedHeaders={autoGeneratedActionConfigHeaders}
|
||||
bodyUIComponent={props.bodyUIComponent}
|
||||
datasourceHeaders={props.datasourceHeaders}
|
||||
datasourceParams={props.datasourceParams}
|
||||
formName={formName}
|
||||
paginationUiComponent={props.paginationUIComponent}
|
||||
pushFields={isChangePermitted}
|
||||
showSettings
|
||||
theme={EditorTheme.LIGHT}
|
||||
/>
|
||||
</TabbedViewContainer>
|
||||
<ApiResponseView
|
||||
actionResponse={actionResponse}
|
||||
currentActionConfig={currentActionConfig}
|
||||
isRunDisabled={blockExecution}
|
||||
isRunning={isRunning}
|
||||
onRunClick={onRunClick}
|
||||
theme={theme}
|
||||
/>
|
||||
<RunHistory />
|
||||
</SecondaryWrapper>
|
||||
</div>
|
||||
{actionRightPaneAdditionSections}
|
||||
</Wrapper>
|
||||
</Form>
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommonEditorForm;
|
||||
|
|
@ -1,276 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { submit } from "redux-form";
|
||||
import RestApiEditorForm from "./RestAPIForm";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type {
|
||||
ActionData,
|
||||
ActionDataState,
|
||||
} from "ee/reducers/entityReducers/actionsReducer";
|
||||
import _ from "lodash";
|
||||
import { getCurrentApplication } from "ee/selectors/applicationSelectors";
|
||||
import {
|
||||
getCurrentApplicationId,
|
||||
getCurrentPageName,
|
||||
} from "selectors/editorSelectors";
|
||||
import { type Plugin, PluginPackageName } from "entities/Plugin";
|
||||
import type { Action, PaginationType } from "entities/Action";
|
||||
import Spinner from "components/editorComponents/Spinner";
|
||||
import type { CSSProperties } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
import {
|
||||
changeApi,
|
||||
isActionDeleting,
|
||||
isActionRunning,
|
||||
isPluginActionCreating,
|
||||
} from "PluginActionEditor/store";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
|
||||
import type { ApplicationPayload } from "entities/Application";
|
||||
import {
|
||||
getActionByBaseId,
|
||||
getPageList,
|
||||
getPlugins,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import history from "utils/history";
|
||||
import { saasEditorApiIdURL } from "ee/RouteBuilder";
|
||||
import GraphQLEditorForm from "./GraphQL/GraphQLEditorForm";
|
||||
import type { APIEditorRouteParams } from "constants/routes";
|
||||
import { ApiEditorContext } from "./ApiEditorContext";
|
||||
|
||||
const LoadingContainer = styled(CenteredWrapper)`
|
||||
height: 50%;
|
||||
`;
|
||||
|
||||
interface ReduxStateProps {
|
||||
actions: ActionDataState;
|
||||
isRunning: boolean;
|
||||
isDeleting: boolean;
|
||||
isCreating: boolean;
|
||||
apiId: string;
|
||||
apiName: string;
|
||||
currentApplication?: ApplicationPayload;
|
||||
currentPageName: string | undefined;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pages: any;
|
||||
plugins: Plugin[];
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pluginId: any;
|
||||
apiAction: Action | ActionData | undefined;
|
||||
paginationType: PaginationType;
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
isEditorInitialized: boolean;
|
||||
}
|
||||
|
||||
interface ReduxActionProps {
|
||||
submitForm: (name: string) => void;
|
||||
changeAPIPage: (apiId: string, isSaas: boolean) => void;
|
||||
}
|
||||
|
||||
function getPackageNameFromPluginId(pluginId: string, plugins: Plugin[]) {
|
||||
const plugin = plugins.find((plugin: Plugin) => plugin.id === pluginId);
|
||||
|
||||
return plugin?.packageName;
|
||||
}
|
||||
|
||||
type Props = ReduxActionProps &
|
||||
ReduxStateProps &
|
||||
RouteComponentProps<APIEditorRouteParams> &
|
||||
OwnProps;
|
||||
|
||||
class ApiEditor extends React.Component<Props> {
|
||||
static contextType = ApiEditorContext;
|
||||
context!: React.ContextType<typeof ApiEditorContext>;
|
||||
|
||||
componentDidMount() {
|
||||
const type = this.getFormName();
|
||||
|
||||
if (this.props.apiId) {
|
||||
this.props.changeAPIPage(this.props.apiId, type === "SAAS");
|
||||
}
|
||||
}
|
||||
|
||||
getFormName = () => {
|
||||
const plugins = this.props.plugins;
|
||||
const pluginId = this.props.pluginId;
|
||||
const plugin =
|
||||
plugins &&
|
||||
plugins.find((plug) => {
|
||||
if (plug.id === pluginId) return plug;
|
||||
});
|
||||
|
||||
return plugin && plugin.type;
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.apiId !== this.props.apiId) {
|
||||
const type = this.getFormName();
|
||||
|
||||
this.props.changeAPIPage(this.props.apiId || "", type === "SAAS");
|
||||
}
|
||||
}
|
||||
|
||||
getPluginUiComponentOfId = (
|
||||
id: string,
|
||||
plugins: Plugin[],
|
||||
): string | undefined => {
|
||||
const plugin = plugins.find((plugin) => plugin.id === id);
|
||||
|
||||
if (!plugin) return undefined;
|
||||
|
||||
return plugin.uiComponent;
|
||||
};
|
||||
|
||||
getPluginUiComponentOfName = (plugins: Plugin[]): string | undefined => {
|
||||
const plugin = plugins.find(
|
||||
(plugin) => plugin.packageName === PluginPackageName.REST_API,
|
||||
);
|
||||
|
||||
if (!plugin) return undefined;
|
||||
|
||||
return plugin.uiComponent;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isCreating,
|
||||
isDeleting,
|
||||
isEditorInitialized,
|
||||
isRunning,
|
||||
match: {
|
||||
params: { baseApiId },
|
||||
},
|
||||
paginationType,
|
||||
pluginId,
|
||||
plugins,
|
||||
} = this.props;
|
||||
|
||||
if (!pluginId && baseApiId) {
|
||||
return <EntityNotFoundPane />;
|
||||
}
|
||||
|
||||
if (isCreating || !isEditorInitialized) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Spinner size={30} />
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
|
||||
let formUiComponent: string | undefined;
|
||||
|
||||
if (baseApiId) {
|
||||
if (pluginId) {
|
||||
formUiComponent = this.getPluginUiComponentOfId(pluginId, plugins);
|
||||
} else {
|
||||
formUiComponent = this.getPluginUiComponentOfName(plugins);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={formStyles}>
|
||||
{formUiComponent === "ApiEditorForm" && (
|
||||
<RestApiEditorForm
|
||||
apiName={this.props.apiName}
|
||||
appName={
|
||||
this.props.currentApplication
|
||||
? this.props.currentApplication.name
|
||||
: ""
|
||||
}
|
||||
isDeleting={isDeleting}
|
||||
isRunning={isRunning}
|
||||
onRunClick={this.context.handleRunClick}
|
||||
paginationType={paginationType}
|
||||
pluginId={pluginId}
|
||||
settingsConfig={this.context.settingsConfig}
|
||||
/>
|
||||
)}
|
||||
{formUiComponent === "GraphQLEditorForm" && (
|
||||
<GraphQLEditorForm
|
||||
apiName={this.props.apiName}
|
||||
appName={
|
||||
this.props.currentApplication
|
||||
? this.props.currentApplication.name
|
||||
: ""
|
||||
}
|
||||
isDeleting={isDeleting}
|
||||
isRunning={isRunning}
|
||||
match={this.props.match}
|
||||
onRunClick={this.context.handleRunClick}
|
||||
paginationType={paginationType}
|
||||
pluginId={pluginId}
|
||||
settingsConfig={this.context.settingsConfig}
|
||||
/>
|
||||
)}
|
||||
{formUiComponent === "SaaSEditorForm" &&
|
||||
history.push(
|
||||
saasEditorApiIdURL({
|
||||
basePageId: this.props.match.params.basePageId,
|
||||
pluginPackageName:
|
||||
getPackageNameFromPluginId(
|
||||
this.props.pluginId,
|
||||
this.props.plugins,
|
||||
) ?? "",
|
||||
baseApiId: this.props.match.params.baseApiId || "",
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const formStyles: CSSProperties = {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: "1",
|
||||
overflow: "auto",
|
||||
};
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
|
||||
const apiAction = getActionByBaseId(state, props?.match?.params?.baseApiId);
|
||||
const apiName = apiAction?.name ?? "";
|
||||
const apiId = apiAction?.id ?? "";
|
||||
const isCreating = isPluginActionCreating(state);
|
||||
const isDeleting = isActionDeleting(apiId)(state);
|
||||
const isRunning = isActionRunning(apiId)(state);
|
||||
const pluginId = _.get(apiAction, "pluginId", "");
|
||||
|
||||
return {
|
||||
actions: state.entities.actions,
|
||||
currentApplication: getCurrentApplication(state),
|
||||
currentPageName: getCurrentPageName(state),
|
||||
pages: getPageList(state),
|
||||
apiId,
|
||||
apiName,
|
||||
plugins: getPlugins(state),
|
||||
pluginId,
|
||||
paginationType: _.get(apiAction, "actionConfiguration.paginationType"),
|
||||
apiAction,
|
||||
isRunning,
|
||||
isDeleting,
|
||||
isCreating,
|
||||
applicationId: getCurrentApplicationId(state),
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({
|
||||
submitForm: (name: string) => dispatch(submit(name)),
|
||||
changeAPIPage: (actionId: string, isSaas: boolean) =>
|
||||
dispatch(changeApi(actionId, isSaas)),
|
||||
});
|
||||
|
||||
export default Sentry.withProfiler(
|
||||
connect(mapStateToProps, mapDispatchToProps)(ApiEditor),
|
||||
);
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import type { InjectedFormProps } from "redux-form";
|
||||
import { formValueSelector, reduxForm } from "redux-form";
|
||||
import { API_EDITOR_FORM_NAME } from "ee/constants/forms";
|
||||
import type { Action } from "entities/Action";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import get from "lodash/get";
|
||||
import {
|
||||
getActionByBaseId,
|
||||
getActionData,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import type { CommonFormProps } from "../CommonEditorForm";
|
||||
import CommonEditorForm from "../CommonEditorForm";
|
||||
import Pagination from "PluginActionEditor/components/PluginActionForm/components/GraphQLEditor/Pagination";
|
||||
import { GRAPHQL_HTTP_METHOD_OPTIONS } from "PluginActionEditor/constants/GraphQLEditorConstants";
|
||||
import PostBodyData from "PluginActionEditor/components/PluginActionForm/components/GraphQLEditor/PostBodyData";
|
||||
|
||||
type APIFormProps = {
|
||||
actionConfigurationBody: string;
|
||||
} & CommonFormProps;
|
||||
|
||||
type Props = APIFormProps & InjectedFormProps<Action, APIFormProps>;
|
||||
|
||||
/**
|
||||
* Graphql Editor form which uses the Common Editor and pass on the differentiating components from the API Editor.
|
||||
* @param props using type Props
|
||||
* @returns Graphql Editor Area which is used to editor APIs using GraphQL datasource.
|
||||
*/
|
||||
function GraphQLEditorForm(props: Props) {
|
||||
const { actionName } = props;
|
||||
|
||||
return (
|
||||
<CommonEditorForm
|
||||
{...props}
|
||||
bodyUIComponent={<PostBodyData actionName={actionName} />}
|
||||
formName={API_EDITOR_FORM_NAME}
|
||||
httpsMethods={GRAPHQL_HTTP_METHOD_OPTIONS}
|
||||
paginationUIComponent={
|
||||
<Pagination
|
||||
actionName={actionName}
|
||||
formName={API_EDITOR_FORM_NAME}
|
||||
paginationType={props.paginationType}
|
||||
query={props.actionConfigurationBody}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const selector = formValueSelector(API_EDITOR_FORM_NAME);
|
||||
|
||||
export default connect(
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(state: AppState, props: { pluginId: string; match?: any }) => {
|
||||
const actionConfigurationHeaders =
|
||||
selector(state, "actionConfiguration.headers") || [];
|
||||
const actionConfigurationParams =
|
||||
selector(state, "actionConfiguration.queryParameters") || [];
|
||||
let datasourceFromAction = selector(state, "datasource");
|
||||
|
||||
if (datasourceFromAction && datasourceFromAction.hasOwnProperty("id")) {
|
||||
datasourceFromAction = state.entities.datasources.list.find(
|
||||
(d) => d.id === datasourceFromAction.id,
|
||||
);
|
||||
}
|
||||
|
||||
const { baseApiId, baseQueryId } = props.match?.params || {};
|
||||
const baseActionId = baseQueryId || baseApiId;
|
||||
const action = getActionByBaseId(state, baseActionId);
|
||||
const apiId = action?.id ?? "";
|
||||
const actionName = action?.name ?? "";
|
||||
const hintMessages = action?.messages;
|
||||
|
||||
const datasourceHeaders =
|
||||
get(datasourceFromAction, "datasourceConfiguration.headers") || [];
|
||||
const datasourceParams =
|
||||
get(datasourceFromAction, "datasourceConfiguration.queryParameters") ||
|
||||
[];
|
||||
|
||||
const actionConfigurationBody =
|
||||
selector(state, "actionConfiguration.body") || "";
|
||||
|
||||
const actionResponse = getActionData(state, apiId);
|
||||
|
||||
return {
|
||||
actionName,
|
||||
actionResponse,
|
||||
actionConfigurationHeaders,
|
||||
actionConfigurationParams,
|
||||
actionConfigurationBody,
|
||||
datasourceHeaders,
|
||||
datasourceParams,
|
||||
hintMessages,
|
||||
};
|
||||
},
|
||||
)(
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
reduxForm<Action, any>({
|
||||
form: API_EDITOR_FORM_NAME,
|
||||
enableReinitialize: true,
|
||||
})(GraphQLEditorForm),
|
||||
);
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import type { InjectedFormProps } from "redux-form";
|
||||
import { formValueSelector, reduxForm } from "redux-form";
|
||||
import { API_EDITOR_FORM_NAME } from "ee/constants/forms";
|
||||
import type { Action } from "entities/Action";
|
||||
import PostBodyData from "PluginActionEditor/components/PluginActionForm/components/ApiEditor/PostBodyData";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { getApiName } from "selectors/formSelectors";
|
||||
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import get from "lodash/get";
|
||||
import { getAction, getActionResponses } from "ee/selectors/entitiesSelector";
|
||||
import type { CommonFormProps } from "./CommonEditorForm";
|
||||
import CommonEditorForm from "./CommonEditorForm";
|
||||
import Pagination from "PluginActionEditor/components/PluginActionForm/components/ApiEditor/Pagination";
|
||||
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
|
||||
import { HTTP_METHOD_OPTIONS } from "PluginActionEditor/constants/CommonApiConstants";
|
||||
|
||||
type APIFormProps = {
|
||||
httpMethodFromForm: string;
|
||||
} & CommonFormProps;
|
||||
|
||||
type Props = APIFormProps & InjectedFormProps<Action, APIFormProps>;
|
||||
|
||||
function ApiEditorForm(props: Props) {
|
||||
const { actionName } = props;
|
||||
const theme = EditorTheme.LIGHT;
|
||||
|
||||
return (
|
||||
<CommonEditorForm
|
||||
{...props}
|
||||
bodyUIComponent={
|
||||
<PostBodyData dataTreePath={`${actionName}.config`} theme={theme} />
|
||||
}
|
||||
formName={API_EDITOR_FORM_NAME}
|
||||
httpsMethods={HTTP_METHOD_OPTIONS}
|
||||
paginationUIComponent={
|
||||
<Pagination
|
||||
actionName={actionName}
|
||||
onTestClick={props.onRunClick}
|
||||
paginationType={props.paginationType}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const selector = formValueSelector(API_EDITOR_FORM_NAME);
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod");
|
||||
const actionConfigurationHeaders =
|
||||
selector(state, "actionConfiguration.headers") || [];
|
||||
const autoGeneratedActionConfigHeaders =
|
||||
selector(state, "actionConfiguration.autoGeneratedHeaders") || [];
|
||||
const actionConfigurationParams =
|
||||
selector(state, "actionConfiguration.queryParameters") || [];
|
||||
let datasourceFromAction = selector(state, "datasource");
|
||||
|
||||
if (datasourceFromAction && datasourceFromAction.hasOwnProperty("id")) {
|
||||
datasourceFromAction = state.entities.datasources.list.find(
|
||||
(d) => d.id === datasourceFromAction.id,
|
||||
);
|
||||
}
|
||||
|
||||
// get messages from action itself
|
||||
const actionId = selector(state, "id");
|
||||
const action = getAction(state, actionId);
|
||||
const currentEnvironment = getCurrentEnvironmentId(state);
|
||||
const hintMessages = action?.messages;
|
||||
|
||||
const datasourceHeaders =
|
||||
get(
|
||||
datasourceFromAction,
|
||||
`datasourceStorages.${currentEnvironment}.datasourceConfiguration.headers`,
|
||||
) || [];
|
||||
const datasourceParams =
|
||||
get(
|
||||
datasourceFromAction,
|
||||
`datasourceStorages.${currentEnvironment}.datasourceConfiguration.queryParameters`,
|
||||
) || [];
|
||||
|
||||
const apiId = selector(state, "id");
|
||||
const currentActionDatasourceId = selector(state, "datasource.id");
|
||||
|
||||
const actionName = getApiName(state, apiId) || "";
|
||||
|
||||
const responses = getActionResponses(state);
|
||||
const actionResponse = responses[apiId];
|
||||
|
||||
return {
|
||||
actionName,
|
||||
actionResponse,
|
||||
apiId,
|
||||
httpMethodFromForm,
|
||||
actionConfigurationHeaders,
|
||||
actionConfigurationParams,
|
||||
autoGeneratedActionConfigHeaders,
|
||||
currentActionDatasourceId,
|
||||
datasourceHeaders,
|
||||
datasourceParams,
|
||||
hintMessages,
|
||||
};
|
||||
})(
|
||||
reduxForm<Action, APIFormProps>({
|
||||
form: API_EDITOR_FORM_NAME,
|
||||
enableReinitialize: true,
|
||||
})(ApiEditorForm),
|
||||
);
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
import React, { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
|
||||
import {
|
||||
getIsActionConverting,
|
||||
getPageList,
|
||||
getPluginSettingConfigs,
|
||||
getPlugins,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { runAction, saveActionName } from "actions/pluginActionActions";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import Editor from "./Editor";
|
||||
import BackToCanvas from "components/common/BackToCanvas";
|
||||
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
|
||||
import {
|
||||
getIsEditorInitialized,
|
||||
getPagePermissions,
|
||||
} from "selectors/editorSelectors";
|
||||
import { getActionByBaseId } from "ee/selectors/entitiesSelector";
|
||||
import type { APIEditorRouteParams } from "constants/routes";
|
||||
import {
|
||||
getHasCreateActionPermission,
|
||||
getHasDeleteActionPermission,
|
||||
getHasManageActionPermission,
|
||||
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { ApiEditorContextProvider } from "./ApiEditorContext";
|
||||
import type { PaginationField } from "api/ActionAPI";
|
||||
import { get, keyBy } from "lodash";
|
||||
import ConvertToModuleInstanceCTA from "ee/pages/Editor/EntityEditor/ConvertToModuleInstanceCTA";
|
||||
import { MODULE_TYPE } from "ee/constants/ModuleConstants";
|
||||
import Disabler from "pages/common/Disabler";
|
||||
import ConvertEntityNotification from "ee/pages/common/ConvertEntityNotification";
|
||||
import { Icon } from "@appsmith/ads";
|
||||
import { resolveIcon } from "../utils";
|
||||
import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
|
||||
import { getIDEViewMode } from "selectors/ideSelectors";
|
||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||
|
||||
type ApiEditorWrapperProps = RouteComponentProps<APIEditorRouteParams>;
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getPageName(pages: any, basePageId: string) {
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const page = pages.find((page: any) => page.basePageId === basePageId);
|
||||
|
||||
return page ? page.pageName : "";
|
||||
}
|
||||
|
||||
function ApiEditorWrapper(props: ApiEditorWrapperProps) {
|
||||
const { baseApiId = "", basePageId } = props.match.params;
|
||||
const dispatch = useDispatch();
|
||||
const isEditorInitialized = useSelector(getIsEditorInitialized);
|
||||
const action = useSelector((state) => getActionByBaseId(state, baseApiId));
|
||||
const apiName = action?.name || "";
|
||||
const pluginId = get(action, "pluginId", "");
|
||||
const datasourceId = action?.datasource.id || "";
|
||||
const plugins = useSelector(getPlugins);
|
||||
const pages = useSelector(getPageList);
|
||||
const pageName = getPageName(pages, basePageId);
|
||||
const settingsConfig = useSelector((state) =>
|
||||
getPluginSettingConfigs(state, pluginId),
|
||||
);
|
||||
const pagePermissions = useSelector(getPagePermissions);
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const isConverting = useSelector((state) =>
|
||||
getIsActionConverting(state, action?.id || ""),
|
||||
);
|
||||
const editorMode = useSelector(getIDEViewMode);
|
||||
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
|
||||
const icon = resolveIcon({
|
||||
iconLocation: pluginGroups[pluginId]?.iconLocation || "",
|
||||
pluginType: action?.pluginType || "",
|
||||
moduleType: action?.actionConfiguration?.body?.moduleType,
|
||||
}) || (
|
||||
<EntityIcon
|
||||
height={`${ENTITY_ICON_SIZE}px`}
|
||||
width={`${ENTITY_ICON_SIZE}px`}
|
||||
>
|
||||
<Icon name="module" />
|
||||
</EntityIcon>
|
||||
);
|
||||
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
isFeatureEnabled,
|
||||
action?.userPermissions,
|
||||
);
|
||||
const isDeletePermitted = getHasDeleteActionPermission(
|
||||
isFeatureEnabled,
|
||||
action?.userPermissions,
|
||||
);
|
||||
const isCreatePermitted = getHasCreateActionPermission(
|
||||
isFeatureEnabled,
|
||||
pagePermissions,
|
||||
);
|
||||
|
||||
const moreActionsMenu = useMemo(() => {
|
||||
const convertToModuleProps = {
|
||||
canCreateModuleInstance: isCreatePermitted,
|
||||
canDeleteEntity: isDeletePermitted,
|
||||
entityId: action?.id || "",
|
||||
moduleType: MODULE_TYPE.QUERY,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MoreActionsMenu
|
||||
basePageId={basePageId}
|
||||
className="t--more-action-menu"
|
||||
id={action?.id || ""}
|
||||
isChangePermitted={isChangePermitted}
|
||||
isDeletePermitted={isDeletePermitted}
|
||||
name={action?.name || ""}
|
||||
prefixAdditionalMenus={
|
||||
editorMode === EditorViewMode.SplitScreen && (
|
||||
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
{editorMode !== EditorViewMode.SplitScreen && (
|
||||
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
action?.id,
|
||||
action?.name,
|
||||
isChangePermitted,
|
||||
isDeletePermitted,
|
||||
basePageId,
|
||||
isCreatePermitted,
|
||||
editorMode,
|
||||
]);
|
||||
|
||||
const handleRunClick = useCallback(
|
||||
(paginationField?: PaginationField) => {
|
||||
const pluginName = plugins.find((plugin) => plugin.id === pluginId)?.name;
|
||||
|
||||
AnalyticsUtil.logEvent("RUN_API_CLICK", {
|
||||
apiName,
|
||||
apiID: action?.id,
|
||||
pageName: pageName,
|
||||
datasourceId,
|
||||
pluginName: pluginName,
|
||||
isMock: false, // as mock db exists only for postgres and mongo plugins
|
||||
});
|
||||
dispatch(runAction(action?.id ?? "", paginationField));
|
||||
},
|
||||
[action?.id, apiName, pageName, plugins, pluginId, datasourceId, dispatch],
|
||||
);
|
||||
|
||||
const actionRightPaneBackLink = useMemo(() => {
|
||||
return <BackToCanvas basePageId={basePageId} />;
|
||||
}, [basePageId]);
|
||||
|
||||
const notification = useMemo(() => {
|
||||
if (!isConverting) return null;
|
||||
|
||||
return <ConvertEntityNotification icon={icon} name={action?.name || ""} />;
|
||||
}, [action?.name, isConverting, icon]);
|
||||
|
||||
return (
|
||||
<ApiEditorContextProvider
|
||||
actionRightPaneBackLink={actionRightPaneBackLink}
|
||||
handleRunClick={handleRunClick}
|
||||
moreActionsMenu={moreActionsMenu}
|
||||
notification={notification}
|
||||
saveActionName={saveActionName}
|
||||
settingsConfig={settingsConfig}
|
||||
>
|
||||
<Disabler isDisabled={isConverting}>
|
||||
<Editor {...props} isEditorInitialized={isEditorInitialized} />
|
||||
</Disabler>
|
||||
</ApiEditorContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiEditorWrapper;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import localStorage from "utils/localStorage";
|
||||
import { render, waitFor } from "test/testUtils";
|
||||
import { Route } from "react-router-dom";
|
||||
import { BUILDER_PATH } from "ee/constants/routes/appRoutes";
|
||||
|
|
@ -17,7 +16,6 @@ const FeatureFlags = {
|
|||
const basePageId = "0123456789abcdef00000000";
|
||||
|
||||
describe("IDE Render: JS", () => {
|
||||
localStorage.setItem("SPLITPANE_ANNOUNCEMENT", "false");
|
||||
describe("JS Blank State", () => {
|
||||
it("Renders Fullscreen Blank State", async () => {
|
||||
const { findByText, getByRole, getByText } = render(
|
||||
|
|
@ -132,7 +130,7 @@ describe("IDE Render: JS", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { container, getAllByText, getByRole, getByTestId } = render(
|
||||
const { getAllByText, getByRole, getByTestId } = render(
|
||||
<Route path={BUILDER_PATH}>
|
||||
<IDE />
|
||||
</Route>,
|
||||
|
|
@ -147,13 +145,13 @@ describe("IDE Render: JS", () => {
|
|||
async () => {
|
||||
const elements = getAllByText("JSObject1"); // Use the common test ID or selector
|
||||
|
||||
expect(elements).toHaveLength(3); // Wait until there are exactly 3 elements
|
||||
expect(elements).toHaveLength(2); // Wait until there are exactly 2 elements
|
||||
},
|
||||
{ timeout: 3000, interval: 500 },
|
||||
);
|
||||
|
||||
// There will be 3 JSObject1 text (Left pane list, editor tab and Editor form)
|
||||
expect(getAllByText("JSObject1").length).toEqual(3);
|
||||
// There will be 2 JSObject1 text (Left pane list and editor tab)
|
||||
expect(getAllByText("JSObject1").length).toEqual(2);
|
||||
// Left pane active state
|
||||
expect(
|
||||
getByTestId("t--entity-item-JSObject1").classList.contains("active"),
|
||||
|
|
@ -162,13 +160,12 @@ describe("IDE Render: JS", () => {
|
|||
expect(
|
||||
getByTestId("t--ide-tab-jsobject1").classList.contains("active"),
|
||||
).toBe(true);
|
||||
// Check if the form is rendered
|
||||
expect(container.querySelector(".js-editor-tab")).not.toBeNull();
|
||||
// Check if the code and settings tabs is visible
|
||||
getByRole("tab", { name: /code/i });
|
||||
getByRole("tab", { name: /settings/i });
|
||||
// Check if run button is visible
|
||||
// Check toolbar elements
|
||||
getByRole("button", { name: /myFun1/i });
|
||||
getByRole("button", { name: /run/i });
|
||||
getByTestId("t--js-settings-trigger");
|
||||
getByTestId("t--more-action-trigger");
|
||||
|
||||
// Check if the Add new button is shown
|
||||
getByTestId("t--add-item");
|
||||
});
|
||||
|
|
@ -190,7 +187,7 @@ describe("IDE Render: JS", () => {
|
|||
ideView: EditorViewMode.SplitScreen,
|
||||
});
|
||||
|
||||
const { container, getAllByText, getByRole, getByTestId } = render(
|
||||
const { getAllByText, getByRole, getByTestId } = render(
|
||||
<Route path={BUILDER_PATH}>
|
||||
<IDE />
|
||||
</Route>,
|
||||
|
|
@ -206,19 +203,17 @@ describe("IDE Render: JS", () => {
|
|||
getByTestId("t--widgets-editor");
|
||||
|
||||
// Check if js is rendered in side by side
|
||||
expect(getAllByText("JSObject2").length).toBe(2);
|
||||
expect(getAllByText("JSObject2").length).toBe(1);
|
||||
// Tabs active state
|
||||
expect(
|
||||
getByTestId("t--ide-tab-jsobject2").classList.contains("active"),
|
||||
).toBe(true);
|
||||
|
||||
// Check if the form is rendered
|
||||
expect(container.querySelector(".js-editor-tab")).not.toBeNull();
|
||||
// Check if the code and settings tabs is visible
|
||||
getByRole("tab", { name: /code/i });
|
||||
getByRole("tab", { name: /settings/i });
|
||||
// Check if run button is visible
|
||||
// Check toolbar elements
|
||||
getByRole("button", { name: /myFun1/i });
|
||||
getByRole("button", { name: /run/i });
|
||||
getByTestId("t--more-action-trigger");
|
||||
|
||||
// Check if the Add new button is shown
|
||||
getByTestId("t--ide-tabs-add-button");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ import { createMessage, EDITOR_PANE_TEXTS } from "ee/constants/messages";
|
|||
import { BUILDER_PATH } from "ee/constants/routes/appRoutes";
|
||||
import { EditorEntityTab, EditorViewMode } from "ee/entities/IDE/constants";
|
||||
import { APIFactory } from "test/factories/Actions/API";
|
||||
import localStorage from "utils/localStorage";
|
||||
import { PostgresFactory } from "test/factories/Actions/Postgres";
|
||||
import { sagasToRunForTests } from "test/sagas";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { getIDETestState } from "test/factories/AppIDEFactoryUtils";
|
||||
import { PageFactory } from "test/factories/PageFactory";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
|
|
@ -22,7 +20,6 @@ const FeatureFlags = {
|
|||
const basePageId = "0123456789abcdef00000000";
|
||||
|
||||
describe("IDE URL rendering of Queries", () => {
|
||||
localStorage.setItem("SPLITPANE_ANNOUNCEMENT", "false");
|
||||
describe("Query Blank State", () => {
|
||||
it("Renders Fullscreen Blank State", async () => {
|
||||
const { findByText, getByRole, getByText } = render(
|
||||
|
|
@ -144,7 +141,7 @@ describe("IDE URL rendering of Queries", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { getAllByText, getByRole, getByTestId } = render(
|
||||
const { getAllByRole, getAllByText, getByRole, getByTestId } = render(
|
||||
<Route path={BUILDER_PATH}>
|
||||
<IDE />
|
||||
</Route>,
|
||||
|
|
@ -159,13 +156,13 @@ describe("IDE URL rendering of Queries", () => {
|
|||
async () => {
|
||||
const elements = getAllByText("Api1"); // Use the common test ID or selector
|
||||
|
||||
expect(elements).toHaveLength(3); // Wait until there are exactly 3 elements
|
||||
expect(elements).toHaveLength(2); // Wait until there are exactly 3 elements
|
||||
},
|
||||
{ timeout: 3000, interval: 500 },
|
||||
);
|
||||
|
||||
// There will be 3 Api1 text (Left pane list, editor tab and Editor form)
|
||||
expect(getAllByText("Api1").length).toEqual(3);
|
||||
// There will be 2 Api1 text (Left pane list, editor tab)
|
||||
expect(getAllByText("Api1").length).toEqual(2);
|
||||
// Left pane active state
|
||||
expect(
|
||||
getByTestId("t--entity-item-Api1").classList.contains("active"),
|
||||
|
|
@ -175,11 +172,11 @@ describe("IDE URL rendering of Queries", () => {
|
|||
true,
|
||||
);
|
||||
// Check if the form is rendered
|
||||
getByTestId("t--action-form-API");
|
||||
getByTestId("t--api-editor-form");
|
||||
// Check if the params tabs is visible
|
||||
getByRole("tab", { name: /params/i });
|
||||
// Check if run button is visible
|
||||
getByRole("button", { name: /run/i });
|
||||
expect(getAllByRole("button", { name: /run/i })).toHaveLength(2);
|
||||
// Check if the Add new button is shown
|
||||
getByTestId("t--add-item");
|
||||
});
|
||||
|
|
@ -201,7 +198,7 @@ describe("IDE URL rendering of Queries", () => {
|
|||
ideView: EditorViewMode.SplitScreen,
|
||||
});
|
||||
|
||||
const { getAllByText, getByRole, getByTestId } = render(
|
||||
const { getAllByRole, getAllByText, getByTestId } = render(
|
||||
<Route path={BUILDER_PATH}>
|
||||
<IDE />
|
||||
</Route>,
|
||||
|
|
@ -217,15 +214,15 @@ describe("IDE URL rendering of Queries", () => {
|
|||
getByTestId("t--widgets-editor");
|
||||
|
||||
// Check if api is rendered in side by side
|
||||
expect(getAllByText("Api2").length).toBe(2);
|
||||
expect(getAllByText("Api2").length).toBe(1);
|
||||
// Tabs active state
|
||||
expect(getByTestId("t--ide-tab-api2").classList.contains("active")).toBe(
|
||||
true,
|
||||
);
|
||||
// Check if the form is rendered
|
||||
getByTestId("t--action-form-API");
|
||||
getByTestId("t--api-editor-form");
|
||||
// Check if run button is visible
|
||||
getByRole("button", { name: /run/i });
|
||||
expect(getAllByRole("button", { name: /run/i }).length).toBe(2);
|
||||
// Check if the Add new button is shown
|
||||
getByTestId("t--ide-tabs-add-button");
|
||||
});
|
||||
|
|
@ -358,12 +355,12 @@ describe("IDE URL rendering of Queries", () => {
|
|||
async () => {
|
||||
const elements = getAllByText("Query1"); // Use the common test ID or selector
|
||||
|
||||
expect(elements).toHaveLength(3); // Wait until there are exactly 3 elements
|
||||
expect(elements).toHaveLength(2); // Wait until there are exactly 3 elements
|
||||
},
|
||||
{ timeout: 3000, interval: 500 },
|
||||
);
|
||||
// There will be 3 Query1 text (Left pane list, editor tab and Editor form)
|
||||
expect(getAllByText("Query1").length).toBe(3);
|
||||
// There will be 2 Query1 text (Left pane list, editor tab)
|
||||
expect(getAllByText("Query1").length).toBe(2);
|
||||
// Left pane active state
|
||||
expect(
|
||||
getByTestId("t--entity-item-Query1").classList.contains("active"),
|
||||
|
|
@ -372,11 +369,8 @@ describe("IDE URL rendering of Queries", () => {
|
|||
expect(
|
||||
getByTestId("t--ide-tab-query1").classList.contains("active"),
|
||||
).toBe(true);
|
||||
|
||||
await userEvent.click(getByRole("tab", { name: "Query" }));
|
||||
|
||||
// Check if the form is rendered
|
||||
getByTestId("t--action-form-DB");
|
||||
getByTestId("t--uqi-editor-form");
|
||||
// Check if run button is visible
|
||||
getByRole("button", { name: /run/i });
|
||||
// Check if the Add new button is shown
|
||||
|
|
@ -417,16 +411,14 @@ describe("IDE URL rendering of Queries", () => {
|
|||
getByTestId("t--widgets-editor");
|
||||
|
||||
// Check if api is rendered in side by side
|
||||
expect(getAllByText("Query2").length).toBe(2);
|
||||
expect(getAllByText("Query2").length).toBe(1);
|
||||
// Tabs active state
|
||||
expect(
|
||||
getByTestId("t--ide-tab-query2").classList.contains("active"),
|
||||
).toBe(true);
|
||||
|
||||
await userEvent.click(getByRole("tab", { name: "Query" }));
|
||||
|
||||
// Check if the form is rendered
|
||||
getByTestId("t--action-form-DB");
|
||||
getByTestId("t--uqi-editor-form");
|
||||
// Check if run button is visible
|
||||
getByRole("button", { name: /run/i });
|
||||
// Check if the Add new button is shown
|
||||
|
|
@ -449,7 +441,7 @@ describe("IDE URL rendering of Queries", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { container, getByTestId, getByText } = render(
|
||||
const { getByTestId, getByText } = render(
|
||||
<Route path={BUILDER_PATH}>
|
||||
<IDE />
|
||||
</Route>,
|
||||
|
|
@ -461,8 +453,6 @@ describe("IDE URL rendering of Queries", () => {
|
|||
},
|
||||
);
|
||||
|
||||
screen.logTestingPlaygroundURL(container);
|
||||
|
||||
// Create options are rendered
|
||||
getByText(createMessage(EDITOR_PANE_TEXTS.queries_create_from_existing));
|
||||
getByText("New datasource");
|
||||
|
|
@ -562,8 +552,8 @@ describe("IDE URL rendering of Queries", () => {
|
|||
},
|
||||
);
|
||||
|
||||
// There will be 3 Query1 text (Left pane list, editor tab and Editor form)
|
||||
expect(getAllByText("Sheets1").length).toBe(3);
|
||||
// There will be 2 Query1 text (Left pane list, editor tab)
|
||||
expect(getAllByText("Sheets1").length).toBe(2);
|
||||
// Left pane active state
|
||||
expect(
|
||||
getByTestId("t--entity-item-Sheets1").classList.contains("active"),
|
||||
|
|
@ -573,10 +563,8 @@ describe("IDE URL rendering of Queries", () => {
|
|||
getByTestId("t--ide-tab-sheets1").classList.contains("active"),
|
||||
).toBe(true);
|
||||
|
||||
await userEvent.click(getByRole("tab", { name: "Query" }));
|
||||
|
||||
// Check if the form is rendered
|
||||
getByTestId("t--action-form-SAAS");
|
||||
getByTestId("t--uqi-editor-form");
|
||||
// Check if run button is visible
|
||||
getByRole("button", { name: /run/i });
|
||||
// Check if the Add new button is shown
|
||||
|
|
@ -618,18 +606,16 @@ describe("IDE URL rendering of Queries", () => {
|
|||
getByTestId("t--widgets-editor");
|
||||
|
||||
// Check if api is rendered in side by side
|
||||
expect(getAllByText("Sheets2").length).toBe(2);
|
||||
expect(getAllByText("Sheets2").length).toBe(1);
|
||||
// Tabs active state
|
||||
expect(
|
||||
getByTestId("t--ide-tab-sheets2").classList.contains("active"),
|
||||
).toBe(true);
|
||||
|
||||
await userEvent.click(getByRole("tab", { name: "Query" }));
|
||||
|
||||
screen.logTestingPlaygroundURL(container);
|
||||
|
||||
// Check if the form is rendered
|
||||
getByTestId("t--action-form-SAAS");
|
||||
getByTestId("t--uqi-editor-form");
|
||||
// Check if run button is visible
|
||||
getByRole("button", { name: /run/i });
|
||||
// Check if the Add new button is shown
|
||||
|
|
@ -653,7 +639,7 @@ describe("IDE URL rendering of Queries", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { container, getByTestId, getByText } = render(
|
||||
const { getByTestId, getByText } = render(
|
||||
<Route path={BUILDER_PATH}>
|
||||
<IDE />
|
||||
</Route>,
|
||||
|
|
@ -665,8 +651,6 @@ describe("IDE URL rendering of Queries", () => {
|
|||
},
|
||||
);
|
||||
|
||||
screen.logTestingPlaygroundURL(container);
|
||||
|
||||
// Create options are rendered
|
||||
getByText(createMessage(EDITOR_PANE_TEXTS.queries_create_from_existing));
|
||||
getByText("New datasource");
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import { AnnouncementModal, Button } from "@appsmith/ads";
|
||||
import localStorage, { LOCAL_STORAGE_KEYS } from "utils/localStorage";
|
||||
import { SPLITPANE_ANNOUNCEMENT, createMessage } from "ee/constants/messages";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
|
||||
const Announcement = () => {
|
||||
const localStorageFlag =
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.SPLITPANE_ANNOUNCEMENT) || "true";
|
||||
const [show, setShow] = useState(JSON.parse(localStorageFlag));
|
||||
|
||||
const tryClickHandler = () => {
|
||||
setShow(false);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.SPLITPANE_ANNOUNCEMENT, "false");
|
||||
};
|
||||
|
||||
const learnClickHandler = () => {
|
||||
window.open(
|
||||
"https://community.appsmith.com/content/blog/discover-ide-20-building-more-efficient-ide",
|
||||
"_blank",
|
||||
);
|
||||
};
|
||||
|
||||
const featureIsOutOfBeta = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
const modalFooter = () => (
|
||||
<>
|
||||
<Button
|
||||
data-testid="t--ide-close-announcement"
|
||||
kind="primary"
|
||||
onClick={tryClickHandler}
|
||||
size="md"
|
||||
>
|
||||
Try it out
|
||||
</Button>
|
||||
<Button kind="tertiary" onClick={learnClickHandler} size="md">
|
||||
Learn more
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
// If the feature is out of beta, don't show the announcement
|
||||
if (featureIsOutOfBeta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementModal
|
||||
banner={getAssetUrl(`${ASSETS_CDN_URL}/splitpane-banner.svg`)}
|
||||
description={createMessage(SPLITPANE_ANNOUNCEMENT.DESCRIPTION)}
|
||||
footer={modalFooter()}
|
||||
isBeta
|
||||
isOpen={show}
|
||||
title={createMessage(SPLITPANE_ANNOUNCEMENT.TITLE)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Announcement };
|
||||
|
|
@ -15,7 +15,6 @@ const FeatureFlags = {
|
|||
};
|
||||
|
||||
describe("EditorTabs render checks", () => {
|
||||
localStorage.setItem("SPLITPANE_ANNOUNCEMENT", "false");
|
||||
const page = PageFactory.build();
|
||||
|
||||
const renderComponent = (url: string, state: Partial<AppState>) =>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import Container from "./Container";
|
|||
import { useCurrentEditorState, useIDETabClickHandlers } from "../hooks";
|
||||
import { SCROLL_AREA_OPTIONS, TabSelectors } from "./constants";
|
||||
import { AddButton } from "./AddButton";
|
||||
import { Announcement } from "../EditorPane/components/Announcement";
|
||||
import { useLocation } from "react-router";
|
||||
import { identifyEntityFromPath } from "navigation/FocusEntity";
|
||||
import { List } from "./List";
|
||||
|
|
@ -162,9 +161,6 @@ const EditorTabs = () => {
|
|||
{isListViewActive && ideViewMode === EditorViewMode.SplitScreen && (
|
||||
<List />
|
||||
)}
|
||||
|
||||
{/* Announcement modal */}
|
||||
{ideViewMode === EditorViewMode.SplitScreen && <Announcement />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ import { useEditorType } from "ee/hooks";
|
|||
import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { isWidgetActionConnectionPresent } from "selectors/onboardingSelectors";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import localStorage, { LOCAL_STORAGE_KEYS } from "utils/localStorage";
|
||||
|
||||
export const useCurrentEditorState = () => {
|
||||
|
|
@ -212,12 +210,8 @@ export const useShowSideBySideNudge: () => [boolean, () => void] = () => {
|
|||
LOCAL_STORAGE_KEYS.NUDGE_SHOWN_SPLIT_PANE,
|
||||
);
|
||||
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
const { setFalse, value } = useBoolean(
|
||||
widgetBindingsExist && isActionRedesignEnabled && !localStorageFlag,
|
||||
widgetBindingsExist && !localStorageFlag,
|
||||
);
|
||||
|
||||
const dismissNudge = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -55,14 +55,14 @@ import {
|
|||
type JSActionDropdownOption,
|
||||
convertJSActionToDropdownOption,
|
||||
getJSActionOption,
|
||||
type OnUpdateSettingsProps,
|
||||
} from "./JSEditorToolbar";
|
||||
import type { JSFunctionSettingsProps } from "./JSEditorToolbar/components/old/JSFunctionSettings";
|
||||
|
||||
interface JSFormProps {
|
||||
jsCollectionData: JSCollectionData;
|
||||
contextMenu: React.ReactNode;
|
||||
showSettings?: boolean;
|
||||
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
|
||||
onUpdateSettings: (props: OnUpdateSettingsProps) => void;
|
||||
saveJSObjectName: JSObjectNameEditorProps["saveJSObjectName"];
|
||||
backLink?: React.ReactNode;
|
||||
hideContextMenuOnEditor?: boolean;
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import {
|
|||
MenuTrigger,
|
||||
Text,
|
||||
} from "@appsmith/ads";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
|
||||
export interface ContextMenuOption {
|
||||
id?: string;
|
||||
|
|
@ -37,10 +35,6 @@ export function JSEditorContextMenu({
|
|||
onMenuClose,
|
||||
options,
|
||||
}: EntityContextMenuProps) {
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -59,8 +53,8 @@ export function JSEditorContextMenu({
|
|||
data-testid="t--more-action-trigger"
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
size={isActionRedesignEnabled ? "sm" : "md"}
|
||||
startIcon={isActionRedesignEnabled ? "more-2-fill" : "context-menu"}
|
||||
size={"sm"}
|
||||
startIcon={"more-2-fill"}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
<MenuContent align="end" avoidCollisions>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import React from "react";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import type { JSEditorTab } from "reducers/uiReducers/jsPaneReducer";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import {
|
||||
type BlockCompletion,
|
||||
CodeEditorBorder,
|
||||
|
|
@ -12,7 +10,6 @@ import {
|
|||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import type { CodeEditorGutter } from "components/editorComponents/CodeEditor";
|
||||
import type { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import { OldJSEditorForm } from "./old/JSEditorForm";
|
||||
import type { OnUpdateSettingsProps } from "../JSEditorToolbar";
|
||||
import LazyCodeEditor from "components/editorComponents/LazyCodeEditor";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
|
|
@ -33,29 +30,6 @@ interface Props {
|
|||
}
|
||||
|
||||
export const JSEditorForm = (props: Props) => {
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
if (!isActionRedesignEnabled) {
|
||||
return (
|
||||
<OldJSEditorForm
|
||||
actions={props.actions}
|
||||
blockCompletions={props.blockCompletions}
|
||||
changePermitted={props.changePermitted}
|
||||
currentJSCollection={props.currentJSCollection}
|
||||
customGutter={props.customGutter}
|
||||
executing={props.executing}
|
||||
onChange={props.onChange}
|
||||
onUpdateSettings={props.onUpdateSettings}
|
||||
onValueChange={props.onValueChange}
|
||||
showSettings={props.showSettings}
|
||||
theme={props.theme}
|
||||
value={props.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flex="1" overflowY="scroll">
|
||||
<LazyCodeEditor
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
import { JSEditorTab } from "reducers/uiReducers/jsPaneReducer";
|
||||
import React from "react";
|
||||
import type {
|
||||
BlockCompletion,
|
||||
EditorTheme,
|
||||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import {
|
||||
CodeEditorBorder,
|
||||
EditorModes,
|
||||
EditorSize,
|
||||
TabBehaviour,
|
||||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { TabbedViewContainer } from "../../styledComponents";
|
||||
import { Tab, TabPanel, Tabs, TabsList } from "@appsmith/ads";
|
||||
import LazyCodeEditor from "components/editorComponents/LazyCodeEditor";
|
||||
import type { CodeEditorGutter } from "components/editorComponents/CodeEditor";
|
||||
import type { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import { type OnUpdateSettingsProps } from "../../JSEditorToolbar";
|
||||
import { JSFunctionSettings } from "../../JSEditorToolbar/components/JSFunctionSettings";
|
||||
|
||||
interface Props {
|
||||
executing: boolean;
|
||||
onValueChange: (value: string) => void;
|
||||
value: JSEditorTab;
|
||||
showSettings: undefined | boolean;
|
||||
blockCompletions: Array<BlockCompletion>;
|
||||
customGutter: CodeEditorGutter;
|
||||
currentJSCollection: JSCollection;
|
||||
changePermitted: boolean;
|
||||
onChange: (valueOrEvent: React.ChangeEvent | string) => void;
|
||||
theme: EditorTheme.LIGHT;
|
||||
actions: JSAction[];
|
||||
onUpdateSettings?: (props: OnUpdateSettingsProps) => void;
|
||||
}
|
||||
|
||||
export function OldJSEditorForm(props: Props) {
|
||||
return (
|
||||
<TabbedViewContainer isExecuting={props.executing}>
|
||||
<Tabs
|
||||
defaultValue={JSEditorTab.CODE}
|
||||
onValueChange={props.onValueChange}
|
||||
value={props.value}
|
||||
>
|
||||
<TabsList>
|
||||
<Tab
|
||||
data-testid={`t--js-editor-` + JSEditorTab.CODE}
|
||||
value={JSEditorTab.CODE}
|
||||
>
|
||||
Code
|
||||
</Tab>
|
||||
{props.showSettings && (
|
||||
<Tab
|
||||
data-testid={`t--js-editor-` + JSEditorTab.SETTINGS}
|
||||
value={JSEditorTab.SETTINGS}
|
||||
>
|
||||
Settings
|
||||
</Tab>
|
||||
)}
|
||||
</TabsList>
|
||||
<TabPanel value={JSEditorTab.CODE}>
|
||||
<div className="js-editor-tab">
|
||||
<LazyCodeEditor
|
||||
AIAssisted
|
||||
blockCompletions={props.blockCompletions}
|
||||
border={CodeEditorBorder.NONE}
|
||||
borderLess
|
||||
className={"js-editor"}
|
||||
customGutter={props.customGutter}
|
||||
dataTreePath={`${props.currentJSCollection.name}.body`}
|
||||
disabled={!props.changePermitted}
|
||||
folding
|
||||
height={"100%"}
|
||||
hideEvaluatedValue
|
||||
input={{
|
||||
value: props.currentJSCollection.body,
|
||||
onChange: props.onChange,
|
||||
}}
|
||||
isJSObject
|
||||
jsObjectName={props.currentJSCollection.name}
|
||||
mode={EditorModes.JAVASCRIPT}
|
||||
placeholder="Let's write some code!"
|
||||
showLightningMenu={false}
|
||||
showLineNumbers
|
||||
size={EditorSize.EXTENDED}
|
||||
tabBehaviour={TabBehaviour.INDENT}
|
||||
theme={props.theme}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
{props.showSettings && (
|
||||
<TabPanel value={JSEditorTab.SETTINGS}>
|
||||
<div className="js-editor-tab">
|
||||
<JSFunctionSettings
|
||||
actions={props.actions}
|
||||
disabled={!props.changePermitted}
|
||||
onUpdateSettings={props.onUpdateSettings}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
)}
|
||||
</Tabs>
|
||||
</TabbedViewContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -34,17 +34,7 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
describe("JSEditorToolbar", () => {
|
||||
it("renders JSHeader when action redesign is disabled", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(false);
|
||||
render(<JSEditorToolbar {...defaultProps} />);
|
||||
// Old header shows the name of the JS object
|
||||
// since we don't provide the name via props, it has the placeholder text
|
||||
expect(
|
||||
screen.getByText("Name of the JS Object in camelCase"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders IDEToolbar with JSFunctionRun and JSFunctionSettings when action redesign is enabled", () => {
|
||||
it("renders IDEToolbar with JSFunctionRun and JSFunctionSettings", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
render(<JSEditorToolbar {...defaultProps} />);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import React, { useState } from "react";
|
||||
import { IDEToolbar, ToolbarSettingsPopover } from "IDE";
|
||||
import { JSFunctionRun } from "./components/JSFunctionRun";
|
||||
import type { JSActionDropdownOption } from "./types";
|
||||
import type { JSActionDropdownOption, OnUpdateSettingsProps } from "./types";
|
||||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import type { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import type { DropdownOnSelect } from "@appsmith/ads-old";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import { createMessage, JS_EDITOR_SETTINGS } from "ee/constants/messages";
|
||||
import { JSHeader } from "./JSHeader";
|
||||
import { JSFunctionSettings } from "./components/JSFunctionSettings";
|
||||
import type { JSFunctionSettingsProps } from "./components/old/JSFunctionSettings";
|
||||
import { convertJSActionsToDropdownOptions } from "./utils";
|
||||
import { JSObjectNameEditor } from "./JSObjectNameEditor";
|
||||
|
||||
|
|
@ -33,7 +29,7 @@ interface Props {
|
|||
onSelect: DropdownOnSelect;
|
||||
jsActions: JSAction[];
|
||||
selected: JSActionDropdownOption;
|
||||
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
|
||||
onUpdateSettings: (props: OnUpdateSettingsProps) => void;
|
||||
showNameEditor?: boolean;
|
||||
showSettings: boolean;
|
||||
}
|
||||
|
|
@ -41,23 +37,12 @@ interface Props {
|
|||
/**
|
||||
* JSEditorToolbar component.
|
||||
*
|
||||
* This component renders a toolbar for the JS editor. It conditionally renders
|
||||
* different components based on the `release_actions_redesign_enabled` feature flag.
|
||||
* This component renders a toolbar for the JS editor.
|
||||
*
|
||||
*/
|
||||
export const JSEditorToolbar = (props: Props) => {
|
||||
// Check if the action redesign feature flag is enabled
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// If the action redesign is not enabled, render the JSHeader component
|
||||
if (!isActionRedesignEnabled) {
|
||||
return <JSHeader {...props} />;
|
||||
}
|
||||
|
||||
// Render the IDEToolbar with JSFunctionRun and JSFunctionSettings components
|
||||
return (
|
||||
<IDEToolbar>
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
import React from "react";
|
||||
import { JSFunctionRun } from "./components/JSFunctionRun";
|
||||
import type { JSActionDropdownOption } from "./types";
|
||||
import { ActionButtons, NameWrapper, StyledFormRow } from "../styledComponents";
|
||||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import type { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import type { DropdownOnSelect } from "@appsmith/ads-old";
|
||||
import { JSObjectNameEditor } from "./JSObjectNameEditor";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
import { convertJSActionsToDropdownOptions } from "./utils";
|
||||
|
||||
interface Props {
|
||||
changePermitted: boolean;
|
||||
hideEditIconOnEditor?: boolean;
|
||||
saveJSObjectName: (
|
||||
params: SaveActionNameParams,
|
||||
) => ReduxAction<SaveActionNameParams>;
|
||||
hideContextMenuOnEditor?: boolean;
|
||||
contextMenu: React.ReactNode;
|
||||
disableRunFunctionality: boolean;
|
||||
executePermitted: boolean;
|
||||
loading: boolean;
|
||||
jsCollection: JSCollection;
|
||||
onButtonClick: (
|
||||
event: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent,
|
||||
) => void;
|
||||
onSelect: DropdownOnSelect;
|
||||
jsActions: JSAction[];
|
||||
selected: JSActionDropdownOption;
|
||||
}
|
||||
|
||||
export const JSHeader = (props: Props) => {
|
||||
return (
|
||||
<Flex paddingTop="spaces-5">
|
||||
<StyledFormRow className="form-row-header">
|
||||
<NameWrapper className="t--nameOfJSObject">
|
||||
<JSObjectNameEditor
|
||||
disabled={!props.changePermitted || props.hideEditIconOnEditor}
|
||||
saveJSObjectName={props.saveJSObjectName}
|
||||
/>
|
||||
</NameWrapper>
|
||||
<ActionButtons className="t--formActionButtons">
|
||||
{!props.hideContextMenuOnEditor && props.contextMenu}
|
||||
<JSFunctionRun
|
||||
disabled={props.disableRunFunctionality || !props.executePermitted}
|
||||
isLoading={props.loading}
|
||||
jsCollection={props.jsCollection}
|
||||
onButtonClick={props.onButtonClick}
|
||||
onSelect={props.onSelect}
|
||||
options={convertJSActionsToDropdownOptions(props.jsActions)}
|
||||
selected={props.selected}
|
||||
showTooltip={!props.selected.data}
|
||||
/>
|
||||
</ActionButtons>
|
||||
</StyledFormRow>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import React, { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
|
|
@ -14,7 +12,6 @@ import {
|
|||
getJsCollectionByBaseId,
|
||||
getPlugin,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { JSObjectNameEditor as OldJSObjectNameEditor } from "./old/JSObjectNameEditor";
|
||||
import { EditableName, useIsRenaming } from "IDE";
|
||||
|
||||
export interface SaveActionNameParams {
|
||||
|
|
@ -102,10 +99,6 @@ export const JSObjectNameEditor = ({
|
|||
[currentJSObjectConfig, saveJSObjectName],
|
||||
);
|
||||
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (!currentPlugin) return null;
|
||||
|
||||
|
|
@ -119,15 +112,6 @@ export const JSObjectNameEditor = ({
|
|||
);
|
||||
}, [currentPlugin]);
|
||||
|
||||
if (!isActionRedesignEnabled) {
|
||||
return (
|
||||
<OldJSObjectNameEditor
|
||||
disabled={disabled}
|
||||
saveJSObjectName={saveJSObjectName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NameWrapper
|
||||
data-testid="t--js-object-name-editor"
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { useParams } from "react-router-dom";
|
||||
import { removeSpecialChars } from "utils/helpers";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import {
|
||||
getJsCollectionByBaseId,
|
||||
getPlugin,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import {
|
||||
ACTION_NAME_PLACEHOLDER,
|
||||
JS_OBJECT_ID_NOT_FOUND_IN_URL,
|
||||
createMessage,
|
||||
} from "ee/constants/messages";
|
||||
import EditableText, {
|
||||
EditInteractionKind,
|
||||
} from "components/editorComponents/EditableText";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import NameEditorComponent, {
|
||||
IconBox,
|
||||
IconWrapper,
|
||||
NameWrapper,
|
||||
} from "components/utils/NameEditorComponent";
|
||||
import { getSavingStatusForJSObjectName } from "selectors/actionSelectors";
|
||||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
|
||||
export interface JSObjectNameEditorProps {
|
||||
disabled?: boolean;
|
||||
saveJSObjectName: (
|
||||
params: SaveActionNameParams,
|
||||
) => ReduxAction<SaveActionNameParams>;
|
||||
}
|
||||
|
||||
export function JSObjectNameEditor(props: JSObjectNameEditorProps) {
|
||||
const params = useParams<{
|
||||
baseCollectionId?: string;
|
||||
baseQueryId?: string;
|
||||
}>();
|
||||
|
||||
const currentJSObjectConfig = useSelector((state: AppState) =>
|
||||
getJsCollectionByBaseId(state, params.baseCollectionId || ""),
|
||||
);
|
||||
|
||||
const currentPlugin = useSelector((state: AppState) =>
|
||||
getPlugin(state, currentJSObjectConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
const saveStatus = useSelector((state) =>
|
||||
getSavingStatusForJSObjectName(state, currentJSObjectConfig?.id || ""),
|
||||
);
|
||||
|
||||
return (
|
||||
<NameEditorComponent
|
||||
id={currentJSObjectConfig?.id}
|
||||
idUndefinedErrorMessage={JS_OBJECT_ID_NOT_FOUND_IN_URL}
|
||||
name={currentJSObjectConfig?.name}
|
||||
onSaveName={props.saveJSObjectName}
|
||||
saveStatus={saveStatus}
|
||||
>
|
||||
{({
|
||||
forceUpdate,
|
||||
handleNameChange,
|
||||
isInvalidNameForEntity,
|
||||
isNew,
|
||||
saveStatus,
|
||||
}: {
|
||||
forceUpdate: boolean;
|
||||
handleNameChange: (value: string) => void;
|
||||
isInvalidNameForEntity: (value: string) => string | boolean;
|
||||
isNew: boolean;
|
||||
saveStatus: { isSaving: boolean; error: boolean };
|
||||
}) => (
|
||||
<NameWrapper enableFontStyling>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
gap="spaces-3"
|
||||
overflow="hidden"
|
||||
width="100%"
|
||||
>
|
||||
{currentPlugin && (
|
||||
<IconBox>
|
||||
<IconWrapper
|
||||
alt={currentPlugin.name}
|
||||
src={getAssetUrl(currentPlugin.iconLocation)}
|
||||
/>
|
||||
</IconBox>
|
||||
)}
|
||||
<EditableText
|
||||
className="t--js-action-name-edit-field"
|
||||
defaultValue={
|
||||
currentJSObjectConfig ? currentJSObjectConfig.name : ""
|
||||
}
|
||||
disabled={props.disabled}
|
||||
editInteractionKind={EditInteractionKind.SINGLE}
|
||||
errorTooltipClass="t--action-name-edit-error"
|
||||
forceDefault={forceUpdate}
|
||||
isEditingDefault={isNew}
|
||||
isInvalid={isInvalidNameForEntity}
|
||||
onTextChanged={handleNameChange}
|
||||
placeholder={createMessage(ACTION_NAME_PLACEHOLDER, "JS Object")}
|
||||
type="text"
|
||||
underline
|
||||
updating={saveStatus.isSaving}
|
||||
valueTransform={removeSpecialChars}
|
||||
/>
|
||||
</Flex>
|
||||
</NameWrapper>
|
||||
)}
|
||||
</NameEditorComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export default JSObjectNameEditor;
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, fireEvent } from "test/testUtils";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { JSObjectFactory } from "test/factories/Actions/JSObject";
|
||||
|
||||
import { convertJSActionsToDropdownOptions } from "../utils";
|
||||
import { JSFunctionRun } from "./JSFunctionRun";
|
||||
import { JS_FUNCTION_RUN_NAME_LENGTH } from "./constants";
|
||||
|
||||
jest.mock("utils/hooks/useFeatureFlag");
|
||||
const mockUseFeatureFlag = useFeatureFlag as jest.Mock;
|
||||
|
||||
const JSObject = JSObjectFactory.build();
|
||||
|
||||
const mockProps = {
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
jsCollection: JSObject,
|
||||
onButtonClick: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
options: convertJSActionsToDropdownOptions(JSObject.actions),
|
||||
selected: {
|
||||
label: JSObject.actions[0].name,
|
||||
value: JSObject.actions[0].name,
|
||||
data: JSObject.actions[0],
|
||||
},
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
describe("JSFunctionRun", () => {
|
||||
it("renders OldJSFunctionRun when feature flag is disabled", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(false);
|
||||
render(<JSFunctionRun {...mockProps} />);
|
||||
expect(screen.getByText("myFun1")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Run" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders new JSFunctionRun when feature flag is enabled", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
render(<JSFunctionRun {...mockProps} />);
|
||||
// Assert the Function select is a popup menu
|
||||
expect(screen.getByText("myFun1")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "myFun1" })).toHaveAttribute(
|
||||
"aria-haspopup",
|
||||
"menu",
|
||||
);
|
||||
});
|
||||
|
||||
// This test is skipped because menu does not open in the test environment
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip("calls onSelect when a menu item is selected", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
render(<JSFunctionRun {...mockProps} />);
|
||||
// click the button to open the menu
|
||||
fireEvent.click(screen.getByRole("button", { name: "myFun1" }));
|
||||
|
||||
fireEvent.click(screen.getByText("myFun2"));
|
||||
expect(mockProps.onSelect).toHaveBeenCalledWith("myFun2");
|
||||
});
|
||||
|
||||
it("disables the button when props.disabled is true", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
render(<JSFunctionRun {...mockProps} disabled />);
|
||||
expect(screen.getByRole("button", { name: "myFun1" })).toBeDisabled();
|
||||
});
|
||||
|
||||
// This test is skipped because tooltip does not show in the test environment
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip("shows tooltip when showTooltip is true", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
render(<JSFunctionRun {...mockProps} showTooltip />);
|
||||
fireEvent.mouseOver(screen.getByText("Run"));
|
||||
expect(
|
||||
screen.getByText("No JS function to run in TestCollection"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onButtonClick when run button is clicked", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
render(<JSFunctionRun {...mockProps} />);
|
||||
fireEvent.click(screen.getByText("Run"));
|
||||
expect(mockProps.onButtonClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("truncates long names to 30 characters", () => {
|
||||
mockUseFeatureFlag.mockReturnValue(true);
|
||||
const options = [
|
||||
{
|
||||
label:
|
||||
"aReallyReallyLongFunctionNameThatConveysALotOfMeaningAndCannotBeShortenedAtAllBecauseItConveysALotOfMeaningAndCannotBeShortened",
|
||||
value: "1",
|
||||
},
|
||||
];
|
||||
const [selected] = options;
|
||||
const jsCollection = { name: "CollectionName" };
|
||||
const params = { options, selected, jsCollection } as Parameters<
|
||||
typeof JSFunctionRun
|
||||
>[0];
|
||||
|
||||
render(<JSFunctionRun {...params} />);
|
||||
|
||||
expect(screen.getByTestId("t--js-function-run").textContent?.length).toBe(
|
||||
JS_FUNCTION_RUN_NAME_LENGTH,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { truncate } from "lodash";
|
||||
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import { JSFunctionRun as OldJSFunctionRun } from "./old/JSFunctionRun";
|
||||
import type { JSCollection } from "entities/JSCollection";
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -37,9 +34,6 @@ interface Props {
|
|||
*/
|
||||
export const JSFunctionRun = (props: Props) => {
|
||||
const { onSelect } = props;
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
// Callback function to handle function selection from the dropdown menu
|
||||
const onFunctionSelect = useCallback(
|
||||
|
|
@ -51,11 +45,6 @@ export const JSFunctionRun = (props: Props) => {
|
|||
[onSelect],
|
||||
);
|
||||
|
||||
if (!isActionRedesignEnabled) {
|
||||
return <OldJSFunctionRun {...props} />;
|
||||
}
|
||||
|
||||
// Render the new version of the component
|
||||
return (
|
||||
<Flex gap="spaces-2">
|
||||
<Menu>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { Flex, Switch, Text } from "@appsmith/ads";
|
||||
import JSFunctionSettingsView, {
|
||||
type JSFunctionSettingsProps,
|
||||
} from "./old/JSFunctionSettings";
|
||||
import type { JSAction } from "entities/JSCollection";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import {
|
||||
createMessage,
|
||||
JS_EDITOR_SETTINGS,
|
||||
NO_JS_FUNCTIONS,
|
||||
} from "ee/constants/messages";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import type { OnUpdateSettingsProps } from "../types";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
actions: JSAction[];
|
||||
onUpdateSettings: JSFunctionSettingsProps["onUpdateSettings"];
|
||||
onUpdateSettings: (props: OnUpdateSettingsProps) => void;
|
||||
}
|
||||
|
||||
interface FunctionSettingsRowProps extends Omit<Props, "actions"> {
|
||||
|
|
@ -73,22 +69,6 @@ const FunctionSettingRow = (props: FunctionSettingsRowProps) => {
|
|||
* It conditionally renders the old or new version of the component based on a feature flag.
|
||||
*/
|
||||
export const JSFunctionSettings = (props: Props) => {
|
||||
const isActionRedesignEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_actions_redesign_enabled,
|
||||
);
|
||||
|
||||
// If the feature flag is disabled, render the old version of the component
|
||||
if (!isActionRedesignEnabled) {
|
||||
return (
|
||||
<JSFunctionSettingsView
|
||||
actions={props.actions}
|
||||
disabled={props.disabled}
|
||||
onUpdateSettings={props.onUpdateSettings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Render the new version of the component
|
||||
return (
|
||||
<Flex flexDirection="column" gap="spaces-4" w="100%">
|
||||
<Text kind="heading-xs">
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import type { JSCollection } from "entities/JSCollection";
|
||||
import type { SelectProps } from "@appsmith/ads";
|
||||
import { Button, Option, Select, Tooltip, Text } from "@appsmith/ads";
|
||||
import { createMessage, NO_JS_FUNCTION_TO_RUN } from "ee/constants/messages";
|
||||
import type { JSActionDropdownOption } from "../../types";
|
||||
import { RUN_BUTTON_DEFAULTS, testLocators } from "../../constants";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
jsCollection: JSCollection;
|
||||
onButtonClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
onSelect: SelectProps["onSelect"];
|
||||
options: JSActionDropdownOption[];
|
||||
selected: JSActionDropdownOption;
|
||||
showTooltip: boolean;
|
||||
}
|
||||
|
||||
export interface DropdownWithCTAWrapperProps {
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
const DropdownWithCTAWrapper = styled.div<DropdownWithCTAWrapperProps>`
|
||||
display: flex;
|
||||
gap: var(--ads-v2-spaces-3);
|
||||
`;
|
||||
|
||||
const OptionWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const OptionLabelWrapper = styled.div<{ fullSize?: boolean }>`
|
||||
width: ${(props) => (props?.fullSize ? "100%" : "80%")};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const OptionLabel = styled(Text)`
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export function JSFunctionRun({
|
||||
disabled,
|
||||
isLoading,
|
||||
jsCollection,
|
||||
onButtonClick,
|
||||
onSelect,
|
||||
options,
|
||||
selected,
|
||||
showTooltip,
|
||||
}: Props) {
|
||||
return (
|
||||
<DropdownWithCTAWrapper isDisabled={disabled}>
|
||||
<Select
|
||||
className="function-select-dropdown"
|
||||
isDisabled={disabled}
|
||||
onSelect={onSelect}
|
||||
size="md"
|
||||
value={
|
||||
selected.label && {
|
||||
key: selected.label,
|
||||
label: (
|
||||
<OptionLabelWrapper fullSize>
|
||||
<OptionLabel renderAs="p">{selected.label}</OptionLabel>
|
||||
</OptionLabelWrapper>
|
||||
),
|
||||
}
|
||||
}
|
||||
virtual={false}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Option key={option.value}>
|
||||
<OptionWrapper>
|
||||
<Tooltip
|
||||
content={option.label}
|
||||
// Here, 18 is the maximum charecter length because the width of this menu does not change
|
||||
isDisabled={(option.label?.length || 0) < 18}
|
||||
placement="right"
|
||||
>
|
||||
<OptionLabelWrapper>
|
||||
<OptionLabel renderAs="p">{option.label}</OptionLabel>
|
||||
</OptionLabelWrapper>
|
||||
</Tooltip>
|
||||
</OptionWrapper>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Tooltip
|
||||
content={createMessage(NO_JS_FUNCTION_TO_RUN, jsCollection.name)}
|
||||
isDisabled={!showTooltip}
|
||||
placement="topRight"
|
||||
>
|
||||
{/* this span exists to make the disabled button visible to the tooltip */}
|
||||
<span>
|
||||
<Button
|
||||
className={testLocators.runJSAction}
|
||||
data-testid={testLocators.runJSActionTestID}
|
||||
isDisabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onClick={onButtonClick}
|
||||
size="md"
|
||||
>
|
||||
{RUN_BUTTON_DEFAULTS.CTA_TEXT}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</DropdownWithCTAWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
import {
|
||||
createMessage,
|
||||
FUNCTION_SETTINGS_HEADING,
|
||||
NO_JS_FUNCTIONS,
|
||||
} from "ee/constants/messages";
|
||||
import type { JSAction } from "entities/JSCollection";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
CONFIRM_BEFORE_CALLING_HEADING,
|
||||
SETTINGS_HEADINGS,
|
||||
} from "../../../constants";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { Icon, Switch, Tooltip } from "@appsmith/ads";
|
||||
import RemoveConfirmationModal from "../../../RemoveConfirmBeforeCallingDialog";
|
||||
import type { OnUpdateSettingsProps } from "../../types";
|
||||
|
||||
interface SettingsHeadingProps {
|
||||
text: string;
|
||||
hasInfo?: boolean;
|
||||
info?: string;
|
||||
grow: boolean;
|
||||
headingCount: number;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
interface SettingsItemProps {
|
||||
headingCount: number;
|
||||
action: JSAction;
|
||||
disabled?: boolean;
|
||||
onUpdateSettings?: (props: OnUpdateSettingsProps) => void;
|
||||
renderAdditionalColumns?: (
|
||||
action: JSAction,
|
||||
headingCount: number,
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface JSFunctionSettingsProps {
|
||||
actions: JSAction[];
|
||||
disabled?: boolean;
|
||||
onUpdateSettings: SettingsItemProps["onUpdateSettings"];
|
||||
renderAdditionalColumns?: SettingsItemProps["renderAdditionalColumns"];
|
||||
additionalHeadings?: typeof SETTINGS_HEADINGS;
|
||||
}
|
||||
|
||||
const SettingRow = styled.div<{ isHeading?: boolean; noBorder?: boolean }>`
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
${(props) =>
|
||||
!props.noBorder &&
|
||||
`
|
||||
border-bottom: solid 1px var(--ads-v2-color-border);
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.isHeading &&
|
||||
`
|
||||
background: var(--ads-v2-color-bg-subtle);
|
||||
font-size: ${props.theme.typography.h5.fontSize}px;
|
||||
`};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
`;
|
||||
|
||||
export const SettingColumn = styled.div<{
|
||||
headingCount: number;
|
||||
grow?: boolean;
|
||||
isHeading?: boolean;
|
||||
hidden?: boolean;
|
||||
}>`
|
||||
visibility: ${(props) => (props.hidden ? "hidden" : "visible")};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: ${(props) => (props.grow ? 1 : 0)};
|
||||
padding: 5px 12px;
|
||||
width: ${({ headingCount }) => `calc(100% / ${headingCount})`};
|
||||
|
||||
${(props) =>
|
||||
props.isHeading &&
|
||||
`
|
||||
font-weight: ${props.theme.fontWeights[2]};
|
||||
font-size: ${props.theme.fontSizes[2]}px;
|
||||
margin-right: 9px;
|
||||
`}
|
||||
|
||||
${StyledIcon} {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const JSFunctionSettingsWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const SettingsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
& > h3 {
|
||||
margin: 20px 0;
|
||||
font-size: ${(props) => props.theme.fontSizes[5]}px;
|
||||
font-weight: ${(props) => props.theme.fontWeights[2]};
|
||||
color: var(--ads-v2-color-fg-emphasis);
|
||||
}
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const SettingsRowWrapper = styled.div`
|
||||
border-radius: var(--ads-v2-border-radius);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
const SettingsHeaderWrapper = styled.div``;
|
||||
const SettingsBodyWrapper = styled.div`
|
||||
overflow: auto;
|
||||
max-height: calc(100% - 48px);
|
||||
`;
|
||||
const SwitchWrapper = styled.div`
|
||||
margin-left: 6ch;
|
||||
`;
|
||||
|
||||
function SettingsHeading({
|
||||
grow,
|
||||
hasInfo,
|
||||
headingCount,
|
||||
hidden,
|
||||
info,
|
||||
text,
|
||||
}: SettingsHeadingProps) {
|
||||
return (
|
||||
<SettingColumn
|
||||
grow={grow}
|
||||
headingCount={headingCount}
|
||||
hidden={hidden}
|
||||
isHeading
|
||||
>
|
||||
<span>{text}</span>
|
||||
{hasInfo && info && (
|
||||
<Tooltip content={createMessage(() => info)}>
|
||||
<StyledIcon name="question-line" size="md" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingColumn>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsItem({
|
||||
action,
|
||||
headingCount,
|
||||
onUpdateSettings,
|
||||
renderAdditionalColumns,
|
||||
}: SettingsItemProps) {
|
||||
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
|
||||
|
||||
const [executeOnPageLoad, setExecuteOnPageLoad] = useState(
|
||||
String(!!action.executeOnLoad),
|
||||
);
|
||||
const [confirmBeforeExecute, setConfirmBeforeExecute] = useState(
|
||||
String(!!action.confirmBeforeExecute),
|
||||
);
|
||||
|
||||
const onChangeExecuteOnPageLoad = (value: string) => {
|
||||
setExecuteOnPageLoad(value);
|
||||
onUpdateSettings?.({
|
||||
value: value === "true",
|
||||
propertyName: "executeOnLoad",
|
||||
action,
|
||||
});
|
||||
|
||||
AnalyticsUtil.logEvent("JS_OBJECT_SETTINGS_CHANGED", {
|
||||
toggleSetting: "ON_PAGE_LOAD",
|
||||
toggleValue: value,
|
||||
});
|
||||
};
|
||||
const onChangeConfirmBeforeExecute = (value: string) => {
|
||||
setConfirmBeforeExecute(value);
|
||||
onUpdateSettings?.({
|
||||
value: value === "true",
|
||||
propertyName: "confirmBeforeExecute",
|
||||
action,
|
||||
});
|
||||
|
||||
AnalyticsUtil.logEvent("JS_OBJECT_SETTINGS_CHANGED", {
|
||||
toggleSetting: "CONFIRM_BEFORE_RUN",
|
||||
toggleValue: value,
|
||||
});
|
||||
};
|
||||
|
||||
const showConfirmBeforeExecute = action.confirmBeforeExecute;
|
||||
|
||||
const onRemoveConfirm = useCallback(() => {
|
||||
setShowConfirmationModal(false);
|
||||
onChangeConfirmBeforeExecute("false");
|
||||
}, []);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setShowConfirmationModal(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
className="t--async-js-function-settings"
|
||||
id={`${action.name}-settings`}
|
||||
>
|
||||
<SettingColumn grow headingCount={headingCount}>
|
||||
<span>{action.name}</span>
|
||||
</SettingColumn>
|
||||
<SettingColumn
|
||||
className={`${action.name}-on-page-load-setting`}
|
||||
headingCount={headingCount}
|
||||
>
|
||||
<SwitchWrapper>
|
||||
<Switch
|
||||
defaultSelected={JSON.parse(executeOnPageLoad)}
|
||||
name={`execute-on-page-load-${action.id}`}
|
||||
onChange={(isSelected) =>
|
||||
onChangeExecuteOnPageLoad(String(isSelected))
|
||||
}
|
||||
/>
|
||||
</SwitchWrapper>
|
||||
</SettingColumn>
|
||||
<SettingColumn
|
||||
className={`${action.name}-confirm-before-execute`}
|
||||
headingCount={headingCount}
|
||||
>
|
||||
<SwitchWrapper>
|
||||
{showConfirmBeforeExecute ? (
|
||||
<Switch
|
||||
className="flex justify-center "
|
||||
isSelected={JSON.parse(confirmBeforeExecute)}
|
||||
name={`confirm-before-execute-${action.id}`}
|
||||
onChange={() => setShowConfirmationModal(true)}
|
||||
/>
|
||||
) : null}
|
||||
</SwitchWrapper>
|
||||
</SettingColumn>
|
||||
{renderAdditionalColumns?.(action, headingCount)}
|
||||
<RemoveConfirmationModal
|
||||
isOpen={showConfirmationModal}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onRemoveConfirm}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
function JSFunctionSettingsView({
|
||||
actions,
|
||||
additionalHeadings = [],
|
||||
disabled = false,
|
||||
onUpdateSettings,
|
||||
renderAdditionalColumns,
|
||||
}: JSFunctionSettingsProps) {
|
||||
const showConfirmBeforeExecuteOption = actions.some(
|
||||
(action) => action.confirmBeforeExecute === true,
|
||||
);
|
||||
const headings = [...SETTINGS_HEADINGS, ...additionalHeadings];
|
||||
|
||||
headings.forEach((heading) => {
|
||||
if (heading.key === CONFIRM_BEFORE_CALLING_HEADING.key) {
|
||||
CONFIRM_BEFORE_CALLING_HEADING.hidden = !showConfirmBeforeExecuteOption;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<JSFunctionSettingsWrapper>
|
||||
<SettingsContainer>
|
||||
<h3>{createMessage(FUNCTION_SETTINGS_HEADING)}</h3>
|
||||
<SettingsRowWrapper>
|
||||
<SettingsHeaderWrapper>
|
||||
<SettingRow isHeading>
|
||||
{headings.map((setting, index) => (
|
||||
<SettingsHeading
|
||||
grow={index === 0}
|
||||
hasInfo={setting.hasInfo}
|
||||
headingCount={headings.length}
|
||||
hidden={setting?.hidden}
|
||||
info={setting.info}
|
||||
key={setting.key}
|
||||
text={setting.text}
|
||||
/>
|
||||
))}
|
||||
</SettingRow>
|
||||
</SettingsHeaderWrapper>
|
||||
<SettingsBodyWrapper>
|
||||
{actions && actions.length ? (
|
||||
actions.map((action) => (
|
||||
<SettingsItem
|
||||
action={action}
|
||||
disabled={disabled}
|
||||
headingCount={headings.length}
|
||||
key={action.id}
|
||||
onUpdateSettings={onUpdateSettings}
|
||||
renderAdditionalColumns={renderAdditionalColumns}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SettingRow noBorder>
|
||||
<SettingColumn headingCount={0}>
|
||||
{createMessage(NO_JS_FUNCTIONS)}
|
||||
</SettingColumn>
|
||||
</SettingRow>
|
||||
)}
|
||||
</SettingsBodyWrapper>
|
||||
</SettingsRowWrapper>
|
||||
</SettingsContainer>
|
||||
</JSFunctionSettingsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default JSFunctionSettingsView;
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Icon } from "@appsmith/ads";
|
||||
import DropdownField from "components/editorComponents/form/fields/DropdownField";
|
||||
import { CREATE_NEW_DATASOURCE, createMessage } from "ee/constants/messages";
|
||||
import styled from "styled-components";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import {
|
||||
getHasCreateDatasourcePermission,
|
||||
getHasManageActionPermission,
|
||||
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import type { Action } from "entities/Action";
|
||||
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
|
||||
import { getPluginImages } from "ee/selectors/entitiesSelector";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import type { Plugin } from "entities/Plugin";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { getCurrentAppWorkspace } from "ee/selectors/selectedWorkspaceSelectors";
|
||||
|
||||
const DropdownSelect = styled.div`
|
||||
font-size: 14px;
|
||||
width: 230px;
|
||||
|
||||
.rc-select-selector {
|
||||
min-width: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
const CreateDatasource = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
formName: string;
|
||||
currentActionConfig?: Action;
|
||||
plugin?: Plugin;
|
||||
dataSources: Datasource[];
|
||||
onCreateDatasourceClick: () => void;
|
||||
}
|
||||
|
||||
interface DATASOURCES_OPTIONS_TYPE {
|
||||
label: string;
|
||||
value: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const DatasourceSelector = (props: Props) => {
|
||||
const {
|
||||
currentActionConfig,
|
||||
dataSources,
|
||||
formName,
|
||||
onCreateDatasourceClick,
|
||||
plugin,
|
||||
} = props;
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const userWorkspacePermissions = useSelector(
|
||||
(state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [],
|
||||
);
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
isFeatureEnabled,
|
||||
currentActionConfig?.userPermissions,
|
||||
);
|
||||
const canCreateDatasource = getHasCreateDatasourcePermission(
|
||||
isFeatureEnabled,
|
||||
userWorkspacePermissions,
|
||||
);
|
||||
const showDatasourceSelector = doesPluginRequireDatasource(plugin);
|
||||
const pluginImages = useSelector(getPluginImages);
|
||||
|
||||
const DATASOURCES_OPTIONS: Array<DATASOURCES_OPTIONS_TYPE> =
|
||||
dataSources.reduce(
|
||||
(acc: Array<DATASOURCES_OPTIONS_TYPE>, dataSource: Datasource) => {
|
||||
if (dataSource.pluginId === plugin?.id) {
|
||||
acc.push({
|
||||
label: dataSource.name,
|
||||
value: dataSource.id,
|
||||
image: pluginImages[dataSource.pluginId],
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (!showDatasourceSelector) return null;
|
||||
|
||||
return (
|
||||
<DropdownSelect>
|
||||
<DropdownField
|
||||
className={"t--switch-datasource"}
|
||||
formName={formName}
|
||||
isDisabled={!isChangePermitted}
|
||||
name="datasource.id"
|
||||
options={DATASOURCES_OPTIONS}
|
||||
placeholder="Datasource"
|
||||
>
|
||||
{canCreateDatasource && (
|
||||
// this additional div is here so that rc-select can render the child with the onClick correctly
|
||||
<div>
|
||||
<CreateDatasource onClick={() => onCreateDatasourceClick()}>
|
||||
<Icon className="createIcon" name="plus" size="md" />
|
||||
{createMessage(CREATE_NEW_DATASOURCE)}
|
||||
</CreateDatasource>
|
||||
</div>
|
||||
)}
|
||||
</DropdownField>
|
||||
</DropdownSelect>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasourceSelector;
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
import React from "react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import { getFormValues } from "redux-form";
|
||||
import styled from "styled-components";
|
||||
import type { QueryEditorRouteParams } from "constants/routes";
|
||||
import QueryEditorForm from "./Form";
|
||||
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions";
|
||||
import {
|
||||
deleteAction,
|
||||
runAction,
|
||||
setActionResponseDisplayFormat,
|
||||
setActionProperty,
|
||||
} from "actions/pluginActionActions";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { getCurrentApplicationId } from "selectors/editorSelectors";
|
||||
import { QUERY_EDITOR_FORM_NAME } from "ee/constants/forms";
|
||||
import { type Plugin, UIComponentTypes } from "entities/Plugin";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import {
|
||||
getPluginIdsOfPackageNames,
|
||||
getPlugins,
|
||||
getActionByBaseId,
|
||||
getActionResponses,
|
||||
getDatasourceByPluginId,
|
||||
getDBAndRemoteDatasources,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants";
|
||||
import type { QueryAction, SaaSAction } from "entities/Action";
|
||||
import Spinner from "components/editorComponents/Spinner";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { initFormEvaluations } from "actions/evaluationActions";
|
||||
import { getUIComponent } from "./helpers";
|
||||
import type { Diff } from "deep-diff";
|
||||
import { diff } from "deep-diff";
|
||||
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
|
||||
import { getConfigInitialValues } from "components/formControls/utils";
|
||||
import { merge } from "lodash";
|
||||
import { getPathAndValueFromActionDiffObject } from "../../../utils/getPathAndValueFromActionDiffObject";
|
||||
import { getCurrentEnvironmentDetails } from "ee/selectors/environmentSelectors";
|
||||
import { QueryEditorContext } from "./QueryEditorContext";
|
||||
import {
|
||||
isActionDeleting,
|
||||
isActionRunning,
|
||||
isPluginActionCreating,
|
||||
} from "PluginActionEditor/store";
|
||||
|
||||
const EmptyStateContainer = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
const LoadingContainer = styled(CenteredWrapper)`
|
||||
height: 50%;
|
||||
`;
|
||||
|
||||
interface ReduxDispatchProps {
|
||||
runAction: (actionId: string) => void;
|
||||
deleteAction: (id: string, name: string) => void;
|
||||
initFormEvaluation: (
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editorConfig: any,
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settingConfig: any,
|
||||
formId: string,
|
||||
) => void;
|
||||
updateActionResponseDisplayFormat: ({
|
||||
field,
|
||||
id,
|
||||
value,
|
||||
}: UpdateActionPropertyActionPayload) => void;
|
||||
setActionProperty: (
|
||||
actionId: string,
|
||||
propertyName: string,
|
||||
value: string,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface ReduxStateProps {
|
||||
plugins: Plugin[];
|
||||
dataSources: Datasource[];
|
||||
isRunning: boolean;
|
||||
isDeleting: boolean;
|
||||
formData: QueryAction | SaaSAction;
|
||||
runErrorMessage: Record<string, string>;
|
||||
pluginId: string | undefined;
|
||||
pluginIds: Array<string> | undefined;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
responses: any;
|
||||
isCreating: boolean;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editorConfig: any;
|
||||
uiComponent: UIComponentTypes;
|
||||
applicationId: string;
|
||||
actionId: string;
|
||||
baseActionId: string;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actionObjectDiff?: any;
|
||||
isSaas: boolean;
|
||||
datasourceId?: string;
|
||||
currentEnvironmentId: string;
|
||||
currentEnvironmentName: string;
|
||||
}
|
||||
|
||||
type StateAndRouteProps = RouteComponentProps<QueryEditorRouteParams>;
|
||||
type OwnProps = StateAndRouteProps & {
|
||||
isEditorInitialized: boolean;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settingsConfig: any;
|
||||
};
|
||||
type Props = ReduxDispatchProps & ReduxStateProps & OwnProps;
|
||||
|
||||
class QueryEditor extends React.Component<Props> {
|
||||
static contextType = QueryEditorContext;
|
||||
context!: React.ContextType<typeof QueryEditorContext>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Call the first evaluations when the page loads
|
||||
// call evaluations only for queries and not google sheets (which uses apiId)
|
||||
if (this.props.match.params.baseQueryId) {
|
||||
this.props.initFormEvaluation(
|
||||
this.props.editorConfig,
|
||||
this.props.settingsConfig,
|
||||
this.props.match.params.baseQueryId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// if the current action is non existent, do not dispatch change query page action
|
||||
// this action should only be dispatched when switching from an existent action.
|
||||
if (!this.props.pluginId) return;
|
||||
|
||||
this.context?.changeQueryPage?.(this.props.baseActionId);
|
||||
|
||||
// fixes missing where key issue by populating the action with a where object when the component is mounted.
|
||||
if (this.props.isSaas) {
|
||||
const { path = "", value = "" } = {
|
||||
...getPathAndValueFromActionDiffObject(this.props.actionObjectDiff),
|
||||
};
|
||||
|
||||
if (value && path) {
|
||||
this.props.setActionProperty(this.props.actionId, path, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { formData } = this.props;
|
||||
|
||||
this.props.deleteAction(this.props.actionId, formData.name);
|
||||
};
|
||||
|
||||
handleRunClick = () => {
|
||||
const { dataSources } = this.props;
|
||||
const datasource = dataSources.find(
|
||||
(datasource) => datasource.id === this.props.datasourceId,
|
||||
);
|
||||
const pluginName = this.props.plugins.find(
|
||||
(plugin) => plugin.id === this.props.pluginId,
|
||||
)?.name;
|
||||
|
||||
AnalyticsUtil.logEvent("RUN_QUERY_CLICK", {
|
||||
actionId: this.props.actionId,
|
||||
dataSourceSize: dataSources.length,
|
||||
environmentId: this.props.currentEnvironmentId,
|
||||
environmentName: this.props.currentEnvironmentName,
|
||||
pluginName: pluginName,
|
||||
datasourceId: datasource?.id,
|
||||
isMock: !!datasource?.isMock,
|
||||
});
|
||||
this.props.runAction(this.props.actionId);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// Update the page when the queryID is changed by changing the
|
||||
// URL or selecting new query from the query pane
|
||||
// reusing same logic for changing query panes for switching query editor datasources, since the operations are similar.
|
||||
if (
|
||||
prevProps.baseActionId !== this.props.baseActionId ||
|
||||
prevProps.pluginId !== this.props.pluginId
|
||||
) {
|
||||
this.context?.changeQueryPage?.(this.props.baseActionId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
actionId,
|
||||
dataSources,
|
||||
editorConfig,
|
||||
isCreating,
|
||||
isDeleting,
|
||||
isEditorInitialized,
|
||||
isRunning,
|
||||
pluginId,
|
||||
pluginIds,
|
||||
responses,
|
||||
runErrorMessage,
|
||||
uiComponent,
|
||||
updateActionResponseDisplayFormat,
|
||||
} = this.props;
|
||||
const { onCreateDatasourceClick, onEntityNotFoundBackClick } = this.context;
|
||||
|
||||
// if the action can not be found, generate a entity not found page
|
||||
if (!pluginId && actionId) {
|
||||
return <EntityNotFoundPane goBackFn={onEntityNotFoundBackClick} />;
|
||||
}
|
||||
|
||||
if (!pluginIds?.length) {
|
||||
return (
|
||||
<EmptyStateContainer>{"Plugin is not installed"}</EmptyStateContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCreating || !isEditorInitialized) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Spinner size={30} />
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryEditorForm
|
||||
actionResponse={responses[actionId]}
|
||||
dataSources={dataSources}
|
||||
datasourceId={this.props.datasourceId}
|
||||
editorConfig={editorConfig}
|
||||
formData={this.props.formData}
|
||||
isDeleting={isDeleting}
|
||||
isRunning={isRunning}
|
||||
location={this.props.location}
|
||||
onCreateDatasourceClick={onCreateDatasourceClick}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onRunClick={this.handleRunClick}
|
||||
pluginId={this.props.pluginId}
|
||||
runErrorMessage={runErrorMessage[actionId]}
|
||||
settingConfig={this.props.settingsConfig}
|
||||
uiComponent={uiComponent}
|
||||
updateActionResponseDisplayFormat={updateActionResponseDisplayFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, props: OwnProps): ReduxStateProps => {
|
||||
const { baseApiId, baseQueryId } = props.match.params;
|
||||
const baseActionId = baseQueryId || baseApiId || "";
|
||||
const { runErrorMessage } = state.ui.pluginActionEditor;
|
||||
const { plugins } = state.entities;
|
||||
|
||||
const { editorConfigs } = plugins;
|
||||
|
||||
const action = getActionByBaseId(state, baseActionId) as
|
||||
| QueryAction
|
||||
| SaaSAction;
|
||||
const actionId = action?.id;
|
||||
|
||||
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as
|
||||
| QueryAction
|
||||
| SaaSAction;
|
||||
let pluginId;
|
||||
|
||||
if (action) {
|
||||
pluginId = action.pluginId;
|
||||
}
|
||||
|
||||
const isCreating = isPluginActionCreating(state);
|
||||
const isDeleting = isActionDeleting(actionId)(state);
|
||||
const isRunning = isActionRunning(actionId)(state);
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let editorConfig: any;
|
||||
|
||||
if (editorConfigs && pluginId) {
|
||||
editorConfig = editorConfigs[pluginId];
|
||||
}
|
||||
|
||||
const initialValues = {};
|
||||
|
||||
if (editorConfig) {
|
||||
merge(initialValues, getConfigInitialValues(editorConfig));
|
||||
}
|
||||
|
||||
if (props.settingsConfig) {
|
||||
merge(initialValues, getConfigInitialValues(props.settingsConfig));
|
||||
}
|
||||
|
||||
// initialValues contains merge of action, editorConfig, settingsConfig and will be passed to redux form
|
||||
merge(initialValues, action);
|
||||
|
||||
// @ts-expect-error: Types are not available
|
||||
const actionObjectDiff: undefined | Diff<Action | undefined, Action>[] = diff(
|
||||
action,
|
||||
initialValues,
|
||||
);
|
||||
|
||||
const allPlugins = getPlugins(state);
|
||||
let uiComponent = UIComponentTypes.DbEditorForm;
|
||||
|
||||
if (!!pluginId) uiComponent = getUIComponent(pluginId, allPlugins);
|
||||
|
||||
const currentEnvDetails = getCurrentEnvironmentDetails(state);
|
||||
|
||||
return {
|
||||
actionId,
|
||||
baseActionId,
|
||||
currentEnvironmentId: currentEnvDetails?.id || "",
|
||||
currentEnvironmentName: currentEnvDetails?.name || "",
|
||||
pluginId,
|
||||
plugins: allPlugins,
|
||||
runErrorMessage,
|
||||
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
|
||||
dataSources: !!baseApiId
|
||||
? getDatasourceByPluginId(state, action?.pluginId)
|
||||
: getDBAndRemoteDatasources(state),
|
||||
responses: getActionResponses(state),
|
||||
isCreating,
|
||||
isRunning,
|
||||
isDeleting,
|
||||
isSaas: !!baseApiId,
|
||||
formData,
|
||||
editorConfig,
|
||||
uiComponent,
|
||||
applicationId: getCurrentApplicationId(state),
|
||||
actionObjectDiff,
|
||||
datasourceId: action?.datasource?.id,
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
|
||||
deleteAction: (id: string, name: string) =>
|
||||
dispatch(deleteAction({ id, name })),
|
||||
runAction: (actionId: string) => dispatch(runAction(actionId)),
|
||||
initFormEvaluation: (
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editorConfig: any,
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settingsConfig: any,
|
||||
formId: string,
|
||||
) => {
|
||||
dispatch(initFormEvaluations(editorConfig, settingsConfig, formId));
|
||||
},
|
||||
updateActionResponseDisplayFormat: ({
|
||||
field,
|
||||
id,
|
||||
value,
|
||||
}: UpdateActionPropertyActionPayload) => {
|
||||
dispatch(setActionResponseDisplayFormat({ id, field, value }));
|
||||
},
|
||||
setActionProperty: (
|
||||
actionId: string,
|
||||
propertyName: string,
|
||||
value: string,
|
||||
) => {
|
||||
dispatch(setActionProperty({ actionId, propertyName, value }));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryEditor);
|
||||
|
|
@ -1,382 +0,0 @@
|
|||
import { useContext } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import type { InjectedFormProps } from "redux-form";
|
||||
import { noop } from "lodash";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import type { Action, QueryAction, SaaSAction } from "entities/Action";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import ActionSettings from "pages/Editor/ActionSettings";
|
||||
import { Button, Tab, TabPanel, Tabs, TabsList, Tooltip } from "@appsmith/ads";
|
||||
import styled from "styled-components";
|
||||
import FormRow from "components/editorComponents/FormRow";
|
||||
import {
|
||||
createMessage,
|
||||
DOCUMENTATION,
|
||||
DOCUMENTATION_TOOLTIP,
|
||||
} from "ee/constants/messages";
|
||||
import { useParams } from "react-router";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { thinScrollbar } from "constants/DefaultTheme";
|
||||
import type { ActionResponse } from "api/ActionAPI";
|
||||
import type { Plugin, UIComponentTypes } from "entities/Plugin";
|
||||
import { EDITOR_TABS, SQL_DATASOURCES } from "constants/QueryEditorConstants";
|
||||
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
|
||||
import {
|
||||
getPluginActionConfigSelectedTab,
|
||||
setPluginActionEditorSelectedTab,
|
||||
} from "PluginActionEditor/store";
|
||||
import type { SourceEntity } from "entities/AppsmithConsole";
|
||||
import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
|
||||
import { DocsLink, openDoc } from "constants/DocumentationLinks";
|
||||
import { QueryEditorContext } from "./QueryEditorContext";
|
||||
import QueryDebuggerTabs from "./QueryDebuggerTabs";
|
||||
import useShowSchema from "PluginActionEditor/components/PluginActionResponse/hooks/useShowSchema";
|
||||
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
|
||||
import FormRender from "PluginActionEditor/components/PluginActionForm/components/UQIEditor/FormRender";
|
||||
import QueryEditorHeader from "./QueryEditorHeader";
|
||||
import RunHistory from "ee/components/RunHistory";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import { getHasExecuteActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { getPluginNameFromId } from "ee/selectors/entitiesSelector";
|
||||
|
||||
const QueryFormContainer = styled.form`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: var(--ads-v2-spaces-5) 0 0;
|
||||
width: 100%;
|
||||
.statementTextArea {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.queryInput {
|
||||
max-width: 30%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.executeOnLoad {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SettingsWrapper = styled.div`
|
||||
${thinScrollbar};
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const SecondaryWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const StyledFormRow = styled(FormRow)`
|
||||
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-5)
|
||||
var(--ads-v2-spaces-7);
|
||||
flex: 0;
|
||||
`;
|
||||
|
||||
const TabContainerView = styled.div`
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
${thinScrollbar}
|
||||
a {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
position: relative;
|
||||
|
||||
& > .ads-v2-tabs {
|
||||
height: 100%;
|
||||
|
||||
& > .ads-v2-tabs__panel {
|
||||
height: calc(100% - 50px);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TabsListWrapper = styled.div`
|
||||
padding: 0 var(--ads-v2-spaces-7);
|
||||
`;
|
||||
|
||||
const TabPanelWrapper = styled(TabPanel)`
|
||||
padding: 0 var(--ads-v2-spaces-7);
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100% - 50px);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const DocumentationButton = styled(Button)`
|
||||
position: absolute !important;
|
||||
right: 24px;
|
||||
margin: 7px 0 0;
|
||||
z-index: 6;
|
||||
`;
|
||||
|
||||
export const SegmentedControlContainer = styled.div`
|
||||
padding: 0 var(--ads-v2-spaces-7);
|
||||
padding-top: var(--ads-v2-spaces-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ads-v2-spaces-4);
|
||||
overflow-y: clip;
|
||||
overflow-x: scroll;
|
||||
`;
|
||||
|
||||
const StyledNotificationWrapper = styled.div`
|
||||
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-3)
|
||||
var(--ads-v2-spaces-7);
|
||||
`;
|
||||
|
||||
interface QueryFormProps {
|
||||
onDeleteClick: () => void;
|
||||
onRunClick: () => void;
|
||||
onCreateDatasourceClick: () => void;
|
||||
isDeleting: boolean;
|
||||
isRunning: boolean;
|
||||
dataSources: Datasource[];
|
||||
uiComponent: UIComponentTypes;
|
||||
actionResponse?: ActionResponse;
|
||||
runErrorMessage: string | undefined;
|
||||
location: {
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
state: any;
|
||||
};
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editorConfig?: any;
|
||||
formName: string;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settingConfig: any;
|
||||
formData: SaaSAction | QueryAction;
|
||||
responseDisplayFormat: { title: string; value: string };
|
||||
datasourceId: string;
|
||||
showCloseEditor: boolean;
|
||||
}
|
||||
|
||||
interface ReduxProps {
|
||||
actionName: string;
|
||||
plugin?: Plugin;
|
||||
pluginId: string;
|
||||
documentationLink: string | undefined;
|
||||
formEvaluationState: FormEvalOutput;
|
||||
}
|
||||
|
||||
export type EditorJSONtoFormProps = QueryFormProps & ReduxProps;
|
||||
|
||||
type Props = EditorJSONtoFormProps &
|
||||
InjectedFormProps<Action, EditorJSONtoFormProps>;
|
||||
|
||||
export function EditorJSONtoForm(props: Props) {
|
||||
const {
|
||||
actionName,
|
||||
actionResponse,
|
||||
dataSources,
|
||||
documentationLink,
|
||||
editorConfig,
|
||||
formName,
|
||||
handleSubmit,
|
||||
isRunning,
|
||||
onCreateDatasourceClick,
|
||||
onRunClick,
|
||||
plugin,
|
||||
runErrorMessage,
|
||||
settingConfig,
|
||||
uiComponent,
|
||||
} = props;
|
||||
|
||||
const { actionRightPaneAdditionSections, notification } =
|
||||
useContext(QueryEditorContext);
|
||||
|
||||
const params = useParams<{ baseApiId?: string; baseQueryId?: string }>();
|
||||
// fetch the error count from the store.
|
||||
const actions: Action[] = useSelector((state: AppState) =>
|
||||
state.entities.actions.map((action) => action.config),
|
||||
);
|
||||
const currentActionConfig: Action | undefined = actions.find(
|
||||
(action) =>
|
||||
action.baseId === params.baseApiId ||
|
||||
action.baseId === params.baseQueryId,
|
||||
);
|
||||
|
||||
const pluginRequireDatasource = doesPluginRequireDatasource(plugin);
|
||||
|
||||
const showSchema =
|
||||
useShowSchema(currentActionConfig?.pluginId || "") &&
|
||||
pluginRequireDatasource;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleDocumentationClick = () => {
|
||||
openDoc(DocsLink.QUERY, plugin?.documentationLink, plugin?.name);
|
||||
};
|
||||
|
||||
// action source for analytics.
|
||||
const actionSource: SourceEntity = {
|
||||
type: SOURCE_ENTITY_TYPE.ACTION,
|
||||
name: currentActionConfig ? currentActionConfig.name : "",
|
||||
id: currentActionConfig ? currentActionConfig.id : "",
|
||||
};
|
||||
|
||||
const selectedTab = useSelector(getPluginActionConfigSelectedTab);
|
||||
|
||||
const setSelectedConfigTab = useCallback(
|
||||
(selectedIndex: string) => {
|
||||
dispatch(setPluginActionEditorSelectedTab(selectedIndex));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const isExecutePermitted = getHasExecuteActionPermission(
|
||||
isFeatureEnabled,
|
||||
currentActionConfig?.userPermissions,
|
||||
);
|
||||
|
||||
// get the current action's plugin name
|
||||
const currentActionPluginName = useSelector((state: AppState) =>
|
||||
getPluginNameFromId(state, currentActionConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
let actionBody = "";
|
||||
|
||||
if (!!currentActionConfig?.actionConfiguration) {
|
||||
if ("formData" in currentActionConfig?.actionConfiguration) {
|
||||
// if the action has a formData (the action is postUQI e.g. Oracle)
|
||||
actionBody =
|
||||
currentActionConfig.actionConfiguration.formData?.body?.data || "";
|
||||
} else {
|
||||
// if the action is pre UQI, the path is different e.g. mySQL
|
||||
actionBody = currentActionConfig.actionConfiguration?.body || "";
|
||||
}
|
||||
}
|
||||
|
||||
// if (the body is empty and the action is an sql datasource) or the user does not have permission, block action execution.
|
||||
const blockExecution =
|
||||
(!actionBody && SQL_DATASOURCES.includes(currentActionPluginName)) ||
|
||||
!isExecutePermitted;
|
||||
|
||||
// when switching between different redux forms, make sure this redux form has been initialized before rendering anything.
|
||||
// the initialized prop below comes from redux-form.
|
||||
if (!props.initialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryFormContainer onSubmit={handleSubmit(noop)}>
|
||||
<QueryEditorHeader
|
||||
dataSources={dataSources}
|
||||
formName={formName}
|
||||
isRunDisabled={blockExecution}
|
||||
isRunning={isRunning}
|
||||
onCreateDatasourceClick={onCreateDatasourceClick}
|
||||
onRunClick={onRunClick}
|
||||
plugin={plugin}
|
||||
/>
|
||||
{notification && (
|
||||
<StyledNotificationWrapper>{notification}</StyledNotificationWrapper>
|
||||
)}
|
||||
<Wrapper>
|
||||
<div className="flex flex-1 w-full">
|
||||
<SecondaryWrapper>
|
||||
<TabContainerView>
|
||||
<Tabs
|
||||
onValueChange={setSelectedConfigTab}
|
||||
value={selectedTab || EDITOR_TABS.QUERY}
|
||||
>
|
||||
<TabsListWrapper>
|
||||
<TabsList>
|
||||
<Tab
|
||||
data-testid={`t--query-editor-` + EDITOR_TABS.QUERY}
|
||||
value={EDITOR_TABS.QUERY}
|
||||
>
|
||||
Query
|
||||
</Tab>
|
||||
<Tab
|
||||
data-testid={`t--query-editor-` + EDITOR_TABS.SETTINGS}
|
||||
value={EDITOR_TABS.SETTINGS}
|
||||
>
|
||||
Settings
|
||||
</Tab>
|
||||
</TabsList>
|
||||
</TabsListWrapper>
|
||||
<TabPanelWrapper
|
||||
className="tab-panel"
|
||||
value={EDITOR_TABS.QUERY}
|
||||
>
|
||||
<SettingsWrapper
|
||||
data-testid={`t--action-form-${plugin?.type}`}
|
||||
>
|
||||
<FormRender
|
||||
editorConfig={editorConfig}
|
||||
formData={props.formData}
|
||||
formEvaluationState={props.formEvaluationState}
|
||||
formName={formName}
|
||||
uiComponent={uiComponent}
|
||||
/>
|
||||
</SettingsWrapper>
|
||||
</TabPanelWrapper>
|
||||
<TabPanelWrapper value={EDITOR_TABS.SETTINGS}>
|
||||
<SettingsWrapper>
|
||||
<ActionSettings
|
||||
actionSettingsConfig={settingConfig}
|
||||
formName={formName}
|
||||
/>
|
||||
</SettingsWrapper>
|
||||
</TabPanelWrapper>
|
||||
</Tabs>
|
||||
{documentationLink && (
|
||||
<Tooltip
|
||||
content={createMessage(DOCUMENTATION_TOOLTIP)}
|
||||
placement="top"
|
||||
>
|
||||
<DocumentationButton
|
||||
className="t--datasource-documentation-link"
|
||||
kind="tertiary"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleDocumentationClick();
|
||||
}}
|
||||
size="sm"
|
||||
startIcon="book-line"
|
||||
>
|
||||
{createMessage(DOCUMENTATION)}
|
||||
</DocumentationButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TabContainerView>
|
||||
<QueryDebuggerTabs
|
||||
actionName={actionName}
|
||||
actionResponse={actionResponse}
|
||||
actionSource={actionSource}
|
||||
currentActionConfig={currentActionConfig}
|
||||
isRunDisabled={blockExecution}
|
||||
isRunning={isRunning}
|
||||
onRunClick={onRunClick}
|
||||
runErrorMessage={runErrorMessage}
|
||||
showSchema={showSchema}
|
||||
/>
|
||||
<RunHistory />
|
||||
</SecondaryWrapper>
|
||||
</div>
|
||||
{actionRightPaneAdditionSections}
|
||||
</Wrapper>
|
||||
</QueryFormContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { formValueSelector, reduxForm } from "redux-form";
|
||||
import { QUERY_EDITOR_FORM_NAME } from "ee/constants/forms";
|
||||
import type { Action } from "entities/Action";
|
||||
import { connect } from "react-redux";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import {
|
||||
getPluginResponseTypes,
|
||||
getPluginDocumentationLinks,
|
||||
getPlugin,
|
||||
getActionData,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import type { EditorJSONtoFormProps } from "./EditorJSONtoForm";
|
||||
import { EditorJSONtoForm } from "./EditorJSONtoForm";
|
||||
import { getFormEvaluationState } from "selectors/formSelectors";
|
||||
import { actionResponseDisplayDataFormats } from "../utils";
|
||||
|
||||
const valueSelector = formValueSelector(QUERY_EDITOR_FORM_NAME);
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mapStateToProps = (state: AppState, props: any) => {
|
||||
const actionId = valueSelector(state, "id");
|
||||
const actionName = valueSelector(state, "name");
|
||||
const pluginId = valueSelector(state, "datasource.pluginId");
|
||||
const selectedDbId = valueSelector(state, "datasource.id");
|
||||
const actionData = getActionData(state, actionId);
|
||||
const { responseDataTypes, responseDisplayFormat } =
|
||||
actionResponseDisplayDataFormats(actionData);
|
||||
|
||||
const responseTypes = getPluginResponseTypes(state);
|
||||
const documentationLinks = getPluginDocumentationLinks(state);
|
||||
const plugin = getPlugin(state, pluginId);
|
||||
// State to manage the evaluations for the form
|
||||
let formEvaluationState = {};
|
||||
|
||||
// Fetching evaluations state only once the formData is populated
|
||||
if (!!props.formData) {
|
||||
formEvaluationState = getFormEvaluationState(state)[props.formData.id];
|
||||
}
|
||||
|
||||
return {
|
||||
actionName,
|
||||
plugin,
|
||||
pluginId,
|
||||
selectedDbId,
|
||||
responseDataTypes,
|
||||
responseDisplayFormat,
|
||||
responseType: responseTypes[pluginId],
|
||||
documentationLink: documentationLinks[pluginId],
|
||||
formName: QUERY_EDITOR_FORM_NAME,
|
||||
formEvaluationState,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(
|
||||
reduxForm<Action, EditorJSONtoFormProps>({
|
||||
form: QUERY_EDITOR_FORM_NAME,
|
||||
enableReinitialize: true,
|
||||
})(EditorJSONtoForm),
|
||||
);
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import configureStore from "redux-mock-store";
|
||||
import { Provider } from "react-redux";
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { unitTestBaseMockStore } from "layoutSystems/common/dropTarget/unitTestUtils";
|
||||
import { lightTheme } from "selectors/themeSelectors";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import QueryDebuggerTabs from "./QueryDebuggerTabs";
|
||||
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
|
||||
|
||||
const mockStore = configureStore([]);
|
||||
|
||||
const storeState = {
|
||||
...unitTestBaseMockStore,
|
||||
evaluations: {
|
||||
tree: {},
|
||||
},
|
||||
entities: {
|
||||
plugins: {
|
||||
list: [],
|
||||
},
|
||||
datasources: {
|
||||
structure: {},
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
...unitTestBaseMockStore.ui,
|
||||
users: {
|
||||
featureFlag: {
|
||||
data: {},
|
||||
overriddenFlags: {},
|
||||
},
|
||||
},
|
||||
ide: {
|
||||
view: EditorViewMode.FullScreen,
|
||||
},
|
||||
debugger: {
|
||||
context: {
|
||||
errorCount: 0,
|
||||
},
|
||||
},
|
||||
pluginActionEditor: {
|
||||
debugger: {
|
||||
open: true,
|
||||
responseTabHeight: 200,
|
||||
selectedTab: "response",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("ApiResponseView", () => {
|
||||
let store = mockStore(storeState);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(storeState);
|
||||
});
|
||||
|
||||
it("the container should have class select-text to enable the selection of text for user", () => {
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Router>
|
||||
<QueryDebuggerTabs
|
||||
actionName="Query1"
|
||||
actionSource={{
|
||||
id: "ID1",
|
||||
name: "Query1",
|
||||
type: ENTITY_TYPE.ACTION,
|
||||
}}
|
||||
isRunning={false}
|
||||
onRunClick={() => {}}
|
||||
/>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
container
|
||||
.querySelector(".t--query-bottom-pane-container")
|
||||
?.classList.contains("select-text"),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import type { ReduxAction } from "actions/ReduxActionTypes";
|
||||
import type { SaveActionNameParams } from "PluginActionEditor";
|
||||
import React, { createContext, useMemo } from "react";
|
||||
|
||||
interface QueryEditorContextContextProps {
|
||||
moreActionsMenu?: React.ReactNode;
|
||||
onCreateDatasourceClick?: () => void;
|
||||
onEntityNotFoundBackClick?: () => void;
|
||||
changeQueryPage?: (baseQueryId: string) => void;
|
||||
actionRightPaneBackLink?: React.ReactNode;
|
||||
saveActionName: (
|
||||
params: SaveActionNameParams,
|
||||
) => ReduxAction<SaveActionNameParams>;
|
||||
actionRightPaneAdditionSections?: React.ReactNode;
|
||||
showSuggestedWidgets?: boolean;
|
||||
notification?: string | React.ReactNode;
|
||||
}
|
||||
|
||||
type QueryEditorContextProviderProps =
|
||||
React.PropsWithChildren<QueryEditorContextContextProps>;
|
||||
|
||||
export const QueryEditorContext = createContext<QueryEditorContextContextProps>(
|
||||
{} as QueryEditorContextContextProps,
|
||||
);
|
||||
|
||||
export function QueryEditorContextProvider({
|
||||
actionRightPaneAdditionSections,
|
||||
actionRightPaneBackLink,
|
||||
changeQueryPage,
|
||||
children,
|
||||
moreActionsMenu,
|
||||
notification,
|
||||
onCreateDatasourceClick,
|
||||
onEntityNotFoundBackClick,
|
||||
saveActionName,
|
||||
showSuggestedWidgets,
|
||||
}: QueryEditorContextProviderProps) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
actionRightPaneBackLink,
|
||||
actionRightPaneAdditionSections,
|
||||
changeQueryPage,
|
||||
moreActionsMenu,
|
||||
onCreateDatasourceClick,
|
||||
onEntityNotFoundBackClick,
|
||||
saveActionName,
|
||||
showSuggestedWidgets,
|
||||
notification,
|
||||
}),
|
||||
[
|
||||
actionRightPaneBackLink,
|
||||
actionRightPaneAdditionSections,
|
||||
changeQueryPage,
|
||||
moreActionsMenu,
|
||||
onCreateDatasourceClick,
|
||||
onEntityNotFoundBackClick,
|
||||
saveActionName,
|
||||
showSuggestedWidgets,
|
||||
notification,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryEditorContext.Provider value={value}>
|
||||
{children}
|
||||
</QueryEditorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import React, { useContext } from "react";
|
||||
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
|
||||
import { Button } from "@appsmith/ads";
|
||||
import { StyledFormRow } from "./EditorJSONtoForm";
|
||||
import styled from "styled-components";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getActionByBaseId, getPlugin } from "ee/selectors/entitiesSelector";
|
||||
import { QueryEditorContext } from "./QueryEditorContext";
|
||||
import type { Plugin } from "entities/Plugin";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import DatasourceSelector from "./DatasourceSelector";
|
||||
import { getSavingStatusForActionName } from "selectors/actionSelectors";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { ActionUrlIcon } from "../Explorer/ExplorerIcons";
|
||||
|
||||
const NameWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
input {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionsWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 50%;
|
||||
justify-content: flex-end;
|
||||
gap: var(--ads-v2-spaces-3);
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
plugin?: Plugin;
|
||||
formName: string;
|
||||
dataSources: Datasource[];
|
||||
onCreateDatasourceClick: () => void;
|
||||
isRunDisabled?: boolean;
|
||||
isRunning: boolean;
|
||||
onRunClick: () => void;
|
||||
}
|
||||
|
||||
const QueryEditorHeader = (props: Props) => {
|
||||
const {
|
||||
dataSources,
|
||||
formName,
|
||||
isRunDisabled = false,
|
||||
isRunning,
|
||||
onCreateDatasourceClick,
|
||||
onRunClick,
|
||||
plugin,
|
||||
} = props;
|
||||
const { moreActionsMenu, saveActionName } = useContext(QueryEditorContext);
|
||||
|
||||
const activeActionBaseId = useActiveActionBaseId();
|
||||
const currentActionConfig = useSelector((state) =>
|
||||
activeActionBaseId
|
||||
? getActionByBaseId(state, activeActionBaseId)
|
||||
: undefined,
|
||||
);
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
isFeatureEnabled,
|
||||
currentActionConfig?.userPermissions,
|
||||
);
|
||||
|
||||
const isDatasourceSelectorEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_ide_datasource_selector_enabled,
|
||||
);
|
||||
|
||||
const currentPlugin = useSelector((state: AppState) =>
|
||||
getPlugin(state, currentActionConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
const saveStatus = useSelector((state) =>
|
||||
getSavingStatusForActionName(state, currentActionConfig?.id || ""),
|
||||
);
|
||||
|
||||
const iconUrl = getAssetUrl(currentPlugin?.iconLocation) || "";
|
||||
|
||||
const icon = ActionUrlIcon(iconUrl);
|
||||
|
||||
return (
|
||||
<StyledFormRow>
|
||||
<NameWrapper>
|
||||
<ActionNameEditor
|
||||
actionConfig={currentActionConfig}
|
||||
disabled={!isChangePermitted}
|
||||
icon={icon}
|
||||
saveActionName={saveActionName}
|
||||
saveStatus={saveStatus}
|
||||
/>
|
||||
</NameWrapper>
|
||||
<ActionsWrapper>
|
||||
{moreActionsMenu}
|
||||
{isDatasourceSelectorEnabled && (
|
||||
<DatasourceSelector
|
||||
currentActionConfig={currentActionConfig}
|
||||
dataSources={dataSources}
|
||||
formName={formName}
|
||||
onCreateDatasourceClick={onCreateDatasourceClick}
|
||||
plugin={plugin}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="t--run-query"
|
||||
data-guided-tour-iid="run-query"
|
||||
isDisabled={isRunDisabled}
|
||||
isLoading={isRunning}
|
||||
onClick={onRunClick}
|
||||
size="md"
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</ActionsWrapper>
|
||||
</StyledFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryEditorHeader;
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import React, { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import Editor from "./Editor";
|
||||
import history from "utils/history";
|
||||
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
|
||||
import BackToCanvas from "components/common/BackToCanvas";
|
||||
import { INTEGRATION_TABS } from "constants/routes";
|
||||
import {
|
||||
getCurrentApplicationId,
|
||||
getIsEditorInitialized,
|
||||
getPagePermissions,
|
||||
} from "selectors/editorSelectors";
|
||||
import { changeQuery } from "PluginActionEditor/store";
|
||||
import { DatasourceCreateEntryPoints } from "constants/Datasource";
|
||||
import {
|
||||
getActionByBaseId,
|
||||
getIsActionConverting,
|
||||
getPluginImages,
|
||||
getPluginSettingConfigs,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { integrationEditorURL } from "ee/RouteBuilder";
|
||||
import { QueryEditorContextProvider } from "./QueryEditorContext";
|
||||
import type { QueryEditorRouteParams } from "constants/routes";
|
||||
import {
|
||||
getHasCreateActionPermission,
|
||||
getHasDeleteActionPermission,
|
||||
getHasManageActionPermission,
|
||||
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import Disabler from "pages/common/Disabler";
|
||||
import ConvertToModuleInstanceCTA from "ee/pages/Editor/EntityEditor/ConvertToModuleInstanceCTA";
|
||||
import { MODULE_TYPE } from "ee/constants/ModuleConstants";
|
||||
import ConvertEntityNotification from "ee/pages/common/ConvertEntityNotification";
|
||||
import { PluginType } from "entities/Plugin";
|
||||
import { Icon } from "@appsmith/ads";
|
||||
import { resolveIcon } from "../utils";
|
||||
import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
|
||||
import { getIDEViewMode } from "selectors/ideSelectors";
|
||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||
import { saveActionName } from "actions/pluginActionActions";
|
||||
|
||||
type QueryEditorProps = RouteComponentProps<QueryEditorRouteParams>;
|
||||
|
||||
function QueryEditor(props: QueryEditorProps) {
|
||||
const { baseApiId, basePageId, baseQueryId } = props.match.params;
|
||||
const baseActionId = baseQueryId || baseApiId;
|
||||
const dispatch = useDispatch();
|
||||
const action = useSelector((state) =>
|
||||
getActionByBaseId(state, baseActionId || ""),
|
||||
);
|
||||
const pluginId = action?.pluginId || "";
|
||||
const isEditorInitialized = useSelector(getIsEditorInitialized);
|
||||
const applicationId: string = useSelector(getCurrentApplicationId);
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const settingsConfig = useSelector((state) =>
|
||||
getPluginSettingConfigs(state, pluginId),
|
||||
);
|
||||
const pagePermissions = useSelector(getPagePermissions);
|
||||
const isConverting = useSelector((state) =>
|
||||
getIsActionConverting(state, action?.id || ""),
|
||||
);
|
||||
const pluginImages = useSelector(getPluginImages);
|
||||
const editorMode = useSelector(getIDEViewMode);
|
||||
const icon = resolveIcon({
|
||||
iconLocation: pluginImages[pluginId] || "",
|
||||
pluginType: action?.pluginType || "",
|
||||
moduleType: action?.actionConfiguration?.body?.moduleType,
|
||||
}) || (
|
||||
<EntityIcon
|
||||
height={`${ENTITY_ICON_SIZE}px`}
|
||||
width={`${ENTITY_ICON_SIZE}px`}
|
||||
>
|
||||
<Icon name="module" />
|
||||
</EntityIcon>
|
||||
);
|
||||
|
||||
const isDeletePermitted = getHasDeleteActionPermission(
|
||||
isFeatureEnabled,
|
||||
action?.userPermissions,
|
||||
);
|
||||
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
isFeatureEnabled,
|
||||
action?.userPermissions,
|
||||
);
|
||||
|
||||
const isCreatePermitted = getHasCreateActionPermission(
|
||||
isFeatureEnabled,
|
||||
pagePermissions,
|
||||
);
|
||||
|
||||
const moreActionsMenu = useMemo(() => {
|
||||
const convertToModuleProps = {
|
||||
canCreateModuleInstance: isCreatePermitted,
|
||||
canDeleteEntity: isDeletePermitted,
|
||||
entityId: action?.id || "",
|
||||
moduleType: MODULE_TYPE.QUERY,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MoreActionsMenu
|
||||
basePageId={basePageId}
|
||||
className="t--more-action-menu"
|
||||
id={action?.id || ""}
|
||||
isChangePermitted={isChangePermitted}
|
||||
isDeletePermitted={isDeletePermitted}
|
||||
name={action?.name || ""}
|
||||
prefixAdditionalMenus={
|
||||
editorMode === EditorViewMode.SplitScreen && (
|
||||
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
{action?.pluginType !== PluginType.INTERNAL &&
|
||||
editorMode !== EditorViewMode.SplitScreen && (
|
||||
// Need to remove this check once workflow query is supported in module
|
||||
<ConvertToModuleInstanceCTA {...convertToModuleProps} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
action?.id,
|
||||
action?.name,
|
||||
action?.pluginType,
|
||||
isChangePermitted,
|
||||
isDeletePermitted,
|
||||
basePageId,
|
||||
isCreatePermitted,
|
||||
editorMode,
|
||||
]);
|
||||
|
||||
const actionRightPaneBackLink = useMemo(() => {
|
||||
return <BackToCanvas basePageId={basePageId} />;
|
||||
}, [basePageId]);
|
||||
|
||||
const changeQueryPage = useCallback(
|
||||
(baseQueryId: string) => {
|
||||
dispatch(
|
||||
changeQuery({ baseQueryId: baseQueryId, basePageId, applicationId }),
|
||||
);
|
||||
},
|
||||
[basePageId, applicationId, dispatch],
|
||||
);
|
||||
|
||||
const onCreateDatasourceClick = useCallback(() => {
|
||||
history.push(
|
||||
integrationEditorURL({
|
||||
basePageId: basePageId,
|
||||
selectedTab: INTEGRATION_TABS.NEW,
|
||||
}),
|
||||
);
|
||||
// Event for datasource creation click
|
||||
const entryPoint = DatasourceCreateEntryPoints.QUERY_EDITOR;
|
||||
|
||||
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
|
||||
entryPoint,
|
||||
});
|
||||
}, [basePageId]);
|
||||
|
||||
// custom function to return user to integrations page if action is not found
|
||||
const onEntityNotFoundBackClick = useCallback(
|
||||
() =>
|
||||
history.push(
|
||||
integrationEditorURL({
|
||||
basePageId: basePageId,
|
||||
selectedTab: INTEGRATION_TABS.ACTIVE,
|
||||
}),
|
||||
),
|
||||
[basePageId],
|
||||
);
|
||||
|
||||
const notification = useMemo(() => {
|
||||
if (!isConverting) return null;
|
||||
|
||||
return (
|
||||
<ConvertEntityNotification
|
||||
icon={icon}
|
||||
name={action?.name || ""}
|
||||
withPadding
|
||||
/>
|
||||
);
|
||||
}, [action?.name, isConverting, icon]);
|
||||
|
||||
return (
|
||||
<QueryEditorContextProvider
|
||||
actionRightPaneBackLink={actionRightPaneBackLink}
|
||||
changeQueryPage={changeQueryPage}
|
||||
moreActionsMenu={moreActionsMenu}
|
||||
notification={notification}
|
||||
onCreateDatasourceClick={onCreateDatasourceClick}
|
||||
onEntityNotFoundBackClick={onEntityNotFoundBackClick}
|
||||
saveActionName={saveActionName}
|
||||
>
|
||||
<Disabler isDisabled={isConverting}>
|
||||
<Editor
|
||||
{...props}
|
||||
isEditorInitialized={isEditorInitialized}
|
||||
settingsConfig={settingsConfig}
|
||||
/>
|
||||
</Disabler>
|
||||
</QueryEditorContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryEditor;
|
||||
|
|
@ -45,6 +45,7 @@ import {
|
|||
import type {
|
||||
Action,
|
||||
ApiAction,
|
||||
AutoGeneratedHeader,
|
||||
CreateApiActionDefaultsParams,
|
||||
} from "entities/Action";
|
||||
import { type Plugin, PluginPackageName, PluginType } from "entities/Plugin";
|
||||
|
|
@ -60,8 +61,7 @@ import { getCurrentBasePageId } from "selectors/editorSelectors";
|
|||
import { validateResponse } from "./ErrorSagas";
|
||||
import type { CreateDatasourceSuccessAction } from "actions/datasourceActions";
|
||||
import { removeTempDatasource } from "actions/datasourceActions";
|
||||
import type { AutoGeneratedHeader } from "pages/Editor/APIEditor/helpers";
|
||||
import { deriveAutoGeneratedHeaderState } from "pages/Editor/APIEditor/helpers";
|
||||
import { deriveAutoGeneratedHeaderState } from "../PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/utils/autoGeneratedHeaders";
|
||||
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
|
||||
import type { FeatureFlags } from "ee/entities/FeatureFlag";
|
||||
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { toast } from "@appsmith/ads";
|
|||
|
||||
export const LOCAL_STORAGE_KEYS = {
|
||||
CANVAS_CARDS_STATE: "CANVAS_CARDS_STATE",
|
||||
SPLITPANE_ANNOUNCEMENT: "SPLITPANE_ANNOUNCEMENT",
|
||||
NUDGE_SHOWN_SPLIT_PANE: "NUDGE_SHOWN_SPLIT_PANE",
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user