fix: API Body format focus retention (#37150)

## Description


- Use Focus retention to store user context of the POST body format of
APIs
- Update component to use hooks instead of redux connect hoc
- remove need of passing Action ID in form data as it is redundant for
focus retention states


Fixes #36984

## Automation

/ok-to-test tags="@tag.Datasource, @tag.IDE"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/11609483418>
> Commit: a3a1a59101773372a65ded41ea1cce3cd83ffd5f
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11609483418&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Datasource, @tag.IDE`
> Spec:
> <hr>Thu, 31 Oct 2024 10:31:09 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Introduced a streamlined method for managing API body content types
and form data.
- Added new action creator `setExtraFormData` for enhanced form data
handling.

- **Improvements**
- Simplified the `PostBodyData` component by utilizing React hooks for
state management.
- Updated selectors and reducers to support a flatter structure for form
data, improving clarity and maintainability.

- **Updates**
- Enhanced focus elements configuration to better manage plugin action
form data within the application.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Hetu Nandu 2024-11-01 11:24:49 +05:30 committed by GitHub
parent 951be4a34e
commit 9a9a7c4b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 76 additions and 89 deletions

View File

@ -1,15 +1,12 @@
import React from "react"; import React, { useCallback } from "react";
import { connect } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components"; import styled from "styled-components";
import { formValueSelector } from "redux-form";
import { import {
POST_BODY_FORMAT_OPTIONS, POST_BODY_FORMAT_OPTIONS,
POST_BODY_FORMAT_TITLES, POST_BODY_FORMAT_TITLES,
} from "../../../../constants/CommonApiConstants"; } from "../../../../constants/CommonApiConstants";
import { API_EDITOR_FORM_NAME } from "ee/constants/forms";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField"; import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import type { AppState } from "ee/reducers";
import FIELD_VALUES from "constants/FieldExpectedValue"; import FIELD_VALUES from "constants/FieldExpectedValue";
import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { import {
@ -61,11 +58,8 @@ const NoBodyMessage = styled.div`
`; `;
interface PostDataProps { interface PostDataProps {
displayFormat: { label: string; value: string };
dataTreePath: string; dataTreePath: string;
theme?: EditorTheme; theme?: EditorTheme;
apiId: string;
updateBodyContentType: (contentType: string, apiId: string) => void;
} }
type Props = PostDataProps; type Props = PostDataProps;
@ -77,9 +71,13 @@ const expectedPostBody: CodeEditorExpected = {
}; };
function PostBodyData(props: Props) { function PostBodyData(props: Props) {
const [selectedTab, setSelectedTab] = React.useState( const postBodyFormat = useSelector(getPostBodyFormat);
props.displayFormat?.value, const dispatch = useDispatch();
);
const updateBodyContentType = useCallback((tab: string) => {
dispatch(updatePostBodyContentType(tab));
}, []);
const { dataTreePath, theme } = props; const { dataTreePath, theme } = props;
const tabComponentsMap = (key: string) => { const tabComponentsMap = (key: string) => {
@ -172,18 +170,13 @@ function PostBodyData(props: Props) {
value: el.key, value: el.key,
})); }));
const postBodyDataOnChangeFn = (key: string) => {
setSelectedTab(key);
props?.updateBodyContentType(key, props.apiId);
};
return ( return (
<PostBodyContainer> <PostBodyContainer>
<Select <Select
data-testid="t--api-body-tab-switch" data-testid="t--api-body-tab-switch"
defaultValue={selectedTab} defaultValue={postBodyFormat.value}
onSelect={(value) => postBodyDataOnChangeFn(value)} onSelect={updateBodyContentType}
value={selectedTab} value={postBodyFormat.value}
> >
{options.map((option) => ( {options.map((option) => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
@ -191,31 +184,9 @@ function PostBodyData(props: Props) {
</Option> </Option>
))} ))}
</Select> </Select>
{tabComponentsMap(selectedTab)} {tabComponentsMap(postBodyFormat.value)}
</PostBodyContainer> </PostBodyContainer>
); );
} }
const selector = formValueSelector(API_EDITOR_FORM_NAME); export default PostBodyData;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapDispatchToProps = (dispatch: any) => ({
updateBodyContentType: (contentType: string, apiId: string) =>
dispatch(updatePostBodyContentType(contentType, apiId)),
});
export default connect((state: AppState) => {
const apiId = selector(state, "id");
const postBodyFormat = getPostBodyFormat(state, apiId);
// Defaults to NONE when format is not set
const displayFormat = postBodyFormat || {
label: POST_BODY_FORMAT_OPTIONS.NONE,
value: POST_BODY_FORMAT_OPTIONS.NONE,
};
return {
displayFormat,
apiId,
};
}, mapDispatchToProps)(PostBodyData);

View File

@ -28,10 +28,16 @@ export const openPluginActionSettings = (payload: boolean) => ({
export const updatePostBodyContentType = ( export const updatePostBodyContentType = (
title: string, title: string,
apiId: string, ): ReduxAction<{ title: string }> => ({
): ReduxAction<{ title: string; apiId: string }> => ({
type: ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE, type: ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE,
payload: { title, apiId }, payload: { title },
});
export const setExtraFormData = (
values: Record<string, { label: string; value: string }>,
) => ({
type: ReduxActionTypes.SET_EXTRA_FORMDATA,
payload: { values },
}); });
export const changeApi = ( export const changeApi = (

View File

@ -2,6 +2,7 @@ import type { AppState } from "ee/reducers";
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { POST_BODY_FORM_DATA_KEY } from "./constants"; import { POST_BODY_FORM_DATA_KEY } from "./constants";
import { POST_BODY_FORMAT_OPTIONS } from "../constants/CommonApiConstants";
export const getActionEditorSavingMap = (state: AppState) => export const getActionEditorSavingMap = (state: AppState) =>
state.ui.pluginActionEditor.isSaving; state.ui.pluginActionEditor.isSaving;
@ -37,19 +38,25 @@ export const isActionDeleting = (id: string) =>
(deletingMap) => id in deletingMap && deletingMap[id], (deletingMap) => id in deletingMap && deletingMap[id],
); );
type GetFormData = ( export const getFormData = (state: AppState) =>
state: AppState, state.ui.pluginActionEditor.formData;
id: string,
) => { label: string; value: string } | undefined;
export const getPostBodyFormat: GetFormData = (state, id) => { type GetFormPostBodyFormat = (state: AppState) => {
const formData = state.ui.pluginActionEditor.formData; label: string;
value: string;
};
if (id in formData) { export const getPostBodyFormat: GetFormPostBodyFormat = (state) => {
return formData[id][POST_BODY_FORM_DATA_KEY]; const formData = getFormData(state);
if (POST_BODY_FORM_DATA_KEY in formData) {
return formData[POST_BODY_FORM_DATA_KEY];
} }
return undefined; return {
label: POST_BODY_FORMAT_OPTIONS.NONE,
value: POST_BODY_FORMAT_OPTIONS.NONE,
};
}; };
export const getPluginActionConfigSelectedTab = (state: AppState) => export const getPluginActionConfigSelectedTab = (state: AppState) =>
state.ui.pluginActionEditor.selectedConfigTab; state.ui.pluginActionEditor.selectedConfigTab;

View File

@ -26,7 +26,7 @@ export interface PluginActionEditorState {
isDirty: Record<string, boolean>; isDirty: Record<string, boolean>;
runErrorMessage: Record<string, string>; runErrorMessage: Record<string, string>;
selectedConfigTab?: string; selectedConfigTab?: string;
formData: Record<string, Record<string, { label: string; value: string }>>; formData: Record<string, { label: string; value: string }>;
debugger: PluginEditorDebuggerState; debugger: PluginEditorDebuggerState;
settingsOpen?: boolean; settingsOpen?: boolean;
} }
@ -144,13 +144,12 @@ export const handlers = {
[ReduxActionTypes.SET_EXTRA_FORMDATA]: ( [ReduxActionTypes.SET_EXTRA_FORMDATA]: (
state: PluginActionEditorState, state: PluginActionEditorState,
action: ReduxAction<{ action: ReduxAction<{
id: string;
values: Record<string, { label: string; value: string }>; values: Record<string, { label: string; value: string }>;
}>, }>,
) => { ) => {
const { id, values } = action.payload; const { values } = action.payload;
set(state, ["formData", id], values); set(state, ["formData"], values);
}, },
[ReduxActionTypes.SET_PLUGIN_ACTION_EDITOR_FORM_SELECTED_TAB]: ( [ReduxActionTypes.SET_PLUGIN_ACTION_EDITOR_FORM_SELECTED_TAB]: (
state: PluginActionEditorState, state: PluginActionEditorState,

View File

@ -77,11 +77,16 @@ import { ActionExecutionResizerHeight } from "PluginActionEditor/components/Plug
import { import {
getPluginActionConfigSelectedTab, getPluginActionConfigSelectedTab,
getPluginActionDebuggerState, getPluginActionDebuggerState,
getFormData,
setExtraFormData,
setPluginActionEditorDebuggerState, setPluginActionEditorDebuggerState,
setPluginActionEditorSelectedTab, setPluginActionEditorSelectedTab,
} from "PluginActionEditor/store"; } from "PluginActionEditor/store";
import { EDITOR_TABS } from "constants/QueryEditorConstants"; import { EDITOR_TABS } from "constants/QueryEditorConstants";
import { API_EDITOR_TABS } from "PluginActionEditor/constants/CommonApiConstants"; import {
API_EDITOR_TABS,
POST_BODY_FORMAT_OPTIONS,
} from "PluginActionEditor/constants/CommonApiConstants";
export const AppIDEFocusElements: FocusElementsConfigList = { export const AppIDEFocusElements: FocusElementsConfigList = {
[FocusEntity.DATASOURCE_LIST]: [ [FocusEntity.DATASOURCE_LIST]: [
@ -152,9 +157,13 @@ export const AppIDEFocusElements: FocusElementsConfigList = {
}, },
{ {
type: FocusElementConfigType.Redux, type: FocusElementConfigType.Redux,
name: FocusElement.InputField, name: FocusElement.PluginActionFormData,
selector: getFocusableInputField, selector: getFormData,
setter: setFocusableInputField, setter: setExtraFormData,
defaultValue: {
label: POST_BODY_FORMAT_OPTIONS.NONE,
value: POST_BODY_FORMAT_OPTIONS.NONE,
},
}, },
{ {
type: FocusElementConfigType.Redux, type: FocusElementConfigType.Redux,

View File

@ -3,6 +3,7 @@ import type { AppState } from "ee/reducers";
export enum FocusElement { export enum FocusElement {
PluginActionConfigTabs = "PluginActionConfigTabs", PluginActionConfigTabs = "PluginActionConfigTabs",
PluginActionFormData = "PluginActionFormData",
CodeEditorHistory = "CodeEditorHistory", CodeEditorHistory = "CodeEditorHistory",
EntityCollapsibleState = "EntityCollapsibleState", EntityCollapsibleState = "EntityCollapsibleState",
EntityExplorerWidth = "EntityExplorerWidth", EntityExplorerWidth = "EntityExplorerWidth",

View File

@ -61,7 +61,10 @@ import {
import { updateReplayEntity } from "actions/pageActions"; import { updateReplayEntity } from "actions/pageActions";
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
import type { Plugin } from "api/PluginApi"; import type { Plugin } from "api/PluginApi";
import { getPostBodyFormat } from "../PluginActionEditor/store"; import {
getPostBodyFormat,
setExtraFormData,
} from "../PluginActionEditor/store";
import { apiEditorIdURL, datasourcesEditorIdURL } from "ee/RouteBuilder"; import { apiEditorIdURL, datasourcesEditorIdURL } from "ee/RouteBuilder";
import { getCurrentBasePageId } from "selectors/editorSelectors"; import { getCurrentBasePageId } from "selectors/editorSelectors";
import { validateResponse } from "./ErrorSagas"; import { validateResponse } from "./ErrorSagas";
@ -135,10 +138,8 @@ function* syncApiParamsSaga(
} }
} }
function* handleUpdateBodyContentType( function* handleUpdateBodyContentType(action: ReduxAction<{ title: string }>) {
action: ReduxAction<{ title: string; apiId: string }>, const { title } = action.payload;
) {
const { apiId, title } = action.payload;
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
const displayFormatValue = POST_BODY_FORMAT_OPTIONS_ARRAY.find( const displayFormatValue = POST_BODY_FORMAT_OPTIONS_ARRAY.find(
@ -216,18 +217,14 @@ function* handleUpdateBodyContentType(
// Quick Context: The extra formadata action is responsible for updating the current multi switch mode you see on api editor body tab // Quick Context: The extra formadata action is responsible for updating the current multi switch mode you see on api editor body tab
// whenever a user selects a new content type through the tab e.g application/json, this action is dispatched to update that value, which is then read in the PostDataBody file // whenever a user selects a new content type through the tab e.g application/json, this action is dispatched to update that value, which is then read in the PostDataBody file
// to show the appropriate content type section. // to show the appropriate content type section.
yield put({ yield put(
type: ReduxActionTypes.SET_EXTRA_FORMDATA, setExtraFormData({
payload: { [POST_BODY_FORM_DATA_KEY]: {
id: apiId, label: title,
values: { value: title,
displayFormat: {
label: title,
value: title,
},
}, },
}, }),
}); );
// help to prevent cyclic dependency error in case the bodyFormData is empty. // help to prevent cyclic dependency error in case the bodyFormData is empty.
@ -257,7 +254,8 @@ function* updateExtraFormDataSaga() {
const { values } = formData; const { values } = formData;
// when initializing, check if theres a display format present. // when initializing, check if theres a display format present.
const extraFormData: GetFormData = yield select(getPostBodyFormat, values.id); const extraFormData: { label: string; value: string } =
yield select(getPostBodyFormat);
const headers: Array<{ key: string; value: string }> = const headers: Array<{ key: string; value: string }> =
get(values, "actionConfiguration.headers") || []; get(values, "actionConfiguration.headers") || [];
@ -363,15 +361,11 @@ function* setApiBodyTabHeaderFormat(apiId: string, apiContentType?: string) {
}; };
} }
yield put({ yield put(
type: ReduxActionTypes.SET_EXTRA_FORMDATA, setExtraFormData({
payload: { [POST_BODY_FORM_DATA_KEY]: displayFormat,
id: apiId, }),
values: { );
[POST_BODY_FORM_DATA_KEY]: displayFormat,
},
},
});
} }
function* formValueChangeSaga( function* formValueChangeSaga(