From 8463d02740f9e300c8d306c7012edec39067d2ff Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Tue, 8 Oct 2024 16:12:27 +0530 Subject: [PATCH 01/15] chore: [Plugin Action Editor] Combine Plugin Editor UI state (#36651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Removes the parallel apiPaneReducer and queryPaneReducer and combines it into pluginActionEditor Fixes #36153 ## Automation /ok-to-test tags="@tag.All" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 3a204e2902f9733be05e3b7087869d2e91ad52eb > Cypress dashboard. > Tags: `@tag.All` > Spec: >
Tue, 08 Oct 2024 10:15:30 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced a new constant `POST_BODY_FORM_DATA_KEY` for improved data handling. - Added several action creators and selectors for enhanced management of the plugin action editor's state. - Implemented state management for the plugin action editor's debugger. - Added the ability to execute commands related to plugins in the `CodeEditor` component. - Introduced a new enumeration `DEBUGGER_TAB_KEYS` for better organization of debugger tabs. - **Improvements** - Refactored import statements for better organization and reduced complexity. - Enhanced the `PluginActionResponse` component to focus on plugin action state management. - Updated the tab management logic in various components to reflect the new plugin action state. - Improved state management in the `QueryEditor` and `APIEditor` components to accurately represent action processing states. - Added error handling in the `ActionSettings` component for missing configuration. - **Bug Fixes** - Updated logic in hooks to ensure proper state management for plugin actions. - Adjusted navigation and tab selection methods to align with the new plugin action state management. - Corrected test selectors for error tabs in Cypress tests to ensure accurate UI interactions. --- .../OtherUIFeatures/Widget_Error_spec.js | 4 +- .../cypress/locators/commonlocators.json | 2 +- app/client/cypress/support/ApiCommands.js | 2 +- .../cypress/support/Objects/CommonLocators.ts | 4 +- app/client/cypress/support/Pages/ApiPage.ts | 2 +- .../cypress/support/Pages/DataSources.ts | 4 +- .../hooks/useSelectedFormTab.ts | 27 +- .../hooks/useChangeActionCall.test.tsx | 8 +- .../hooks/useChangeActionCall.ts | 3 +- .../PluginActionResponse.tsx | 18 +- .../src/PluginActionEditor/constants.ts | 1 + app/client/src/PluginActionEditor/index.ts | 2 + .../src/PluginActionEditor/store/index.ts | 4 + .../store/pluginActionEditorActions.ts | 57 ++++ .../store/pluginActionEditorSelectors.ts | 61 +++++ .../store/pluginEditorReducer.ts | 177 ++++++++++++ app/client/src/actions/apiPaneActions.ts | 67 ----- app/client/src/actions/pluginActionActions.ts | 45 ++- app/client/src/actions/queryPaneActions.ts | 38 --- .../hooks/usePluginActionResponseTabs.tsx | 10 +- app/client/src/ce/actions/helpers.ts | 10 +- .../src/ce/constants/ReduxActionConstants.tsx | 4 + .../src/ce/navigation/FocusElements/AppIDE.ts | 63 ++--- .../Applications/CreateNewAppsOption.test.tsx | 2 +- app/client/src/ce/reducers/index.tsx | 6 +- .../ce/reducers/uiReducers/apiPaneReducer.ts | 256 ------------------ .../src/ce/reducers/uiReducers/index.tsx | 8 +- .../reducers/uiReducers/queryPaneReducer.ts | 231 ---------------- .../src/ce/selectors/appIDESelectors.ts | 16 ++ .../src/ce/selectors/entitiesSelector.ts | 22 -- .../editorComponents/ApiResponseView.test.tsx | 2 +- .../editorComponents/ApiResponseView.tsx | 20 +- .../CodeEditor/EvaluatedValuePopup.tsx | 6 +- .../editorComponents/CodeEditor/index.tsx | 2 +- .../editorComponents/Debugger/DebugCTA.tsx | 2 +- .../Debugger/DebuggerTabs.tsx | 2 +- .../editorComponents/Debugger/Schema.tsx | 6 +- .../editorComponents/Debugger/constants.ts | 8 + .../editorComponents/Debugger/helpers.tsx | 13 +- .../Debugger/hooks/useDebuggerTriggerClick.ts | 29 +- .../editorComponents/EntityBottomTabs.tsx | 5 +- .../editorComponents/JSResponseView.test.tsx | 3 +- .../editorComponents/JSResponseView.tsx | 5 +- .../ee/reducers/uiReducers/apiPaneReducer.tsx | 3 - .../reducers/uiReducers/queryPaneReducer.ts | 3 - app/client/src/navigation/FocusElements.ts | 7 +- .../src/pages/Editor/APIEditor/Editor.tsx | 17 +- .../pages/Editor/APIEditor/PostBodyData.tsx | 13 +- .../src/pages/Editor/ActionSettings.tsx | 17 +- .../Editor/DataSourceEditor/Debugger.tsx | 10 +- .../DataSourceEditor/NewActionButton.tsx | 2 +- .../ActionPane/ApiPaneNavigation.ts | 27 +- .../ActionPane/QueryPaneNavigation.ts | 8 +- .../pages/Editor/EntityNavigation/types.ts | 2 +- .../CreateNewDatasourceTab.tsx | 3 +- app/client/src/pages/Editor/JSEditor/Form.tsx | 4 +- .../src/pages/Editor/QueryEditor/Editor.tsx | 17 +- .../Editor/QueryEditor/EditorJSONtoForm.tsx | 12 +- .../QueryEditor/QueryDebuggerTabs.test.tsx | 2 +- .../Editor/QueryEditor/QueryDebuggerTabs.tsx | 62 +++-- .../Editor/QueryEditor/QueryResponseTab.tsx | 19 +- .../src/pages/Editor/QueryEditor/index.tsx | 2 +- .../sagas/ActionExecution/PluginActionSaga.ts | 50 ++-- .../src/sagas/ActionExecution/errorUtils.ts | 2 +- app/client/src/sagas/ActionSagas.ts | 6 +- app/client/src/sagas/ApiPaneSagas.ts | 9 +- app/client/src/sagas/JSPaneSagas.ts | 2 +- app/client/src/sagas/QueryPaneSagas.ts | 2 +- app/client/src/selectors/apiPaneSelectors.ts | 29 -- app/client/src/selectors/editorSelectors.tsx | 15 +- .../src/selectors/queryPaneSelectors.ts | 38 --- app/client/src/utils/replayHelpers.tsx | 9 +- 72 files changed, 665 insertions(+), 984 deletions(-) create mode 100644 app/client/src/PluginActionEditor/constants.ts create mode 100644 app/client/src/PluginActionEditor/store/index.ts create mode 100644 app/client/src/PluginActionEditor/store/pluginActionEditorActions.ts create mode 100644 app/client/src/PluginActionEditor/store/pluginActionEditorSelectors.ts create mode 100644 app/client/src/PluginActionEditor/store/pluginEditorReducer.ts delete mode 100644 app/client/src/actions/apiPaneActions.ts delete mode 100644 app/client/src/actions/queryPaneActions.ts delete mode 100644 app/client/src/ce/reducers/uiReducers/apiPaneReducer.ts delete mode 100644 app/client/src/ce/reducers/uiReducers/queryPaneReducer.ts create mode 100644 app/client/src/components/editorComponents/Debugger/constants.ts delete mode 100644 app/client/src/ee/reducers/uiReducers/apiPaneReducer.tsx delete mode 100644 app/client/src/ee/reducers/uiReducers/queryPaneReducer.ts delete mode 100644 app/client/src/selectors/apiPaneSelectors.ts delete mode 100644 app/client/src/selectors/queryPaneSelectors.ts diff --git a/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/Widget_Error_spec.js b/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/Widget_Error_spec.js index 2f35d8795cb..1a67eb90f1c 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/Widget_Error_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/Widget_Error_spec.js @@ -24,7 +24,7 @@ describe("Widget error state", { tags: ["@tag.Widget"] }, function () { //Check if the current value is shown in the debugger _.debuggerHelper.OpenDebugger(); - cy.get("[data-testid=t--tab-ERROR]").click(); + cy.get("[data-testid=t--tab-ERROR_TAB]").click(); //This feature is disabled in updated error log - epic 17720 // _.debuggerHelper.LogStateContains("Test"); }); @@ -37,7 +37,7 @@ describe("Widget error state", { tags: ["@tag.Widget"] }, function () { cy.get(widgetLocators.buttonWidget).click(); cy.get(".t--toast-debug-button").click(); - cy.get("[data-testid='t--tab-ERROR']").should( + cy.get("[data-testid='t--tab-ERROR_TAB']").should( "have.attr", "aria-selected", "true", diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 9fe519e15ad..fdac92e3af4 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -164,7 +164,7 @@ "labelSectionTxt": ".CodeMirror-code .cm-variable", "lintError": ".CodeMirror-lint-mark-error", "debugger": ".t--debugger-count", - "errorTab": "[data-testid=t--tab-ERROR]", + "errorTab": "[data-testid=t--tab-ERROR_TAB]", "debugErrorMsg": "[data-testid=t--debugger-log-message]", "tableButtonVariant": ".t--property-control-buttonvariant", "debuggerLabel": "span.debugger-label", diff --git a/app/client/cypress/support/ApiCommands.js b/app/client/cypress/support/ApiCommands.js index 9c24c425d32..d0d52ec712b 100644 --- a/app/client/cypress/support/ApiCommands.js +++ b/app/client/cypress/support/ApiCommands.js @@ -94,7 +94,7 @@ Cypress.Commands.add( } cy.get(".string-value").contains(baseurl.concat(path)); cy.get(".string-value").contains(verb); - cy.get("[data-testid=t--tab-response]").first().click({ force: true }); + cy.get("[data-testid=t--tab-RESPONSE_TAB]").first().click({ force: true }); }, ); diff --git a/app/client/cypress/support/Objects/CommonLocators.ts b/app/client/cypress/support/Objects/CommonLocators.ts index 9bcf7c1cfec..a8f5d904148 100644 --- a/app/client/cypress/support/Objects/CommonLocators.ts +++ b/app/client/cypress/support/Objects/CommonLocators.ts @@ -89,8 +89,8 @@ export class CommonLocators { _createNew = ".t--add-item"; _uploadFiles = "div.uppy-Dashboard-AddFiles input"; _uploadBtn = "button.uppy-StatusBar-actionBtn--upload"; - _errorTab = "[data-testid=t--tab-ERROR]"; - _responseTab = "[data-testid=t--tab-response]"; + _errorTab = "[data-testid=t--tab-ERROR_TAB]"; + _responseTab = "[data-testid=t--tab-RESPONSE_TAB]"; _modal = ".t--modal-widget"; _closeModal = "button:contains('Close')"; _entityProperties = (entityNameinLeftSidebar: string) => diff --git a/app/client/cypress/support/Pages/ApiPage.ts b/app/client/cypress/support/Pages/ApiPage.ts index bb394a58287..57b7d33abbb 100644 --- a/app/client/cypress/support/Pages/ApiPage.ts +++ b/app/client/cypress/support/Pages/ApiPage.ts @@ -82,7 +82,7 @@ export class ApiPage { private _paginationTypeLabels = ".t--apiFormPaginationType label"; _saveAsDS = ".t--store-as-datasource"; _responseStatus = ".t--response-status-code"; - public _responseTabHeader = "[data-testid=t--tab-headers]"; + public _responseTabHeader = "[data-testid=t--tab-HEADERS_TAB]"; public _headersTabContent = ".t--headers-tab"; public _autoGeneratedHeaderInfoIcon = (key: string) => `.t--auto-generated-${key}-info`; diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index 35777482759..b41cbd05b34 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -201,7 +201,7 @@ export class DataSources { ".t--datasource-name:contains('" + dsName + "')"; _mandatoryMark = "//span[text()='*']"; _deleteDSHostPort = ".t--delete-field"; - _dsTabSchema = "[data-testid='t--tab-schema']"; + _dsTabSchema = "[data-testid='t--tab-SCHEMA_TAB']"; private _pageSelectionMenu = "[data-testId='t--page-selection']"; private _pageSelectMenuItem = ".ads-v2-menu__menu-item"; @@ -1854,7 +1854,7 @@ export class DataSources { cy.intercept("GET", "/api/v1/datasources/*/structure?ignoreCache=*").as( `getDatasourceStructureUpdated_${ds_entity_name}`, ); - cy.get("[data-testid=t--tab-schema]").first().click({ force: true }); + cy.get("[data-testid=t--tab-SCHEMA_TAB]").first().click({ force: true }); this.RefreshDatasourceSchema(); this.assertHelper .WaitForNetworkCall(`@getDatasourceStructureUpdated_${ds_entity_name}`) diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useSelectedFormTab.ts b/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useSelectedFormTab.ts index b7421a6dfc5..6e3aada35dd 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useSelectedFormTab.ts +++ b/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/hooks/useSelectedFormTab.ts @@ -1,25 +1,22 @@ import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { getApiPaneConfigSelectedTabIndex } from "selectors/apiPaneSelectors"; -import { API_EDITOR_TABS } from "constants/ApiEditorConstants/CommonApiConstants"; -import { setApiPaneConfigSelectedTabIndex } from "actions/apiPaneActions"; +import { + getPluginActionConfigSelectedTab, + setPluginActionEditorSelectedTab, +} from "PluginActionEditor/store"; -export function useSelectedFormTab(): [string, (id: string) => void] { +export function useSelectedFormTab(): [ + string | undefined, + (id: string) => void, +] { const dispatch = useDispatch(); - // the redux form has been configured with indexes, but the new ads components need strings to work. - // these functions convert them back and forth as needed. - const selectedIndex = useSelector(getApiPaneConfigSelectedTabIndex); - const selectedValue = Object.values(API_EDITOR_TABS)[selectedIndex]; - const setSelectedIndex = useCallback( + const selectedValue = useSelector(getPluginActionConfigSelectedTab); + const setSelectedTab = useCallback( (value: string) => { - const index = Object.values(API_EDITOR_TABS).indexOf( - value as API_EDITOR_TABS, - ); - - dispatch(setApiPaneConfigSelectedTabIndex(index)); + dispatch(setPluginActionEditorSelectedTab(value)); }, [dispatch], ); - return [selectedValue, setSelectedIndex]; + return [selectedValue, setSelectedTab]; } diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.test.tsx b/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.test.tsx index 98c144af91a..f68dfa38cd5 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.test.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.test.tsx @@ -2,8 +2,7 @@ import { renderHook } from "@testing-library/react-hooks/dom"; import { useDispatch } from "react-redux"; import { PluginType } from "entities/Action"; import { usePluginActionContext } from "PluginActionEditor"; -import { changeApi } from "actions/apiPaneActions"; -import { changeQuery } from "actions/queryPaneActions"; +import { changeApi, changeQuery } from "../../../store"; import usePrevious from "utils/hooks/usePrevious"; import { useChangeActionCall } from "./useChangeActionCall"; @@ -11,11 +10,8 @@ jest.mock("react-redux", () => ({ useDispatch: jest.fn(), })); -jest.mock("actions/apiPaneActions", () => ({ +jest.mock("../../../store", () => ({ changeApi: jest.fn(), -})); - -jest.mock("actions/queryPaneActions", () => ({ changeQuery: jest.fn(), })); diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.ts b/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.ts index 88a56e14d90..be76f258b79 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.ts +++ b/app/client/src/PluginActionEditor/components/PluginActionForm/hooks/useChangeActionCall.ts @@ -1,9 +1,8 @@ import { useEffect } from "react"; import { useDispatch } from "react-redux"; -import { changeApi } from "actions/apiPaneActions"; -import { changeQuery } from "actions/queryPaneActions"; import { PluginType } from "entities/Action"; import { usePluginActionContext } from "PluginActionEditor"; +import { changeApi, changeQuery } from "PluginActionEditor/store"; import usePrevious from "utils/hooks/usePrevious"; export const useChangeActionCall = () => { diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx index 9359c8130f3..d2c5b0a2525 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/PluginActionResponse.tsx @@ -3,9 +3,9 @@ import { IDEBottomView, ViewHideBehaviour } from "IDE"; import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; import EntityBottomTabs from "components/editorComponents/EntityBottomTabs"; import { useDispatch, useSelector } from "react-redux"; -import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors"; -import { setApiPaneDebuggerState } from "actions/apiPaneActions"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { setPluginActionEditorDebuggerState } from "../../store"; +import { getPluginActionDebuggerState } from "../../store"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { usePluginActionResponseTabs } from "./hooks"; @@ -16,11 +16,11 @@ function PluginActionResponse() { // TODO combine API and Query Debugger state const { open, responseTabHeight, selectedTab } = useSelector( - getApiPaneDebuggerState, + getPluginActionDebuggerState, ); const toggleHide = useCallback( - () => dispatch(setApiPaneDebuggerState({ open: !open })), + () => dispatch(setPluginActionEditorDebuggerState({ open: !open })), [dispatch, open], ); @@ -32,14 +32,18 @@ function PluginActionResponse() { }); } - dispatch(setApiPaneDebuggerState({ open: true, selectedTab: tabKey })); + dispatch( + setPluginActionEditorDebuggerState({ open: true, selectedTab: tabKey }), + ); }, [dispatch], ); const updateResponsePaneHeight = useCallback( (height: number) => { - dispatch(setApiPaneDebuggerState({ responseTabHeight: height })); + dispatch( + setPluginActionEditorDebuggerState({ responseTabHeight: height }), + ); }, [dispatch], ); diff --git a/app/client/src/PluginActionEditor/constants.ts b/app/client/src/PluginActionEditor/constants.ts new file mode 100644 index 00000000000..cba99efea29 --- /dev/null +++ b/app/client/src/PluginActionEditor/constants.ts @@ -0,0 +1 @@ +export const POST_BODY_FORM_DATA_KEY = "displayFormat"; diff --git a/app/client/src/PluginActionEditor/index.ts b/app/client/src/PluginActionEditor/index.ts index e8083e1b0f0..f0f9b933f1a 100644 --- a/app/client/src/PluginActionEditor/index.ts +++ b/app/client/src/PluginActionEditor/index.ts @@ -11,3 +11,5 @@ export type { PluginActionNameEditorProps, } from "./components/PluginActionNameEditor"; export { default as PluginActionNameEditor } from "./components/PluginActionNameEditor"; + +export type { PluginActionEditorState } from "./store/pluginEditorReducer"; diff --git a/app/client/src/PluginActionEditor/store/index.ts b/app/client/src/PluginActionEditor/store/index.ts new file mode 100644 index 00000000000..e1680cfdbf9 --- /dev/null +++ b/app/client/src/PluginActionEditor/store/index.ts @@ -0,0 +1,4 @@ +export { default as pluginActionReducer } from "./pluginEditorReducer"; + +export * from "./pluginActionEditorActions"; +export * from "./pluginActionEditorSelectors"; diff --git a/app/client/src/PluginActionEditor/store/pluginActionEditorActions.ts b/app/client/src/PluginActionEditor/store/pluginActionEditorActions.ts new file mode 100644 index 00000000000..6b3a9d667f7 --- /dev/null +++ b/app/client/src/PluginActionEditor/store/pluginActionEditorActions.ts @@ -0,0 +1,57 @@ +import type { PluginEditorDebuggerState } from "./pluginEditorReducer"; +import { + type ReduxAction, + ReduxActionTypes, +} from "ee/constants/ReduxActionConstants"; +import type { Action } from "entities/Action"; + +export const setPluginActionEditorDebuggerState = ( + payload: Partial, +) => ({ + type: ReduxActionTypes.SET_PLUGIN_ACTION_EDITOR_DEBUGGER_STATE, + payload, +}); + +export const setPluginActionEditorSelectedTab = (payload: string) => ({ + type: ReduxActionTypes.SET_PLUGIN_ACTION_EDITOR_FORM_SELECTED_TAB, + payload: { + selectedTab: payload, + }, +}); + +export const updatePostBodyContentType = ( + title: string, + apiId: string, +): ReduxAction<{ title: string; apiId: string }> => ({ + type: ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE, + payload: { title, apiId }, +}); + +export const changeApi = ( + id: string, + isSaas: boolean, + newApi?: boolean, +): ReduxAction<{ id: string; isSaas: boolean; newApi?: boolean }> => { + return { + type: ReduxActionTypes.API_PANE_CHANGE_API, + payload: { id, isSaas, newApi }, + }; +}; + +export interface ChangeQueryPayload { + baseQueryId: string; + packageId?: string; + applicationId?: string; + basePageId?: string; + moduleId?: string; + workflowId?: string; + newQuery?: boolean; + action?: Action; +} + +export const changeQuery = (payload: ChangeQueryPayload) => { + return { + type: ReduxActionTypes.QUERY_PANE_CHANGE, + payload, + }; +}; diff --git a/app/client/src/PluginActionEditor/store/pluginActionEditorSelectors.ts b/app/client/src/PluginActionEditor/store/pluginActionEditorSelectors.ts new file mode 100644 index 00000000000..a4cd810f1c3 --- /dev/null +++ b/app/client/src/PluginActionEditor/store/pluginActionEditorSelectors.ts @@ -0,0 +1,61 @@ +import type { AppState } from "ee/reducers"; +import { createSelector } from "reselect"; + +import { POST_BODY_FORM_DATA_KEY } from "../constants"; + +export const getActionEditorSavingMap = (state: AppState) => + state.ui.pluginActionEditor.isSaving; + +export const isActionSaving = (id: string) => + createSelector([getActionEditorSavingMap], (savingMap) => { + return id in savingMap && savingMap[id]; + }); + +const getActionDirtyState = (state: AppState) => + state.ui.pluginActionEditor.isDirty; + +export const isActionDirty = (id: string) => + createSelector([getActionDirtyState], (actionDirtyMap) => { + return id in actionDirtyMap && actionDirtyMap[id]; + }); + +const getActionRunningState = (state: AppState) => + state.ui.pluginActionEditor.isRunning; + +export const isActionRunning = (id: string) => + createSelector( + [getActionRunningState], + (isRunningMap) => id in isRunningMap && isRunningMap[id], + ); + +const getActionDeletingState = (state: AppState) => + state.ui.pluginActionEditor.isDeleting; + +export const isActionDeleting = (id: string) => + createSelector( + [getActionDeletingState], + (deletingMap) => id in deletingMap && deletingMap[id], + ); + +type GetFormData = ( + state: AppState, + id: string, +) => { label: string; value: string } | undefined; + +export const getPostBodyFormat: GetFormData = (state, id) => { + const formData = state.ui.pluginActionEditor.formData; + + if (id in formData) { + return formData[id][POST_BODY_FORM_DATA_KEY]; + } + + return undefined; +}; +export const getPluginActionConfigSelectedTab = (state: AppState) => + state.ui.pluginActionEditor.selectedConfigTab; + +export const getPluginActionDebuggerState = (state: AppState) => + state.ui.pluginActionEditor.debugger; + +export const isPluginActionCreating = (state: AppState) => + state.ui.pluginActionEditor.isCreating; diff --git a/app/client/src/PluginActionEditor/store/pluginEditorReducer.ts b/app/client/src/PluginActionEditor/store/pluginEditorReducer.ts new file mode 100644 index 00000000000..781ce8d7165 --- /dev/null +++ b/app/client/src/PluginActionEditor/store/pluginEditorReducer.ts @@ -0,0 +1,177 @@ +import { createImmerReducer } from "utils/ReducerUtils"; +import type { ReduxAction } from "ee/constants/ReduxActionConstants"; +import { + ReduxActionTypes, + ReduxActionErrorTypes, +} from "ee/constants/ReduxActionConstants"; +import type { Action } from "entities/Action"; +import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; +import type { ActionResponse } from "api/ActionAPI"; +import { omit, set } from "lodash"; +import { objectKeys } from "@appsmith/utils"; +import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions"; + +export interface PluginEditorDebuggerState { + open: boolean; + responseTabHeight: number; + selectedTab?: string; +} + +export interface PluginActionEditorState { + isCreating: boolean; + isRunning: Record; + isSaving: Record; + isDeleting: Record; + isDirty: Record; + runErrorMessage: Record; + selectedConfigTab?: string; + formData: Record>; + debugger: PluginEditorDebuggerState; +} + +const initialState: PluginActionEditorState = { + isCreating: false, + isRunning: {}, + isSaving: {}, + isDeleting: {}, + isDirty: {}, + runErrorMessage: {}, + formData: {}, + debugger: { + open: false, + responseTabHeight: ActionExecutionResizerHeight, + }, +}; + +export const handlers = { + [ReduxActionTypes.CREATE_ACTION_INIT]: (state: PluginActionEditorState) => { + state.isCreating = true; + }, + [ReduxActionTypes.CREATE_ACTION_SUCCESS]: ( + state: PluginActionEditorState, + ) => { + state.isCreating = false; + }, + [ReduxActionErrorTypes.CREATE_ACTION_ERROR]: ( + state: PluginActionEditorState, + ) => { + state.isCreating = false; + }, + [ReduxActionTypes.UPDATE_ACTION_PROPERTY]: ( + state: PluginActionEditorState, + action: ReduxAction, + ) => { + set(state, ["isDirty", action.payload.id], true); + }, + [ReduxActionTypes.UPDATE_ACTION_INIT]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isSaving", action.payload.id], true); + }, + [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( + state: PluginActionEditorState, + action: ReduxAction<{ data: Action }>, + ) => { + set(state, ["isSaving", action.payload.data.id], false); + set(state, ["isDirty", action.payload.data.id], false); + }, + [ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isSaving", action.payload.id], false); + }, + [ReduxActionTypes.DELETE_ACTION_INIT]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isDeleting", action.payload.id], true); + }, + [ReduxActionTypes.DELETE_ACTION_SUCCESS]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isDeleting", action.payload.id], false); + }, + [ReduxActionErrorTypes.DELETE_ACTION_ERROR]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isDeleting", action.payload.id], false); + }, + [ReduxActionTypes.RUN_ACTION_REQUEST]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isRunning", action.payload.id], true); + set(state, ["debugger", "selectedTab"], DEBUGGER_TAB_KEYS.RESPONSE_TAB); + set(state, ["debugger", "open"], true); + }, + [ReduxActionTypes.RUN_ACTION_CANCELLED]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string }>, + ) => { + set(state, ["isRunning", action.payload.id], false); + }, + + [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( + state: PluginActionEditorState, + action: ReduxAction>, + ) => { + const actionId = objectKeys(action.payload)[0]; + + set(state, ["isRunning", actionId], false); + set(state, ["runErrorMessage"], omit(state.runErrorMessage, [actionId])); + }, + [ReduxActionErrorTypes.RUN_ACTION_ERROR]: ( + state: PluginActionEditorState, + action: ReduxAction<{ id: string; error: Error }>, + ) => { + const { error, id } = action.payload; + + set(state, ["isRunning", id], false); + set(state, ["runErrorMessage", id], error.message); + }, + /** + * This redux action sets the extra form data field for an action. This is used to select the + * appropriate body type tab selection in the Rest API plugin based on the content-type. + * This redux action can be extended to other functionalities as well in the future. + */ + [ReduxActionTypes.SET_EXTRA_FORMDATA]: ( + state: PluginActionEditorState, + action: ReduxAction<{ + id: string; + values: Record; + }>, + ) => { + const { id, values } = action.payload; + + set(state, ["formData", id], values); + }, + [ReduxActionTypes.SET_PLUGIN_ACTION_EDITOR_FORM_SELECTED_TAB]: ( + state: PluginActionEditorState, + action: ReduxAction<{ selectedTab: string }>, + ) => { + const { selectedTab } = action.payload; + + state.selectedConfigTab = selectedTab; + }, + [ReduxActionTypes.SET_PLUGIN_ACTION_EDITOR_DEBUGGER_STATE]: ( + state: PluginActionEditorState, + action: ReduxAction>, + ) => { + state.debugger = { + ...state.debugger, + ...action.payload, + }; + }, + [ReduxActionTypes.RESET_EDITOR_REQUEST]: (state: PluginActionEditorState) => { + state.isSaving = {}; + }, +}; + +const pluginActionEditorReducer = createImmerReducer(initialState, handlers); + +export default pluginActionEditorReducer; diff --git a/app/client/src/actions/apiPaneActions.ts b/app/client/src/actions/apiPaneActions.ts deleted file mode 100644 index 30c2ee87751..00000000000 --- a/app/client/src/actions/apiPaneActions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ReduxAction } from "ee/constants/ReduxActionConstants"; -import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; -import type { EventLocation } from "ee/utils/analyticsUtilTypes"; -import type { SlashCommandPayload } from "entities/Action"; -import type { ApiPaneDebuggerState } from "ee/reducers/uiReducers/apiPaneReducer"; - -export const changeApi = ( - id: string, - isSaas: boolean, - newApi?: boolean, -): ReduxAction<{ id: string; isSaas: boolean; newApi?: boolean }> => { - return { - type: ReduxActionTypes.API_PANE_CHANGE_API, - payload: { id, isSaas, newApi }, - }; -}; - -export const createNewApiAction = ( - pageId: string, - from: EventLocation, - apiType?: string, -): ReduxAction<{ pageId: string; from: EventLocation; apiType?: string }> => ({ - type: ReduxActionTypes.CREATE_NEW_API_ACTION, - payload: { pageId, from, apiType }, -}); - -export const createNewQueryAction = ( - pageId: string, - from: EventLocation, - datasourceId: string, - queryDefaultTableName?: string, -): ReduxAction<{ - pageId: string; - from: EventLocation; - datasourceId: string; - queryDefaultTableName?: string; -}> => ({ - type: ReduxActionTypes.CREATE_NEW_QUERY_ACTION, - payload: { pageId, from, datasourceId, queryDefaultTableName }, -}); - -export const updateBodyContentType = ( - title: string, - apiId: string, -): ReduxAction<{ title: string; apiId: string }> => ({ - type: ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE, - payload: { title, apiId }, -}); - -export const executeCommandAction = (payload: SlashCommandPayload) => ({ - type: ReduxActionTypes.EXECUTE_COMMAND, - payload: payload, -}); - -export const setApiPaneConfigSelectedTabIndex: ( - payload: number, -) => ReduxAction<{ selectedTabIndex: number }> = (payload: number) => ({ - type: ReduxActionTypes.SET_API_PANE_CONFIG_SELECTED_TAB, - payload: { selectedTabIndex: payload }, -}); - -export const setApiPaneDebuggerState = ( - payload: Partial, -) => ({ - type: ReduxActionTypes.SET_API_PANE_DEBUGGER_STATE, - payload, -}); diff --git a/app/client/src/actions/pluginActionActions.ts b/app/client/src/actions/pluginActionActions.ts index 81a0edf9d06..ca6c9988d52 100644 --- a/app/client/src/actions/pluginActionActions.ts +++ b/app/client/src/actions/pluginActionActions.ts @@ -7,7 +7,11 @@ import { ReduxActionTypes, } from "ee/constants/ReduxActionConstants"; import type { JSUpdate } from "utils/JSPaneUtils"; -import type { Action, ActionViewMode } from "entities/Action"; +import type { + Action, + ActionViewMode, + SlashCommandPayload, +} from "entities/Action"; import { ActionExecutionContext } from "entities/Action"; import { batchAction } from "actions/batchActions"; import type { ExecuteErrorPayload } from "constants/AppsmithActionConstants/ActionConstants"; @@ -16,6 +20,7 @@ import type { OtlpSpan } from "UITelemetry/generateTraces"; import type { ApiResponse } from "api/ApiResponses"; import type { JSCollection } from "entities/JSCollection"; import type { ErrorActionPayload } from "sagas/ErrorSagas"; +import type { EventLocation } from "ee/utils/analyticsUtilTypes"; export const createActionRequest = (payload: Partial) => { return { @@ -428,13 +433,31 @@ export const closeQueryActionTabSuccess = (payload: { }; }; -export default { - createAction: createActionRequest, - fetchActions, - runAction: runAction, - deleteAction, - deleteActionSuccess, - updateAction, - updateActionSuccess, - bindDataOnCanvas, -}; +export const createNewApiAction = ( + pageId: string, + from: EventLocation, + apiType?: string, +): ReduxAction<{ pageId: string; from: EventLocation; apiType?: string }> => ({ + type: ReduxActionTypes.CREATE_NEW_API_ACTION, + payload: { pageId, from, apiType }, +}); + +export const createNewQueryAction = ( + pageId: string, + from: EventLocation, + datasourceId: string, + queryDefaultTableName?: string, +): ReduxAction<{ + pageId: string; + from: EventLocation; + datasourceId: string; + queryDefaultTableName?: string; +}> => ({ + type: ReduxActionTypes.CREATE_NEW_QUERY_ACTION, + payload: { pageId, from, datasourceId, queryDefaultTableName }, +}); + +export const executeCommandAction = (payload: SlashCommandPayload) => ({ + type: ReduxActionTypes.EXECUTE_COMMAND, + payload: payload, +}); diff --git a/app/client/src/actions/queryPaneActions.ts b/app/client/src/actions/queryPaneActions.ts deleted file mode 100644 index 67a2b88f1c0..00000000000 --- a/app/client/src/actions/queryPaneActions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ReduxAction } from "ee/constants/ReduxActionConstants"; -import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; -import type { Action } from "entities/Action"; -import type { QueryPaneDebuggerState } from "ee/reducers/uiReducers/queryPaneReducer"; - -export interface ChangeQueryPayload { - baseQueryId: string; - packageId?: string; - applicationId?: string; - basePageId?: string; - moduleId?: string; - workflowId?: string; - newQuery?: boolean; - action?: Action; -} - -export const changeQuery = (payload: ChangeQueryPayload) => { - return { - type: ReduxActionTypes.QUERY_PANE_CHANGE, - payload, - }; -}; - -export const setQueryPaneConfigSelectedTabIndex: ( - payload: string, -) => ReduxAction<{ selectedTabIndex: string }> = (payload: string) => ({ - type: ReduxActionTypes.SET_QUERY_PANE_CONFIG_SELECTED_TAB, - payload: { selectedTabIndex: payload }, -}); - -export const setQueryPaneDebuggerState = ( - payload: Partial, -) => { - return { - type: ReduxActionTypes.SET_QUERY_PANE_DEBUGGER_STATE, - payload, - }; -}; diff --git a/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx b/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx index 54e4fdf45e1..b303bd34a05 100644 --- a/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx +++ b/app/client/src/ce/PluginActionEditor/components/PluginActionResponse/hooks/usePluginActionResponseTabs.tsx @@ -4,7 +4,7 @@ import type { BottomTab } from "components/editorComponents/EntityBottomTabs"; import { getIDEViewMode } from "selectors/ideSelectors"; import { useSelector } from "react-redux"; import { EditorViewMode } from "ee/entities/IDE/constants"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import { createMessage, DEBUGGER_ERRORS, @@ -20,7 +20,7 @@ import { ApiResponseHeaders } from "PluginActionEditor/components/PluginActionRe import { noop } from "lodash"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import { getErrorCount } from "selectors/debuggerSelectors"; -import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors"; +import { getPluginActionDebuggerState } from "PluginActionEditor/store"; import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers"; import useShowSchema from "components/editorComponents/ActionRightPane/useShowSchema"; import Schema from "components/editorComponents/Debugger/Schema"; @@ -38,7 +38,7 @@ function usePluginActionResponseTabs() { const showSchema = useShowSchema(plugin.id) && pluginRequireDatasource; - const { responseTabHeight } = useSelector(getApiPaneDebuggerState); + const { responseTabHeight } = useSelector(getPluginActionDebuggerState); const tabs: BottomTab[] = []; @@ -110,7 +110,7 @@ function usePluginActionResponseTabs() { if (showSchema) { newTabs.push({ - key: "schema", + key: DEBUGGER_TAB_KEYS.SCHEMA_TAB, title: "Schema", panelComponent: ( ; - isSaving: Record; // RN - isDeleting: Record; - isDirty: Record; - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extraformData: Record; - selectedConfigTabIndex: number; - debugger: ApiPaneDebuggerState; -} - -export const handlers = { - [ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ApiPaneReduxState) => ({ - ...state, - isFetching: true, - }), - [ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: (state: ApiPaneReduxState) => ({ - ...state, - isFetching: false, - }), - [ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ApiPaneReduxState) => ({ - ...state, - isFetching: false, - }), - [ReduxActionTypes.CREATE_ACTION_INIT]: ( - state: ApiPaneReduxState, - ): ApiPaneReduxState => ({ - ...state, - isCreating: true, - }), - [ReduxActionTypes.CREATE_ACTION_SUCCESS]: ( - state: ApiPaneReduxState, - ): ApiPaneReduxState => ({ - ...state, - isCreating: false, - }), - [ReduxActionErrorTypes.CREATE_ACTION_ERROR]: ( - state: ApiPaneReduxState, - ): ApiPaneReduxState => ({ - ...state, - isCreating: false, - }), - [ReduxActionTypes.RUN_ACTION_REQUEST]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isRunning: { - ...state.isRunning, - [action.payload.id]: true, - }, - debugger: { - ...state.debugger, - open: true, - }, - }), - [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( - state: ApiPaneReduxState, - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - action: ReduxAction<{ [id: string]: any }>, - ) => { - const actionId = Object.keys(action.payload)[0]; - - return { - ...state, - isRunning: { - ...state.isRunning, - [actionId]: false, - }, - }; - }, - [ReduxActionErrorTypes.RUN_ACTION_ERROR]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isRunning: { - ...state.isRunning, - [action.payload.id]: false, - }, - }), - [ReduxActionTypes.RUN_ACTION_CANCELLED]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ): ApiPaneReduxState => ({ - ...state, - isRunning: { - ...state.isRunning, - [action.payload.id]: false, - }, - }), - [ReduxActionTypes.UPDATE_ACTION_PROPERTY]: ( - state: ApiPaneReduxState, - action: ReduxAction, - ) => ({ - ...state, - isDirty: { - ...state.isDirty, - [action.payload.id]: true, - }, - }), - [ReduxActionTypes.UPDATE_ACTION_INIT]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isSaving: { - ...state.isSaving, - [action.payload.id]: true, - }, - }), - [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ data: Action }>, - ) => ({ - ...state, - isSaving: { - ...state.isSaving, - [action.payload.data.id]: false, - }, - isDirty: { - ...state.isDirty, - [action.payload.data.id]: false, - }, - }), - [ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isSaving: { - ...state.isSaving, - [action.payload.id]: false, - }, - }), - [ReduxActionTypes.DELETE_ACTION_INIT]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isDeleting: { - ...state.isDeleting, - [action.payload.id]: true, - }, - }), - [ReduxActionTypes.DELETE_ACTION_SUCCESS]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isDeleting: { - ...state.isDeleting, - [action.payload.id]: false, - }, - }), - [ReduxActionErrorTypes.DELETE_ACTION_ERROR]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isDeleting: { - ...state.isDeleting, - [action.payload.id]: false, - }, - }), - - /** - * This redux action sets the extraformData field for an action. This is used to select the - * appropriate body type tab selection in the Rest API plugin based on the content-type. - * This redux action can be extended to other functionalities as well in the future. - * - * @param {ApiPaneReduxState} state - * @param {ReduxAction} action - * @returns {ApiPaneReduxState} - */ - [ReduxActionTypes.SET_EXTRA_FORMDATA]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string; values: Record }>, - ): ApiPaneReduxState => { - const { id, values } = action.payload; - - return { - ...state, - extraformData: { - ...state.extraformData, - [id]: values, - }, - }; - }, - [ReduxActionTypes.SET_API_PANE_CONFIG_SELECTED_TAB]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ selectedTabIndex: number }>, - ) => { - const { selectedTabIndex } = action.payload; - - return { - ...state, - selectedConfigTabIndex: selectedTabIndex, - }; - }, - [ReduxActionTypes.SET_API_PANE_DEBUGGER_STATE]: ( - state: ApiPaneReduxState, - action: ReduxAction>, - ) => { - return { - ...state, - debugger: { - ...state.debugger, - ...action.payload, - }, - }; - }, - [ReduxActionTypes.RESET_EDITOR_REQUEST]: (state: ApiPaneReduxState) => { - return { - ...state, - isSaving: {}, - }; - }, -}; - -const apiPaneReducer = createReducer(initialState, handlers); - -export default apiPaneReducer; diff --git a/app/client/src/ce/reducers/uiReducers/index.tsx b/app/client/src/ce/reducers/uiReducers/index.tsx index 0a746521b70..35c9294b396 100644 --- a/app/client/src/ce/reducers/uiReducers/index.tsx +++ b/app/client/src/ce/reducers/uiReducers/index.tsx @@ -3,7 +3,6 @@ import errorReducer from "reducers/uiReducers/errorReducer"; import propertyPaneReducer from "reducers/uiReducers/propertyPaneReducer"; import appViewReducer from "reducers/uiReducers/appViewReducer"; import applicationsReducer from "ee/reducers/uiReducers/applicationsReducer"; -import apiPaneReducer from "ee/reducers/uiReducers/apiPaneReducer"; import datasourcePaneReducer from "reducers/uiReducers/datasourcePaneReducer"; import authReducer from "reducers/uiReducers/authReducer"; import workspaceReducer from "ee/reducers/uiReducers/workspaceReducer"; @@ -12,7 +11,6 @@ import buildingBlockReducer from "reducers/uiReducers/buildingBlockReducer"; import usersReducer from "reducers/uiReducers/usersReducer"; import { widgetDraggingReducer } from "reducers/uiReducers/dragResizeReducer"; import importReducer from "reducers/uiReducers/importReducer"; -import queryPaneReducer from "ee/reducers/uiReducers/queryPaneReducer"; import helpReducer from "reducers/uiReducers/helpReducer"; import apiNameReducer from "ee/reducers/uiReducers/apiNameReducer"; import explorerReducer from "ee/reducers/uiReducers/explorerReducer"; @@ -48,8 +46,9 @@ import layoutConversionReducer from "reducers/uiReducers/layoutConversionReducer import oneClickBindingReducer from "reducers/uiReducers/oneClickBindingReducer"; import activeFieldReducer from "reducers/uiReducers/activeFieldEditorReducer"; import selectedWorkspaceReducer from "ee/reducers/uiReducers/selectedWorkspaceReducer"; -import ideReducer from "../../../reducers/uiReducers/ideReducer"; +import ideReducer from "reducers/uiReducers/ideReducer"; import consolidatedPageLoadReducer from "reducers/uiReducers/consolidatedPageLoadReducer"; +import { pluginActionReducer } from "PluginActionEditor/store"; export const uiReducerObject = { analytics: analyticsReducer, @@ -59,7 +58,6 @@ export const uiReducerObject = { tableFilterPane: tableFilterPaneReducer, appView: appViewReducer, applications: applicationsReducer, - apiPane: apiPaneReducer, auth: authReducer, templates: templateReducer, buildingBlocks: buildingBlockReducer, @@ -68,7 +66,6 @@ export const uiReducerObject = { users: usersReducer, widgetDragResize: widgetDraggingReducer, imports: importReducer, - queryPane: queryPaneReducer, datasourcePane: datasourcePaneReducer, datasourceName: datasourceNameReducer, help: helpReducer, @@ -104,4 +101,5 @@ export const uiReducerObject = { activeField: activeFieldReducer, ide: ideReducer, consolidatedPageLoad: consolidatedPageLoadReducer, + pluginActionEditor: pluginActionReducer, }; diff --git a/app/client/src/ce/reducers/uiReducers/queryPaneReducer.ts b/app/client/src/ce/reducers/uiReducers/queryPaneReducer.ts deleted file mode 100644 index 83b6d37aab2..00000000000 --- a/app/client/src/ce/reducers/uiReducers/queryPaneReducer.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { createReducer } from "utils/ReducerUtils"; -import type { ReduxAction } from "ee/constants/ReduxActionConstants"; -import { - ReduxActionTypes, - ReduxActionErrorTypes, -} from "ee/constants/ReduxActionConstants"; -import { omit } from "lodash"; -import type { Action } from "entities/Action"; -import type { ActionResponse } from "api/ActionAPI"; -import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; -import { DEBUGGER_TAB_KEYS } from "../../../components/editorComponents/Debugger/helpers"; - -export const initialState: QueryPaneReduxState = { - isRunning: {}, - isSaving: {}, - isDeleting: {}, - runErrorMessage: {}, - debugger: { - open: false, - responseTabHeight: ActionExecutionResizerHeight, - }, - selectedConfigTabIndex: "0", -}; - -export interface QueryPaneDebuggerState { - open: boolean; - responseTabHeight: number; - selectedTab?: string; -} - -export interface QueryPaneReduxState { - isRunning: Record; - isSaving: Record; // RR - isDeleting: Record; - runErrorMessage: Record; - selectedConfigTabIndex: string; - debugger: QueryPaneDebuggerState; -} - -export const handlers = { - [ReduxActionTypes.CREATE_ACTION_INIT]: (state: QueryPaneReduxState) => { - return { - ...state, - isCreating: true, - }; - }, - [ReduxActionTypes.CREATE_ACTION_SUCCESS]: (state: QueryPaneReduxState) => { - return { - ...state, - isCreating: false, - }; - }, - [ReduxActionErrorTypes.CREATE_ACTION_ERROR]: (state: QueryPaneReduxState) => { - return { - ...state, - isCreating: false, - }; - }, - [ReduxActionTypes.QUERY_PANE_CHANGE]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - lastUsed: action.payload.id, - }), - [ReduxActionTypes.UPDATE_ACTION_INIT]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isSaving: { - ...state.isSaving, - [action.payload.id]: true, - }, - }), - [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ data: Action }>, - ) => ({ - ...state, - isSaving: { - ...state.isSaving, - [action.payload.data.id]: false, - }, - }), - [ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isSaving: { - ...state.isSaving, - [action.payload.id]: false, - }, - }), - [ReduxActionTypes.DELETE_ACTION_INIT]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isDeleting: { - ...state.isDeleting, - [action.payload.id]: true, - }, - }), - [ReduxActionTypes.DELETE_ACTION_SUCCESS]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isDeleting: { - ...state.isDeleting, - [action.payload.id]: false, - }, - }), - [ReduxActionErrorTypes.DELETE_ACTION_ERROR]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => ({ - ...state, - isDeleting: { - ...state.isDeleting, - [action.payload.id]: false, - }, - }), - [ReduxActionTypes.RUN_ACTION_REQUEST]: ( - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: any, - action: ReduxAction<{ id: string }>, - ): QueryPaneReduxState => { - return { - ...state, - isRunning: { - ...state.isRunning, - [action.payload.id]: true, - }, - debugger: { - ...state.debugger, - selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, - open: true, - }, - }; - }, - - [ReduxActionTypes.RUN_ACTION_CANCELLED]: ( - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: any, - action: ReduxAction<{ id: string }>, - ) => { - return { - ...state, - isRunning: { - ...state.isRunning, - [action.payload.id]: false, - }, - }; - }, - - [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: any, - action: ReduxAction<{ [id: string]: ActionResponse }>, - ) => { - const actionId = Object.keys(action.payload)[0]; - - return { - ...state, - isRunning: { - ...state.isRunning, - [actionId]: false, - }, - runErrorMessage: omit(state.runErrorMessage, [actionId]), - }; - }, - [ReduxActionErrorTypes.RUN_ACTION_ERROR]: ( - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: any, - action: ReduxAction<{ id: string; error: Error }>, - ) => { - const { error, id } = action.payload; - - return { - ...state, - isRunning: { - ...state.isRunning, - [id]: false, - }, - runErrorMessage: { - ...state.runError, - [id]: error.message, - }, - }; - }, - [ReduxActionTypes.SET_QUERY_PANE_CONFIG_SELECTED_TAB]: ( - state: QueryPaneReduxState, - action: ReduxAction<{ selectedTabIndex: number }>, - ) => { - const { selectedTabIndex } = action.payload; - - return { - ...state, - selectedConfigTabIndex: selectedTabIndex, - }; - }, - [ReduxActionTypes.SET_QUERY_PANE_DEBUGGER_STATE]: ( - state: QueryPaneReduxState, - action: ReduxAction>, - ) => { - return { - ...state, - debugger: { - ...state.debugger, - ...action.payload, - }, - }; - }, - [ReduxActionTypes.RESET_EDITOR_REQUEST]: (state: QueryPaneReduxState) => { - return { - ...state, - isSaving: {}, - }; - }, -}; - -const queryPaneReducer = createReducer(initialState, handlers); - -export default queryPaneReducer; diff --git a/app/client/src/ce/selectors/appIDESelectors.ts b/app/client/src/ce/selectors/appIDESelectors.ts index d7df0c96b1e..3d17b4894ef 100644 --- a/app/client/src/ce/selectors/appIDESelectors.ts +++ b/app/client/src/ce/selectors/appIDESelectors.ts @@ -7,6 +7,9 @@ import { } from "ee/selectors/entitiesSelector"; import { getJSTabs, getQueryTabs } from "selectors/ideSelectors"; import type { AppState } from "ee/reducers"; +import { identifyEntityFromPath } from "navigation/FocusEntity"; +import { getCurrentPageId } from "selectors/editorSelectors"; +import { getQueryEntityItemUrl } from "ee/pages/Editor/IDE/EditorPane/Query/utils"; export type EditorSegmentList = Array<{ group: string | "NA"; @@ -68,3 +71,16 @@ export const selectQuerySegmentEditorTabs = (state: AppState) => { return tabs.map((tab) => keyedItems[tab]).filter(Boolean); }; + +export const getLastQueryTab = createSelector( + selectQuerySegmentEditorTabs, + getCurrentPageId, + (tabs, pageId) => { + if (tabs.length) { + const url = getQueryEntityItemUrl(tabs[tabs.length - 1], pageId); + const urlWithoutQueryParams = url.split("?")[0]; + + return identifyEntityFromPath(urlWithoutQueryParams); + } + }, +); diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts index afe96173b64..4f50d471cb0 100644 --- a/app/client/src/ce/selectors/entitiesSelector.ts +++ b/app/client/src/ce/selectors/entitiesSelector.ts @@ -869,28 +869,6 @@ export function getPageNameByPageId(state: AppState, pageId: string): string { return page ? page.pageName : ""; } -const getQueryPaneSavingMap = (state: AppState) => state.ui.queryPane.isSaving; - -export const getApiPaneSavingMap = (state: AppState) => - state.ui.apiPane.isSaving; -const getActionDirtyState = (state: AppState) => state.ui.apiPane.isDirty; - -export const isActionSaving = (id: string) => - createSelector( - [getQueryPaneSavingMap, getApiPaneSavingMap], - (querySavingMap, apiSavingsMap) => { - return ( - (id in querySavingMap && querySavingMap[id]) || - (id in apiSavingsMap && apiSavingsMap[id]) - ); - }, - ); - -export const isActionDirty = (id: string) => - createSelector([getActionDirtyState], (actionDirtyMap) => { - return id in actionDirtyMap && actionDirtyMap[id]; - }); - export const getAppData = (state: AppState) => state.entities.app; export const getAppStoreData = (state: AppState): AppStoreState => diff --git a/app/client/src/components/editorComponents/ApiResponseView.test.tsx b/app/client/src/components/editorComponents/ApiResponseView.test.tsx index cd747429b4f..b440e9a409c 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.test.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.test.tsx @@ -50,7 +50,7 @@ const storeState = { errorCount: 0, }, }, - apiPane: { + pluginActionEditor: { debugger: { open: true, responseTabHeight: 200, diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 623973d2373..e459b509c03 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -14,13 +14,15 @@ import ErrorLogs from "./Debugger/Errors"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import type { BottomTab } from "./EntityBottomTabs"; import EntityBottomTabs from "./EntityBottomTabs"; -import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "./Debugger/constants"; import { getErrorCount } from "selectors/debuggerSelectors"; import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; import type { Action } from "entities/Action"; import { EMPTY_RESPONSE } from "./emptyResponse"; -import { setApiPaneDebuggerState } from "actions/apiPaneActions"; -import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors"; +import { + getPluginActionDebuggerState, + setPluginActionEditorDebuggerState, +} from "PluginActionEditor/store"; import { getIDEViewMode } from "selectors/ideSelectors"; import { EditorViewMode } from "ee/entities/IDE/constants"; import useDebuggerTriggerClick from "./Debugger/hooks/useDebuggerTriggerClick"; @@ -49,7 +51,7 @@ function ApiResponseView(props: Props) { const dispatch = useDispatch(); const errorCount = useSelector(getErrorCount); const { open, responseTabHeight, selectedTab } = useSelector( - getApiPaneDebuggerState, + getPluginActionDebuggerState, ); const ideViewMode = useSelector(getIDEViewMode); @@ -72,7 +74,9 @@ function ApiResponseView(props: Props) { }); } - dispatch(setApiPaneDebuggerState({ open: true, selectedTab: tabKey })); + dispatch( + setPluginActionEditorDebuggerState({ open: true, selectedTab: tabKey }), + ); }, [dispatch], ); @@ -80,7 +84,9 @@ function ApiResponseView(props: Props) { // update the height of the response pane on resize. const updateResponsePaneHeight = useCallback( (height: number) => { - dispatch(setApiPaneDebuggerState({ responseTabHeight: height })); + dispatch( + setPluginActionEditorDebuggerState({ responseTabHeight: height }), + ); }, [dispatch], ); @@ -135,7 +141,7 @@ function ApiResponseView(props: Props) { // close the debugger //TODO: move this to a common place const toggleHide = useCallback( - () => dispatch(setApiPaneDebuggerState({ open: !open })), + () => dispatch(setPluginActionEditorDebuggerState({ open: !open })), [dispatch, open], ); diff --git a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx index 8e19d836f53..1f691141b9b 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx @@ -29,7 +29,7 @@ import { getEntityNameAndPropertyPath } from "ee/workers/Evaluation/evaluationUt import { getPathNavigationUrl } from "selectors/navigationSelectors"; import { Button, Icon, Link, toast, Tooltip } from "@appsmith/ads"; import type { EvaluationError } from "utils/DynamicBindingUtils"; -import { DEBUGGER_TAB_KEYS } from "../Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "../Debugger/constants"; const modifiers: IPopoverSharedProps["modifiers"] = { offset: { @@ -92,6 +92,7 @@ const CurrentValueWrapper = styled.div<{ colorTheme: EditorTheme }>` display: flex; align-items: center; justify-content: space-between; + &:hover { .copyIconWrapper { display: flex; @@ -99,6 +100,7 @@ const CurrentValueWrapper = styled.div<{ colorTheme: EditorTheme }>` } /* for audit logs */ + .pushed-content .object-key-val, .variable-row { border-left: 1px solid var(--ads-v2-color-border) !important; @@ -173,6 +175,7 @@ const StyledIcon = styled(Icon)` &.open-collapse { transform: rotate(90deg); } + float: right; `; @@ -281,6 +284,7 @@ interface PreparedStatementValue { value: string; parameters: Record; } + export function PreparedStatementViewer(props: { evaluatedValue: PreparedStatementValue; }) { diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index fb825654305..2e67b7514e5 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -95,7 +95,6 @@ import { getRecentEntityIds } from "selectors/globalSearchSelectors"; import type { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import type { Placement } from "@blueprintjs/popover2"; import { getLintAnnotations, getLintTooltipDirection } from "./lintHelpers"; -import { executeCommandAction } from "actions/apiPaneActions"; import { startingEntityUpdate } from "actions/editorActions"; import type { SlashCommandPayload } from "entities/Action"; import type { Indices } from "constants/Layers"; @@ -163,6 +162,7 @@ import { import CodeMirrorTernService from "utils/autocomplete/CodemirrorTernService"; import { getEachEntityInformation } from "ee/utils/autocomplete/EntityDefinitions"; import { getCurrentPageId } from "selectors/editorSelectors"; +import { executeCommandAction } from "actions/pluginActionActions"; type ReduxStateProps = ReturnType; type ReduxDispatchProps = ReturnType; diff --git a/app/client/src/components/editorComponents/Debugger/DebugCTA.tsx b/app/client/src/components/editorComponents/Debugger/DebugCTA.tsx index 9aafa87a247..a6c6e4a23ab 100644 --- a/app/client/src/components/editorComponents/Debugger/DebugCTA.tsx +++ b/app/client/src/components/editorComponents/Debugger/DebugCTA.tsx @@ -7,7 +7,7 @@ import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { getTypographyByKey } from "@appsmith/ads-old"; import type { Message } from "entities/AppsmithConsole"; import ContextualMenu from "./ContextualMenu"; -import { DEBUGGER_TAB_KEYS } from "./helpers"; +import { DEBUGGER_TAB_KEYS } from "./constants"; import type { FieldEntityInformation } from "../CodeEditor/EditorConfig"; import { Button } from "@appsmith/ads"; diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx index 325b6e29d2d..b4a71622d2c 100644 --- a/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx +++ b/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx @@ -20,7 +20,7 @@ import { DEBUGGER_LOGS, INSPECT_ENTITY, } from "ee/constants/messages"; -import { DEBUGGER_TAB_KEYS } from "./helpers"; +import { DEBUGGER_TAB_KEYS } from "./constants"; import EntityBottomTabs from "../EntityBottomTabs"; import { ActionExecutionResizerHeight } from "pages/Editor/APIEditor/constants"; import { IDEBottomView, ViewHideBehaviour, ViewDisplayMode } from "IDE"; diff --git a/app/client/src/components/editorComponents/Debugger/Schema.tsx b/app/client/src/components/editorComponents/Debugger/Schema.tsx index 46e5b3cc366..ffd16c40e30 100644 --- a/app/client/src/components/editorComponents/Debugger/Schema.tsx +++ b/app/client/src/components/editorComponents/Debugger/Schema.tsx @@ -12,7 +12,7 @@ import DatasourceField from "pages/Editor/DatasourceInfo/DatasourceField"; import { find } from "lodash"; import type { AppState } from "ee/reducers"; import RenderInterimDataState from "pages/Editor/DatasourceInfo/RenderInterimDataState"; -import { getQueryPaneDebuggerState } from "selectors/queryPaneSelectors"; +import { getPluginActionDebuggerState } from "PluginActionEditor/store"; interface Props { datasourceId: string; @@ -24,7 +24,7 @@ const Schema = (props: Props) => { const datasourceStructure = useSelector((state) => getDatasourceStructureById(state, props.datasourceId), ); - const { responseTabHeight } = useSelector(getQueryPaneDebuggerState); + const { responseTabHeight } = useSelector(getPluginActionDebuggerState); const [selectedTable, setSelectedTable] = useState(); const selectedTableItems = find(datasourceStructure?.tables, [ @@ -53,7 +53,7 @@ const Schema = (props: Props) => { if (!selectedTable && datasourceStructure?.tables?.length && !isLoading) { setSelectedTable(datasourceStructure.tables[0].name); } - }, [selectedTable, props.datasourceId, isLoading]); + }, [selectedTable, props.datasourceId, isLoading, datasourceStructure]); if (!datasourceStructure) { return ( diff --git a/app/client/src/components/editorComponents/Debugger/constants.ts b/app/client/src/components/editorComponents/Debugger/constants.ts new file mode 100644 index 00000000000..c69ccdb217e --- /dev/null +++ b/app/client/src/components/editorComponents/Debugger/constants.ts @@ -0,0 +1,8 @@ +export enum DEBUGGER_TAB_KEYS { + SCHEMA_TAB = "SCHEMA_TAB", + RESPONSE_TAB = "RESPONSE_TAB", + HEADER_TAB = "HEADERS_TAB", + ERROR_TAB = "ERROR_TAB", + LOGS_TAB = "LOGS_TAB", + INSPECT_TAB = "INSPECT_TAB", +} diff --git a/app/client/src/components/editorComponents/Debugger/helpers.tsx b/app/client/src/components/editorComponents/Debugger/helpers.tsx index 287a30a901b..3dcc6bfb984 100644 --- a/app/client/src/components/editorComponents/Debugger/helpers.tsx +++ b/app/client/src/components/editorComponents/Debugger/helpers.tsx @@ -7,8 +7,8 @@ import { createMessage, OPEN_THE_DEBUGGER, PRESS } from "ee/constants/messages"; import type { DependencyMap } from "utils/DynamicBindingUtils"; import { isChildPropertyPath } from "utils/DynamicBindingUtils"; import { - matchBuilderPath, matchApiPath, + matchBuilderPath, matchQueryPath, } from "constants/routes"; import { getEntityNameAndPropertyPath } from "ee/workers/Evaluation/evaluationUtils"; @@ -22,8 +22,8 @@ const BlankStateWrapper = styled.div` justify-content: center; align-items: center; color: var(--ads-v2-color-fg); - ${getTypographyByKey("p1")} + ${getTypographyByKey("p1")} .debugger-shortcut { color: var(--ads-v2-color-fg); ${getTypographyByKey("h5")} @@ -51,15 +51,6 @@ export function BlankState(props: { ); } -export enum DEBUGGER_TAB_KEYS { - SCHEMA_TAB = "schema", - RESPONSE_TAB = "response", - HEADER_TAB = "headers", - ERROR_TAB = "ERROR", - LOGS_TAB = "LOGS_TAB", - INSPECT_TAB = "INSPECT_TAB", -} - export const SeverityIcon: Record = { [Severity.INFO]: "success", [Severity.ERROR]: "close-circle", diff --git a/app/client/src/components/editorComponents/Debugger/hooks/useDebuggerTriggerClick.ts b/app/client/src/components/editorComponents/Debugger/hooks/useDebuggerTriggerClick.ts index f6ae639baf9..11e38440fc0 100644 --- a/app/client/src/components/editorComponents/Debugger/hooks/useDebuggerTriggerClick.ts +++ b/app/client/src/components/editorComponents/Debugger/hooks/useDebuggerTriggerClick.ts @@ -1,15 +1,15 @@ import { useLocation } from "react-router"; -import { DEBUGGER_TAB_KEYS } from "../helpers"; +import { DEBUGGER_TAB_KEYS } from "../constants"; import { setCanvasDebuggerState } from "actions/debuggerActions"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import type { FocusEntityInfo } from "navigation/FocusEntity"; import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity"; import { setJsPaneDebuggerState } from "actions/jsPaneActions"; -import { setApiPaneDebuggerState } from "actions/apiPaneActions"; -import { setQueryPaneDebuggerState } from "actions/queryPaneActions"; import { getJsPaneDebuggerState } from "selectors/jsPaneSelectors"; -import { getApiPaneDebuggerState } from "selectors/apiPaneSelectors"; -import { getQueryPaneDebuggerState } from "selectors/queryPaneSelectors"; +import { + getPluginActionDebuggerState, + setPluginActionEditorDebuggerState, +} from "PluginActionEditor/store"; import { getCanvasDebuggerState } from "selectors/debuggerSelectors"; import { getIDEViewMode } from "selectors/ideSelectors"; import { useDispatch, useSelector } from "react-redux"; @@ -30,26 +30,15 @@ const canvasDebuggerConfig: Config = { get: getCanvasDebuggerState, }; -const queryDebuggerConfig: Config = { - set: setQueryPaneDebuggerState, - get: getQueryPaneDebuggerState, +const pluginActionEditorDebuggerConfig: Config = { + set: setPluginActionEditorDebuggerState, + get: getPluginActionDebuggerState, }; const getConfig = (focusInfo: FocusEntityInfo): Config => { switch (focusInfo.entity) { case FocusEntity.QUERY: - if (focusInfo.params.baseApiId) { - if (focusInfo.params.pluginPackageName) { - return queryDebuggerConfig; - } - - return { - set: setApiPaneDebuggerState, - get: getApiPaneDebuggerState, - }; - } - - return queryDebuggerConfig; + return pluginActionEditorDebuggerConfig; case FocusEntity.JS_OBJECT: return { set: setJsPaneDebuggerState, diff --git a/app/client/src/components/editorComponents/EntityBottomTabs.tsx b/app/client/src/components/editorComponents/EntityBottomTabs.tsx index 9e2f05ba043..e649ba41d1a 100644 --- a/app/client/src/components/editorComponents/EntityBottomTabs.tsx +++ b/app/client/src/components/editorComponents/EntityBottomTabs.tsx @@ -1,6 +1,6 @@ import React from "react"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "./Debugger/constants"; import { Tab, TabPanel, Tabs, TabsList } from "@appsmith/ads"; import styled from "styled-components"; import { LIST_HEADER_HEIGHT, FOOTER_MARGIN } from "./Debugger/DebuggerLogs"; @@ -9,11 +9,14 @@ import type { RefObject } from "react"; const TabPanelWrapper = styled(TabPanel)` margin-top: 0; height: calc(100% - ${LIST_HEADER_HEIGHT}); + &.ads-v2-tabs__panel { overflow: auto; } + & .t--code-editor-wrapper.codeWrapper { height: calc(100% - ${FOOTER_MARGIN}); + & .CodeMirror-scroll { box-sizing: border-box; } diff --git a/app/client/src/components/editorComponents/JSResponseView.test.tsx b/app/client/src/components/editorComponents/JSResponseView.test.tsx index 1ca5857b41e..5d3e177cc17 100644 --- a/app/client/src/components/editorComponents/JSResponseView.test.tsx +++ b/app/client/src/components/editorComponents/JSResponseView.test.tsx @@ -13,6 +13,7 @@ import type { JSCollectionData } from "ee/reducers/entityReducers/jsActionsReduc import { PluginType } from "entities/Action"; import "@testing-library/jest-dom/extend-expect"; import { EMPTY_RESPONSE_LAST_HALF } from "ee/constants/messages"; +import { DEBUGGER_TAB_KEYS } from "./Debugger/constants"; jest.mock("ee/utils/actionExecutionUtils"); @@ -43,7 +44,7 @@ const storeState = { debugger: { open: true, responseTabHeight: 200, - selectedTab: "response", + selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, }, }, editor: { diff --git a/app/client/src/components/editorComponents/JSResponseView.tsx b/app/client/src/components/editorComponents/JSResponseView.tsx index 91d8519abe0..96d5fbbf006 100644 --- a/app/client/src/components/editorComponents/JSResponseView.tsx +++ b/app/client/src/components/editorComponents/JSResponseView.tsx @@ -23,7 +23,7 @@ import { Flex, Text } from "@appsmith/ads"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; import type { JSCollectionData } from "ee/reducers/entityReducers/jsActionsReducer"; import type { EvaluationError } from "utils/DynamicBindingUtils"; -import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "./Debugger/constants"; import type { BottomTab } from "./EntityBottomTabs"; import EntityBottomTabs from "./EntityBottomTabs"; import { getIsSavingEntity } from "selectors/editorSelectors"; @@ -56,6 +56,7 @@ const ResponseTabWrapper = styled.div` opacity: 0.8; pointer-events: none; } + .response-run { margin: 0 10px; } @@ -190,7 +191,7 @@ function JSResponseView(props: Props) { const tabs: BottomTab[] = [ { - key: "response", + key: DEBUGGER_TAB_KEYS.RESPONSE_TAB, title: createMessage(DEBUGGER_RESPONSE), panelComponent: ( <> diff --git a/app/client/src/ee/reducers/uiReducers/apiPaneReducer.tsx b/app/client/src/ee/reducers/uiReducers/apiPaneReducer.tsx deleted file mode 100644 index dd2ecbe891d..00000000000 --- a/app/client/src/ee/reducers/uiReducers/apiPaneReducer.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from "ce/reducers/uiReducers/apiPaneReducer"; -import { default as CE_apiPaneReducer } from "ce/reducers/uiReducers/apiPaneReducer"; -export default CE_apiPaneReducer; diff --git a/app/client/src/ee/reducers/uiReducers/queryPaneReducer.ts b/app/client/src/ee/reducers/uiReducers/queryPaneReducer.ts deleted file mode 100644 index c2e8eabefee..00000000000 --- a/app/client/src/ee/reducers/uiReducers/queryPaneReducer.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "ce/reducers/uiReducers/queryPaneReducer"; -import { default as CE_queryPaneReducer } from "ce/reducers/uiReducers/queryPaneReducer"; -export default CE_queryPaneReducer; diff --git a/app/client/src/navigation/FocusElements.ts b/app/client/src/navigation/FocusElements.ts index 94b5c071d2d..9a44d305963 100644 --- a/app/client/src/navigation/FocusElements.ts +++ b/app/client/src/navigation/FocusElements.ts @@ -2,7 +2,7 @@ import type { ReduxAction } from "ee/constants/ReduxActionConstants"; import type { AppState } from "ee/reducers"; export enum FocusElement { - ApiPaneConfigTabs = "ApiPaneConfigTabs", + PluginActionConfigTabs = "PluginActionConfigTabs", CodeEditorHistory = "CodeEditorHistory", EntityCollapsibleState = "EntityCollapsibleState", EntityExplorerWidth = "EntityExplorerWidth", @@ -10,8 +10,6 @@ export enum FocusElement { DatasourceViewMode = "DatasourceViewMode", SelectedDatasource = "SelectedDatasource", DebuggerContext = "DebuggerContext", - ApiRightPaneTabs = "ApiRightPaneTabs", - QueryPaneConfigTabs = "QueryPaneConfigTabs", JSPaneConfigTabs = "JSPaneConfigTabs", PropertySections = "PropertySections", PropertyField = "PropertyField", @@ -26,8 +24,7 @@ export enum FocusElement { SelectedJSObject = "SelectedJSObject", SelectedEntity = "SelectedEntity", IDETabs = "IDETabs", - QueryDebugger = "QueryDebugger", - ApiDebugger = "ApiDebugger", + PluginActionDebugger = "PluginActionDebugger", JSDebugger = "JSDebugger", } diff --git a/app/client/src/pages/Editor/APIEditor/Editor.tsx b/app/client/src/pages/Editor/APIEditor/Editor.tsx index f939edc0be2..a1a79bb7b73 100644 --- a/app/client/src/pages/Editor/APIEditor/Editor.tsx +++ b/app/client/src/pages/Editor/APIEditor/Editor.tsx @@ -21,7 +21,12 @@ import Spinner from "components/editorComponents/Spinner"; import type { CSSProperties } from "styled-components"; import styled from "styled-components"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; -import { changeApi } from "actions/apiPaneActions"; +import { + 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"; @@ -237,7 +242,9 @@ 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, isDeleting, isRunning } = state.ui.apiPane; + const isCreating = isPluginActionCreating(state); + const isDeleting = isActionDeleting(apiId)(state); + const isRunning = isActionRunning(apiId)(state); const pluginId = _.get(apiAction, "pluginId", ""); return { @@ -251,9 +258,9 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { pluginId, paginationType: _.get(apiAction, "actionConfiguration.paginationType"), apiAction, - isRunning: isRunning[apiId], - isDeleting: isDeleting[apiId], - isCreating: isCreating, + isRunning, + isDeleting, + isCreating, applicationId: getCurrentApplicationId(state), }; }; diff --git a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx index a874d99c8a6..72009d651d6 100644 --- a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx +++ b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx @@ -19,7 +19,10 @@ import { TabBehaviour, } from "components/editorComponents/CodeEditor/EditorConfig"; import { Classes } from "@appsmith/ads-old"; -import { updateBodyContentType } from "actions/apiPaneActions"; +import { + getPostBodyFormat, + updatePostBodyContentType, +} from "PluginActionEditor/store"; import type { CodeEditorExpected } from "components/editorComponents/CodeEditor"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import { createMessage, API_PANE_NO_BODY } from "ee/constants/messages"; @@ -199,14 +202,14 @@ const selector = formValueSelector(API_EDITOR_FORM_NAME); // eslint-disable-next-line @typescript-eslint/no-explicit-any const mapDispatchToProps = (dispatch: any) => ({ updateBodyContentType: (contentType: string, apiId: string) => - dispatch(updateBodyContentType(contentType, apiId)), + dispatch(updatePostBodyContentType(contentType, apiId)), }); export default connect((state: AppState) => { const apiId = selector(state, "id"); - const extraFormData = state.ui.apiPane.extraformData[apiId] || {}; - // Defaults to NONE when extraformData is empty - const displayFormat = extraFormData["displayFormat"] || { + 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, }; diff --git a/app/client/src/pages/Editor/ActionSettings.tsx b/app/client/src/pages/Editor/ActionSettings.tsx index 003b085d397..60ec02bcc08 100644 --- a/app/client/src/pages/Editor/ActionSettings.tsx +++ b/app/client/src/pages/Editor/ActionSettings.tsx @@ -4,6 +4,7 @@ import FormControl from "./FormControl"; import log from "loglevel"; import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import styled from "styled-components"; +import { Text } from "@appsmith/ads"; interface ActionSettingsProps { // TODO: Fix this the next time the file is edited @@ -21,9 +22,11 @@ const ActionSettingsWrapper = styled.div` width: 100%; max-width: 600px; padding-bottom: 1px; + .form-config-top { flex-grow: 1; } + .t--form-control-SWITCH { display: flex; flex-shrink: 0; @@ -35,10 +38,16 @@ const ActionSettingsWrapper = styled.div` function ActionSettings(props: ActionSettingsProps): JSX.Element { return ( - {/* TODO: Fix this the next time the file is edited */} - {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - {props.actionSettingsConfig.map((section: any) => - renderEachConfig(section, props.formName), + {!props.actionSettingsConfig ? ( + + Error: No settings config found + + ) : ( + /* TODO: Fix this the next time the file is edited */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + props.actionSettingsConfig.map((section: any) => + renderEachConfig(section, props.formName), + ) )} ); diff --git a/app/client/src/pages/Editor/DataSourceEditor/Debugger.tsx b/app/client/src/pages/Editor/DataSourceEditor/Debugger.tsx index 01d2634f678..f02daa78013 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/Debugger.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/Debugger.tsx @@ -13,7 +13,7 @@ import { showDebugger, } from "actions/debuggerActions"; import EntityBottomTabs from "components/editorComponents/EntityBottomTabs"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import Errors from "components/editorComponents/Debugger/Errors"; import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs"; import EntityDeps from "components/editorComponents/Debugger/EntityDependecies"; @@ -37,30 +37,38 @@ export const ResizerContentContainer = styled.div` flex: 1; position: relative; display: flex; + &.db-form-resizer-content, &.saas-form-resizer-content, &.api-datasource-content-container { flex-direction: column; padding: 0 var(--ads-v2-spaces-7) 0 var(--ads-v2-spaces-7); + & .t--ds-form-header { border-bottom: 1px solid var(--ads-v2-color-border); } } + &.db-form-resizer-content.db-form-resizer-content-show-tabs, &.saas-form-resizer-content.saas-form-resizer-content-show-tabs { padding: 0; + & .t--ds-form-header { border-bottom: none; } } + &.saas-form-resizer-content.saas-form-resizer-content-show-tabs form { padding-bottom: 0; } + border-top: none; + .db-form-content-container { display: flex; flex-direction: column; width: 100%; + form { flex-grow: 1; } diff --git a/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx b/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx index 99dc3bfbe52..d59d1fdd260 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx @@ -18,7 +18,7 @@ import { NEW_API_BUTTON_TEXT, NEW_QUERY_BUTTON_TEXT, } from "ee/constants/messages"; -import { createNewQueryAction } from "actions/apiPaneActions"; +import { createNewQueryAction } from "actions/pluginActionActions"; import { useDispatch, useSelector } from "react-redux"; import { getCurrentPageId, getPageList } from "selectors/editorSelectors"; import type { Datasource } from "entities/Datasource"; diff --git a/app/client/src/pages/Editor/EntityNavigation/ActionPane/ApiPaneNavigation.ts b/app/client/src/pages/Editor/EntityNavigation/ActionPane/ApiPaneNavigation.ts index 2fa0dbb5cfe..4671eaae08b 100644 --- a/app/client/src/pages/Editor/EntityNavigation/ActionPane/ApiPaneNavigation.ts +++ b/app/client/src/pages/Editor/EntityNavigation/ActionPane/ApiPaneNavigation.ts @@ -2,16 +2,15 @@ import { call, delay, put } from "redux-saga/effects"; import type { EntityInfo, IApiPaneNavigationConfig } from "../types"; import { ActionPaneNavigation } from "./exports"; import { API_EDITOR_TABS } from "constants/ApiEditorConstants/CommonApiConstants"; -import { setApiPaneConfigSelectedTabIndex } from "actions/apiPaneActions"; +import { setPluginActionEditorSelectedTab } from "PluginActionEditor/store"; import { NAVIGATION_DELAY } from "../costants"; -import { isNumber } from "lodash"; export default class ApiPaneNavigation extends ActionPaneNavigation { constructor(entityInfo: EntityInfo) { super(entityInfo); this.getConfig = this.getConfig.bind(this); this.navigate = this.navigate.bind(this); - this.getTabIndex = this.getTabIndex.bind(this); + this.getTab = this.getTab.bind(this); } *getConfig() { @@ -19,17 +18,18 @@ export default class ApiPaneNavigation extends ActionPaneNavigation { if (!this.entityInfo.propertyPath) return {}; - const tabIndex: number | undefined = yield call( - this.getTabIndex, + const tab: string | undefined = yield call( + this.getTab, this.entityInfo.propertyPath, ); config = { - tabIndex, + tab, }; return config; } + *navigate() { const config: IApiPaneNavigationConfig = yield call(this.getConfig); @@ -37,17 +37,16 @@ export default class ApiPaneNavigation extends ActionPaneNavigation { if (!this.entityInfo.propertyPath) return; - if (isNumber(config.tabIndex)) { - yield put(setApiPaneConfigSelectedTabIndex(config.tabIndex)); + if (config.tab) { + yield put(setPluginActionEditorSelectedTab(config.tab)); yield delay(NAVIGATION_DELAY); } yield call(this.scrollToView, this.entityInfo.propertyPath); } - *getTabIndex(propertyPath: string) { - let currentTab; - let index; + *getTab(propertyPath: string) { + let currentTab: string | undefined; const modifiedProperty = propertyPath.replace( "config", "actionConfiguration", @@ -79,10 +78,6 @@ export default class ApiPaneNavigation extends ActionPaneNavigation { } } - if (currentTab) { - index = Object.values(API_EDITOR_TABS).indexOf(currentTab); - } - - return index; + return currentTab; } } diff --git a/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts b/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts index 68fc9ab0e9c..2d3ee6dc913 100644 --- a/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts +++ b/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts @@ -2,13 +2,15 @@ import { call, delay, put, race, select, take } from "redux-saga/effects"; import type { EntityInfo, IQueryPaneNavigationConfig } from "../types"; import { ActionPaneNavigation } from "./exports"; import { NAVIGATION_DELAY } from "../costants"; -import { setQueryPaneConfigSelectedTabIndex } from "actions/queryPaneActions"; import { EDITOR_TABS } from "constants/QueryEditorConstants"; import { getFormEvaluationState } from "selectors/formSelectors"; import type { FormEvaluationState } from "reducers/evaluationReducers/formEvaluationReducer"; import { isEmpty } from "lodash"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; -import { isActionSaving } from "ee/selectors/entitiesSelector"; +import { + isActionSaving, + setPluginActionEditorSelectedTab, +} from "PluginActionEditor/store"; export default class QueryPaneNavigation extends ActionPaneNavigation { constructor(entityInfo: EntityInfo) { @@ -44,7 +46,7 @@ export default class QueryPaneNavigation extends ActionPaneNavigation { if (!this.entityInfo.propertyPath) return; if (config.tab) { - yield put(setQueryPaneConfigSelectedTabIndex(config.tab)); + yield put(setPluginActionEditorSelectedTab(config.tab)); } yield call(this.waitForFormUpdate); diff --git a/app/client/src/pages/Editor/EntityNavigation/types.ts b/app/client/src/pages/Editor/EntityNavigation/types.ts index 5d6a173e57d..13de1aa9803 100644 --- a/app/client/src/pages/Editor/EntityNavigation/types.ts +++ b/app/client/src/pages/Editor/EntityNavigation/types.ts @@ -33,7 +33,7 @@ export interface IMatchedSection { } export interface IApiPaneNavigationConfig { - tabIndex?: number; + tab?: string; } export interface IQueryPaneNavigationConfig { diff --git a/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx b/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx index 5c304abc7b0..8bbdc69257f 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx @@ -39,6 +39,7 @@ import { useEditorType } from "ee/hooks"; import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks"; import AIDataSources from "./AIDataSources"; import Debugger from "../DataSourceEditor/Debugger"; +import { isPluginActionCreating } from "PluginActionEditor/store"; const NewIntegrationsContainer = styled.div` ${thinScrollbar}; @@ -381,7 +382,7 @@ const mapStateToProps = (state: AppState) => { return { dataSources: getDatasources(state), mockDatasources: getMockDatasources(state), - isCreating: state.ui.apiPane.isCreating, + isCreating: isPluginActionCreating(state), applicationId: getCurrentApplicationId(state), canCreateDatasource, showDebugger, diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index a2cc86b4072..5121ac1164d 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -70,8 +70,9 @@ import { getHasManageActionPermission, } from "ee/utils/BusinessFeatures/permissionPageHelpers"; import type { JSCollectionData } from "ee/reducers/entityReducers/jsActionsReducer"; -import { DEBUGGER_TAB_KEYS } from "../../../components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import RunHistory from "ee/components/RunHistory"; + interface JSFormProps { jsCollectionData: JSCollectionData; contextMenu: React.ReactNode; @@ -99,6 +100,7 @@ const SecondaryWrapper = styled.div` flex-direction: column; flex: 1; overflow: hidden; + &&& { .ads-v2-tabs, &.js-editor-tab { diff --git a/app/client/src/pages/Editor/QueryEditor/Editor.tsx b/app/client/src/pages/Editor/QueryEditor/Editor.tsx index 8e6322c78a7..abb9038a2cd 100644 --- a/app/client/src/pages/Editor/QueryEditor/Editor.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Editor.tsx @@ -41,6 +41,11 @@ 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; @@ -253,7 +258,7 @@ class QueryEditor extends React.Component { const mapStateToProps = (state: AppState, props: OwnProps): ReduxStateProps => { const { baseApiId, baseQueryId } = props.match.params; const baseActionId = baseQueryId || baseApiId || ""; - const { runErrorMessage } = state.ui.queryPane; + const { runErrorMessage } = state.ui.pluginActionEditor; const { plugins } = state.entities; const { editorConfigs } = plugins; @@ -272,6 +277,10 @@ const mapStateToProps = (state: AppState, props: OwnProps): ReduxStateProps => { 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; @@ -319,12 +328,12 @@ const mapStateToProps = (state: AppState, props: OwnProps): ReduxStateProps => { ? getDatasourceByPluginId(state, action?.pluginId) : getDBAndRemoteDatasources(state), responses: getActionResponses(state), - isRunning: state.ui.queryPane.isRunning[actionId], - isDeleting: state.ui.queryPane.isDeleting[actionId], + isCreating, + isRunning, + isDeleting, isSaas: !!baseApiId, formData, editorConfig, - isCreating: state.ui.apiPane.isCreating, uiComponent, applicationId: getCurrentApplicationId(state), actionObjectDiff, diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index 370964cc6dc..02650c405a6 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -23,8 +23,10 @@ import type { Plugin } from "api/PluginApi"; import type { UIComponentTypes } from "api/PluginApi"; import { EDITOR_TABS } from "constants/QueryEditorConstants"; import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer"; -import { getQueryPaneConfigSelectedTabIndex } from "selectors/queryPaneSelectors"; -import { setQueryPaneConfigSelectedTabIndex } from "actions/queryPaneActions"; +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"; @@ -230,11 +232,11 @@ export function EditorJSONtoForm(props: Props) { id: currentActionConfig ? currentActionConfig.id : "", }; - const selectedConfigTab = useSelector(getQueryPaneConfigSelectedTabIndex); + const selectedTab = useSelector(getPluginActionConfigSelectedTab); const setSelectedConfigTab = useCallback( (selectedIndex: string) => { - dispatch(setQueryPaneConfigSelectedTabIndex(selectedIndex)); + dispatch(setPluginActionEditorSelectedTab(selectedIndex)); }, [dispatch], ); @@ -264,7 +266,7 @@ export function EditorJSONtoForm(props: Props) { diff --git a/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.test.tsx b/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.test.tsx index 6d31b9f64f0..23de0834cd0 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.test.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.test.tsx @@ -42,7 +42,7 @@ const storeState = { errorCount: 0, }, }, - queryPane: { + pluginActionEditor: { debugger: { open: true, responseTabHeight: 200, diff --git a/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx b/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx index 160ce631ac0..f3b66675e77 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx @@ -5,7 +5,7 @@ import { useDispatch, useSelector } from "react-redux"; import styled from "styled-components"; import { getErrorCount } from "selectors/debuggerSelectors"; import { Text, TextType } from "@appsmith/ads-old"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import { DEBUGGER_ERRORS, DEBUGGER_LOGS, @@ -27,12 +27,14 @@ import { import { DatasourceComponentTypes } from "api/PluginApi"; import { fetchDatasourceStructure } from "actions/datasourceActions"; import { DatasourceStructureContext } from "entities/Datasource"; -import { getQueryPaneDebuggerState } from "selectors/queryPaneSelectors"; -import { setQueryPaneDebuggerState } from "actions/queryPaneActions"; +import { + getPluginActionDebuggerState, + setPluginActionEditorDebuggerState, +} from "PluginActionEditor/store"; import { actionResponseDisplayDataFormats } from "../utils"; import { getIDEViewMode } from "selectors/ideSelectors"; import { EditorViewMode } from "ee/entities/IDE/constants"; -import { IDEBottomView, ViewHideBehaviour } from "../../../IDE"; +import { IDEBottomView, ViewHideBehaviour } from "IDE"; const ResultsCount = styled.div` position: absolute; @@ -68,7 +70,7 @@ function QueryDebuggerTabs({ const dispatch = useDispatch(); const { open, responseTabHeight, selectedTab } = useSelector( - getQueryPaneDebuggerState, + getPluginActionDebuggerState, ); const { responseDisplayFormat } = @@ -107,7 +109,12 @@ function QueryDebuggerTabs({ ), ); } - }, []); + }, [ + currentActionConfig, + datasourceStructure, + dispatch, + pluginDatasourceForm, + ]); // These useEffects are used to open the response tab by default for page load queries // as for page load queries, query response is available and can be shown in response tab @@ -121,25 +128,30 @@ function QueryDebuggerTabs({ !showResponseOnFirstLoad ) { dispatch( - setQueryPaneDebuggerState({ + setPluginActionEditorDebuggerState({ open: true, selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, }), ); setShowResponseOnFirstLoad(true); } - }, [responseDisplayFormat, actionResponse, showResponseOnFirstLoad]); + }, [ + responseDisplayFormat, + actionResponse, + showResponseOnFirstLoad, + dispatch, + ]); useEffect(() => { if (showSchema && !selectedTab) { dispatch( - setQueryPaneDebuggerState({ + setPluginActionEditorDebuggerState({ open: true, selectedTab: DEBUGGER_TAB_KEYS.SCHEMA_TAB, }), ); } - }, [showSchema, currentActionConfig?.id, selectedTab]); + }, [showSchema, selectedTab, dispatch]); // When multiple page load queries exist, we want to response tab by default for all of them // Hence this useEffect will reset showResponseOnFirstLoad flag used to track whether to show response tab or not @@ -170,17 +182,27 @@ function QueryDebuggerTabs({ } } - const setQueryResponsePaneHeight = useCallback((height: number) => { - dispatch(setQueryPaneDebuggerState({ responseTabHeight: height })); - }, []); + const setQueryResponsePaneHeight = useCallback( + (height: number) => { + dispatch( + setPluginActionEditorDebuggerState({ responseTabHeight: height }), + ); + }, + [dispatch], + ); const onToggle = useCallback(() => { - dispatch(setQueryPaneDebuggerState({ open: !open })); - }, [open]); + dispatch(setPluginActionEditorDebuggerState({ open: !open })); + }, [dispatch, open]); - const setSelectedResponseTab = useCallback((tabKey: string) => { - dispatch(setQueryPaneDebuggerState({ open: true, selectedTab: tabKey })); - }, []); + const setSelectedResponseTab = useCallback( + (tabKey: string) => { + dispatch( + setPluginActionEditorDebuggerState({ open: true, selectedTab: tabKey }), + ); + }, + [dispatch], + ); const ideViewMode = useSelector(getIDEViewMode); @@ -204,7 +226,7 @@ function QueryDebuggerTabs({ if (currentActionConfig) { responseTabs.unshift({ - key: "response", + key: DEBUGGER_TAB_KEYS.RESPONSE_TAB, title: createMessage(DEBUGGER_RESPONSE), panelComponent: ( ` ${HelpSection} { margin-bottom: 10px; } + position: relative; `; @@ -97,7 +100,7 @@ const QueryResponseTab = (props: Props) => { const actionResponse = useSelector((state) => getActionData(state, currentActionConfig.id), ); - const { responseTabHeight } = useSelector(getQueryPaneDebuggerState); + const { responseTabHeight } = useSelector(getPluginActionDebuggerState); const { responseDataTypes, responseDisplayFormat } = actionResponseDisplayDataFormats(actionResponse); @@ -216,8 +219,12 @@ const QueryResponseTab = (props: Props) => { } const navigateToSettings = useCallback(() => { - dispatch(setQueryPaneConfigSelectedTabIndex(EDITOR_TABS.SETTINGS)); - }, []); + dispatch( + setPluginActionEditorDebuggerState({ + selectedTab: EDITOR_TABS.SETTINGS, + }), + ); + }, [dispatch]); const preparedStatementCalloutLinks: CalloutLinkProps[] = [ { diff --git a/app/client/src/pages/Editor/QueryEditor/index.tsx b/app/client/src/pages/Editor/QueryEditor/index.tsx index 03457a6228e..2d458fb90c9 100644 --- a/app/client/src/pages/Editor/QueryEditor/index.tsx +++ b/app/client/src/pages/Editor/QueryEditor/index.tsx @@ -13,7 +13,7 @@ import { getIsEditorInitialized, getPagePermissions, } from "selectors/editorSelectors"; -import { changeQuery } from "actions/queryPaneActions"; +import { changeQuery } from "PluginActionEditor/store"; import { DatasourceCreateEntryPoints } from "constants/Datasource"; import { getActionByBaseId, diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index 0c220b23ec4..5e2d426dc2b 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -44,8 +44,6 @@ import { getDatasource, getJSCollectionFromAllEntities, getPlugin, - isActionDirty, - isActionSaving, } from "ee/selectors/entitiesSelector"; import { getIsGitSyncModalOpen } from "selectors/gitSyncSelectors"; import { @@ -72,7 +70,7 @@ import { } from "sagas/ErrorSagas"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import type { Action } from "entities/Action"; -import { ActionExecutionContext, PluginType } from "entities/Action"; +import { ActionExecutionContext } from "entities/Action"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; import { ACTION_EXECUTION_CANCELLED, @@ -99,7 +97,7 @@ import { getLayoutOnLoadActions, getLayoutOnLoadIssues, } from "selectors/editorSelectors"; -import * as log from "loglevel"; +import log from "loglevel"; import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse"; import type { AppState } from "ee/reducers"; import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "ee/constants/ApiConstants"; @@ -140,17 +138,13 @@ import { setDefaultActionDisplayFormat } from "./PluginActionSagaUtils"; import { checkAndLogErrorsIfCyclicDependency } from "sagas/helper"; import { toast } from "@appsmith/ads"; import type { TRunDescription } from "workers/Evaluation/fns/actionFns"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import { FILE_SIZE_LIMIT_FOR_BLOBS } from "constants/WidgetConstants"; import type { ActionData } from "ee/reducers/entityReducers/actionsReducer"; import { handleStoreOperations } from "./StoreActionSaga"; import { fetchPageAction } from "actions/pageActions"; import type { Datasource } from "entities/Datasource"; import { softRefreshDatasourceStructure } from "actions/datasourceActions"; -import { - changeQuery, - setQueryPaneDebuggerState, -} from "actions/queryPaneActions"; import { getCurrentEnvironmentDetails, getCurrentEnvironmentName, @@ -171,7 +165,12 @@ import { } from "ee/utils/actionExecutionUtils"; import type { JSAction, JSCollection } from "entities/JSCollection"; import { getAllowedActionAnalyticsKeys } from "constants/AppsmithActionConstants/formConfig/ActionAnalyticsConfig"; -import { setApiPaneDebuggerState } from "../../actions/apiPaneActions"; +import { + changeQuery, + isActionDirty, + isActionSaving, + setPluginActionEditorDebuggerState, +} from "PluginActionEditor/store"; enum ActionResponseDataTypes { BINARY = "BINARY", @@ -801,7 +800,12 @@ export function* runActionSaga( // open response tab in debugger on exection of action. if (!reduxAction.payload.skipOpeningDebugger) { - yield call(openDebugger, plugin.type); + yield put( + setPluginActionEditorDebuggerState({ + open: true, + selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, + }), + ); } let payload = EMPTY_RESPONSE; @@ -1193,7 +1197,12 @@ function* executePageLoadAction( // open response tab in debugger on exection of action on page load. // Only if current page is the page on which the action is executed. if (window.location.pathname.includes(pageAction.id)) - yield call(openDebugger, plugin.type); + yield put( + setPluginActionEditorDebuggerState({ + open: true, + selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, + }), + ); if (isError) { AppsmithConsole.addErrors([ @@ -1577,23 +1586,6 @@ function triggerFileUploadInstrumentation( }); } -//Open debugger with response tab selected. -function* openDebugger(pluginType: PluginType) { - if (pluginType === PluginType.API) { - yield put( - setApiPaneDebuggerState({ - open: true, - selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, - }), - ); - } else { - setQueryPaneDebuggerState({ - open: true, - selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB, - }); - } -} - // Function to clear the action responses for the actions which are not executeOnLoad. function* clearTriggerActionResponse() { const currentPageActions: ActionData[] = yield select(getCurrentActions); diff --git a/app/client/src/sagas/ActionExecution/errorUtils.ts b/app/client/src/sagas/ActionExecution/errorUtils.ts index 7ee5ddeb9bc..822c6e5d022 100644 --- a/app/client/src/sagas/ActionExecution/errorUtils.ts +++ b/app/client/src/sagas/ActionExecution/errorUtils.ts @@ -9,7 +9,7 @@ import type { ActionTriggerKeys } from "ee/workers/Evaluation/fns"; import { getActionTriggerFunctionNames } from "ee/workers/Evaluation/fns"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { setDebuggerSelectedTab, showDebugger } from "actions/debuggerActions"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import store from "store"; import showToast from "sagas/ToastSagas"; import { call, put } from "redux-saga/effects"; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 4672069bc12..8bb94aa0c5b 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -28,6 +28,8 @@ import { updateCanvasWithDSL } from "ee/sagas/PageSagas"; import { closeQueryActionTab, closeQueryActionTabSuccess, + createNewApiAction, + createNewQueryAction, type FetchActionsPayload, type SetActionPropertyPayload, } from "actions/pluginActionActions"; @@ -101,10 +103,6 @@ import { import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; -import { - createNewApiAction, - createNewQueryAction, -} from "actions/apiPaneActions"; import type { Plugin } from "api/PluginApi"; import { shouldBeDefined } from "utils/helpers"; import { diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index 96550ff7c81..cf6f4cf7904 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -29,7 +29,7 @@ import { import { DEFAULT_CREATE_API_CONFIG } from "constants/ApiEditorConstants/ApiEditorConstants"; import { DEFAULT_CREATE_GRAPHQL_CONFIG } from "constants/ApiEditorConstants/GraphQLEditorConstants"; import history from "utils/history"; -import { initialize, autofill, change, reset } from "redux-form"; +import { autofill, change, initialize, reset } from "redux-form"; import type { Property } from "api/ActionAPI"; import { getQueryParams } from "utils/URLUtils"; import { getPluginIdOfPackageName } from "sagas/selectors"; @@ -61,7 +61,7 @@ import { import { updateReplayEntity } from "actions/pageActions"; import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; import type { Plugin } from "api/PluginApi"; -import { getDisplayFormat } from "selectors/apiPaneSelectors"; +import { getPostBodyFormat } from "../PluginActionEditor/store"; import { apiEditorIdURL, datasourcesEditorIdURL } from "ee/RouteBuilder"; import { getCurrentBasePageId } from "selectors/editorSelectors"; import { validateResponse } from "./ErrorSagas"; @@ -83,6 +83,7 @@ import { checkAndGetPluginFormConfigsSaga } from "./PluginSagas"; import { convertToBasePageIdSelector } from "selectors/pageListSelectors"; import type { ApplicationPayload } from "entities/Application"; import { klonaLiteWithTelemetry } from "utils/helpers"; +import { POST_BODY_FORM_DATA_KEY } from "../PluginActionEditor/constants"; function* syncApiParamsSaga( actionPayload: ReduxActionWithMeta, @@ -256,7 +257,7 @@ function* updateExtraFormDataSaga() { const { values } = formData; // when initializing, check if theres a display format present. - const extraFormData: GetFormData = yield select(getDisplayFormat, values.id); + const extraFormData: GetFormData = yield select(getPostBodyFormat, values.id); const headers: Array<{ key: string; value: string }> = get(values, "actionConfiguration.headers") || []; @@ -367,7 +368,7 @@ function* setApiBodyTabHeaderFormat(apiId: string, apiContentType?: string) { payload: { id: apiId, values: { - displayFormat, + [POST_BODY_FORM_DATA_KEY]: displayFormat, }, }, }); diff --git a/app/client/src/sagas/JSPaneSagas.ts b/app/client/src/sagas/JSPaneSagas.ts index c36ba5073ad..5d7fcec2b86 100644 --- a/app/client/src/sagas/JSPaneSagas.ts +++ b/app/client/src/sagas/JSPaneSagas.ts @@ -86,7 +86,7 @@ import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUt import type { EventLocation } from "ee/utils/analyticsUtilTypes"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { checkAndLogErrorsIfCyclicDependency } from "./helper"; -import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants"; import { getJSActionPathNameToDisplay, isBrowserExecutionAllowed, diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index 6195973f9fd..39bea13dfc1 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -77,7 +77,7 @@ import type { FeatureFlags } from "ee/entities/FeatureFlag"; import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors"; import { isGACEnabled } from "ee/utils/planHelpers"; import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; -import type { ChangeQueryPayload } from "actions/queryPaneActions"; +import type { ChangeQueryPayload } from "PluginActionEditor/store"; import { getApplicationByIdFromWorkspaces, getCurrentApplicationIdForCreateNewApp, diff --git a/app/client/src/selectors/apiPaneSelectors.ts b/app/client/src/selectors/apiPaneSelectors.ts deleted file mode 100644 index 92d53733783..00000000000 --- a/app/client/src/selectors/apiPaneSelectors.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { AppState } from "ee/reducers"; -import { createSelector } from "reselect"; -import { combinedPreviewModeSelector } from "./editorSelectors"; - -type GetFormData = ( - state: AppState, - apiId: string, -) => { label: string; value: string }; - -export const getDisplayFormat: GetFormData = (state, apiId) => { - const displayFormat = state.ui.apiPane.extraformData[apiId]; - - return displayFormat; -}; - -export const getApiPaneConfigSelectedTabIndex = (state: AppState) => - state.ui.apiPane.selectedConfigTabIndex; - -export const getIsRunning = (state: AppState, apiId: string) => - state.ui.apiPane.isRunning[apiId]; - -export const getApiPaneDebuggerState = (state: AppState) => - state.ui.apiPane.debugger; - -export const showApiPaneDebugger = createSelector( - (state) => state.ui.apiPane.debugger.open, - combinedPreviewModeSelector, - (isOpen, isPreview) => isOpen && !isPreview, -); diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 355a1197c3d..ee3560dd503 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -27,8 +27,8 @@ import { } from "selectors/dataTreeSelectors"; import type { MainCanvasReduxState } from "reducers/uiReducers/mainCanvasReducer"; +import { getActionEditorSavingMap } from "PluginActionEditor/store"; import { - getApiPaneSavingMap, getCanvasWidgets, getJSCollections, } from "ee/selectors/entitiesSelector"; @@ -48,6 +48,7 @@ import { protectedModeSelector } from "./gitSyncSelectors"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; import { getCurrentApplication } from "ee/selectors/applicationSelectors"; import type { Page } from "entities/Page"; +import { objectKeys } from "@appsmith/utils"; const getIsDraggingOrResizing = (state: AppState) => state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; @@ -76,7 +77,7 @@ export const getLoadingError = (state: AppState) => export const getIsPageSaving = createSelector( [ - getApiPaneSavingMap, + getActionEditorSavingMap, (state: AppState) => state.ui.jsPane.isSaving, (state: AppState) => state.ui.appTheming.isSaving, (state: AppState) => state.ui.applications.isSavingNavigationSetting, @@ -84,23 +85,23 @@ export const getIsPageSaving = createSelector( (state: AppState) => state.ui.editor.loadingStates.saving, ], ( - savingApis, + savingActions, savingJSObjects, isSavingAppTheme, isSavingNavigationSetting, isEditorSavingEntity, isEditorSaving, ) => { - const areApisSaving = Object.keys(savingApis).some( - (apiId) => savingApis[apiId], + const areActionsSaving = objectKeys(savingActions).some( + (actionId) => savingActions[actionId], ); - const areJsObjectsSaving = Object.keys(savingJSObjects).some( + const areJsObjectsSaving = objectKeys(savingJSObjects).some( (collectionId) => savingJSObjects[collectionId], ); return ( isEditorSavingEntity || - areApisSaving || + areActionsSaving || areJsObjectsSaving || isSavingAppTheme || isEditorSaving || diff --git a/app/client/src/selectors/queryPaneSelectors.ts b/app/client/src/selectors/queryPaneSelectors.ts deleted file mode 100644 index b87485f2be7..00000000000 --- a/app/client/src/selectors/queryPaneSelectors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { AppState } from "ee/reducers"; -import { getCurrentPageId } from "./editorSelectors"; -import type { FocusEntityInfo } from "../navigation/FocusEntity"; -import { identifyEntityFromPath } from "../navigation/FocusEntity"; -import { getQueryEntityItemUrl } from "ee/pages/Editor/IDE/EditorPane/Query/utils"; -import { selectQuerySegmentEditorTabs } from "ee/selectors/appIDESelectors"; - -export const getQueryPaneConfigSelectedTabIndex = (state: AppState) => - state.ui.queryPane.selectedConfigTabIndex; - -export const getQueryPaneDebuggerState = (state: AppState) => - state.ui.queryPane.debugger; - -export const getQueryRunErrorMessage = (state: AppState, id: string) => { - const { runErrorMessage } = state.ui.queryPane; - - return runErrorMessage[id]; -}; - -export const getQueryIsRunning = (state: AppState, id: string): boolean => { - const { isRunning } = state.ui.queryPane; - - return !!isRunning[id]; -}; - -export const getLastQueryTab = ( - state: AppState, -): FocusEntityInfo | undefined => { - const tabs = selectQuerySegmentEditorTabs(state); - const pageId = getCurrentPageId(state); - - if (tabs.length) { - const url = getQueryEntityItemUrl(tabs[tabs.length - 1], pageId); - const urlWithoutQueryParams = url.split("?")[0]; - - return identifyEntityFromPath(urlWithoutQueryParams); - } -}; diff --git a/app/client/src/utils/replayHelpers.tsx b/app/client/src/utils/replayHelpers.tsx index 121235ec143..8b871586af6 100644 --- a/app/client/src/utils/replayHelpers.tsx +++ b/app/client/src/utils/replayHelpers.tsx @@ -16,8 +16,7 @@ import { ACTION_CONFIGURATION_CHANGED, } from "ee/constants/messages"; import { toast } from "@appsmith/ads"; -import { setApiPaneConfigSelectedTabIndex } from "../actions/apiPaneActions"; -import { API_EDITOR_TABS } from "../constants/ApiEditorConstants/CommonApiConstants"; +import { setPluginActionEditorSelectedTab } from "PluginActionEditor/store"; import store from "../store"; /** @@ -173,11 +172,7 @@ export function switchTab(replayId: string): boolean { if (element.getAttribute("data-state") == "active") return false; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const index = Object.values(API_EDITOR_TABS).indexOf(replayId); - - store.dispatch(setApiPaneConfigSelectedTabIndex(index)); + store.dispatch(setPluginActionEditorSelectedTab(replayId)); return true; } From ac91339d5448f396a462356d998d9fcab836cd10 Mon Sep 17 00:00:00 2001 From: Shivam kumar Date: Wed, 9 Oct 2024 09:57:44 +0530 Subject: [PATCH 02/15] chore:remove space b/w form and CTA onboarding page (#35985) ## Description Following are the improvements made in this PR: - Remove the unnecessary space b/w form and CTA in Gsheet onboarding step - Made one new RadioButtonControl in form control and replaced the current dropdown by radio buttons. - Move the callout to after the permissions | scope property. - Limit the width of the white section Fixes #30523 output screenshot: ![Screenshot from 2024-09-20 15-14-59](https://github.com/user-attachments/assets/61b397fb-8735-4b36-8036-a781ab3bd936) Desired design: ![image](https://github.com/user-attachments/assets/df65fab5-c543-4af8-9bb5-f72d8cb4d004) > > _Please also include relevant motivation and context. List any dependencies that are required for this change. Add links to Notion, Figma or any other documents that might be relevant to the PR._ *Fixes #`35950` _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="" ### :mag: Cypress test results > [!CAUTION] > If you modify the content in this section, you are likely to disrupt the CI result for your PR. ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced a new `RadioButtonControl` component for improved form control options. - Enhanced the `FormControlRegistry` to support radio button controls. - Updated the Google Sheets plugin to use radio buttons for permission settings. - **UI Changes** - Corrected styling syntax in the `FormContainer` for proper rendering. - Reorganized the display order of information banners in the `DatasourceForm` for better clarity. - **Tests** - Added a comprehensive suite of unit tests for the `RadioButtonControl` component to ensure proper functionality and user interaction. --- .../cypress/support/Pages/DataSources.ts | 2 +- .../formControls/RadioButtonControl.test.tsx | 118 ++++++++++++++++++ .../formControls/RadioButtonControl.tsx | 70 +++++++++++ .../Editor/SaaSEditor/DatasourceForm.tsx | 22 ++-- .../utils/formControl/FormControlRegistry.tsx | 7 ++ .../src/utils/formControl/formControlTypes.ts | 1 + .../src/main/resources/form.json | 2 +- 7 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 app/client/src/components/formControls/RadioButtonControl.test.tsx create mode 100644 app/client/src/components/formControls/RadioButtonControl.tsx diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index b41cbd05b34..4770826380c 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -194,7 +194,7 @@ export class DataSources { _globalSearchInput = ".t--global-search-input"; _gsScopeDropdown = "[data-testid^='datasourceStorages.'][data-testid$='.datasourceConfiguration.authentication.scopeString']"; - _gsScopeOptions = ".ads-v2-select__dropdown .rc-select-item-option"; + _gsScopeOptions = ".ads-v2-radio-group"; _queryTimeout = "//input[@name='actionConfiguration.timeoutInMillisecond']"; _getStructureReq = "/api/v1/datasources/*/structure?ignoreCache=true"; _editDatasourceFromActiveTab = (dsName: string) => diff --git a/app/client/src/components/formControls/RadioButtonControl.test.tsx b/app/client/src/components/formControls/RadioButtonControl.test.tsx new file mode 100644 index 00000000000..03077c379b1 --- /dev/null +++ b/app/client/src/components/formControls/RadioButtonControl.test.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import RadioButtonControl from "./RadioButtonControl"; +import { render, screen } from "test/testUtils"; +import { Provider } from "react-redux"; +import { reduxForm } from "redux-form"; +import configureStore from "redux-mock-store"; +import "@testing-library/jest-dom"; + +const mockStore = configureStore([]); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function TestForm(props: any) { + return
{props.children}
; +} + +const ReduxFormDecorator = reduxForm({ + form: "TestForm", +})(TestForm); + +const mockOptions = [ + { label: "Option 1", value: "option1", children: "Option 1" }, + { label: "Option 2", value: "option2", children: "Option 2" }, + { label: "Option 3", value: "option3", children: "Option 3" }, +]; + +let radioButtonProps = { + options: mockOptions, + configProperty: "actionConfiguration.testPath", + controlType: "PROJECTION", + label: "Columns", + id: "column", + formName: "", + isValid: true, + initialValue: "option1", +}; + +describe("RadioButtonControl", () => { + const mockStoreInstance = mockStore(); + let store: typeof mockStoreInstance; + + beforeEach(() => { + store = mockStore(); + }); + it("should render RadioButtonControl and options properly", async () => { + render( + + + + + , + ); + const radioButton = (await screen.findByTestId( + "actionConfiguration.testPath", + )) as HTMLElement; + + expect(radioButton).toBeInTheDocument(); + + const options = screen.getAllByRole("radio"); + + expect(options).toHaveLength(3); + }); + + it("should show the default selected option", async () => { + radioButtonProps = { + ...radioButtonProps, + }; + + render( + + + + + , + ); + const radioButton = (await screen.findByTestId( + "actionConfiguration.testPath", + )) as HTMLElement; + + expect(radioButton).toBeInTheDocument(); + + const options = screen.getAllByRole("radio"); + + expect(options[0]).toBeChecked(); + expect(options[1]).not.toBeChecked(); + expect(options[2]).not.toBeChecked(); + }); + + it("should select the option when clicked", async () => { + radioButtonProps = { + ...radioButtonProps, + }; + + render( + + + + + , + ); + const radioButton = (await screen.findByTestId( + "actionConfiguration.testPath", + )) as HTMLElement; + + expect(radioButton).toBeInTheDocument(); + + const options = screen.getAllByRole("radio"); + + expect(options[0]).toBeChecked(); + expect(options[1]).not.toBeChecked(); + expect(options[2]).not.toBeChecked(); + + options[1].click(); + + expect(options[0]).not.toBeChecked(); + expect(options[1]).toBeChecked(); + expect(options[2]).not.toBeChecked(); + }); +}); diff --git a/app/client/src/components/formControls/RadioButtonControl.tsx b/app/client/src/components/formControls/RadioButtonControl.tsx new file mode 100644 index 00000000000..ecee054f58c --- /dev/null +++ b/app/client/src/components/formControls/RadioButtonControl.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import type { ControlProps } from "./BaseControl"; +import BaseControl from "./BaseControl"; +import type { ControlType } from "constants/PropertyControlConstants"; +import type { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; +import { Field } from "redux-form"; +import { Radio, RadioGroup, type SelectOptionProps } from "@appsmith/ads"; +import styled from "styled-components"; + +class RadioButtonControl extends BaseControl { + getControlType(): ControlType { + return "RADIO_BUTTON"; + } + render() { + return ( + + ); + } +} + +type renderComponentProps = RadioButtonControlProps & { + input?: WrappedFieldInputProps; + meta?: WrappedFieldMetaProps; + options?: Array<{ label: string; value: string }>; +}; + +const StyledRadioGroup = styled(RadioGroup)({ + display: "flex", + flexDirection: "column", + gap: "16px", + marginTop: "16px", +}); + +function renderComponent(props: renderComponentProps) { + const onChangeHandler = (value: string) => { + if (typeof props.input?.onChange === "function") { + props.input.onChange(value); + } + }; + + const options = props.options || []; + const defaultValue = props.initialValue as string; + + return ( + + {options.map((option) => { + return ( + + {option.label} + + ); + })} + + ); +} + +export interface RadioButtonControlProps extends ControlProps { + options: SelectOptionProps[]; +} + +export default RadioButtonControl; diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index 414c920f5aa..94e34ba1c05 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -662,17 +662,6 @@ class DatasourceSaaSEditor extends JSONtoForm { > {(!viewMode || createFlow || isInsideReconnectModal) && ( <> - {/* This adds information banner when creating google sheets datasource, - this info banner explains why appsmith requires permissions from users google account */} - {datasource && isGoogleSheetPlugin && createFlow ? ( - - ) : null} {/* This adds error banner for google sheets datasource if the datasource is unauthorised */} {datasource && isGoogleSheetPlugin && @@ -688,6 +677,17 @@ class DatasourceSaaSEditor extends JSONtoForm { ? map(sections, this.renderMainSection) : null} {""} + {/* This adds information banner when creating google sheets datasource, + this info banner explains why appsmith requires permissions from users google account */} + {datasource && isGoogleSheetPlugin && createFlow ? ( + + ) : null} )} {viewMode && diff --git a/app/client/src/utils/formControl/FormControlRegistry.tsx b/app/client/src/utils/formControl/FormControlRegistry.tsx index 90fe1623630..0779695fbdd 100644 --- a/app/client/src/utils/formControl/FormControlRegistry.tsx +++ b/app/client/src/utils/formControl/FormControlRegistry.tsx @@ -35,6 +35,8 @@ import FormTemplateControl from "components/formControls/FormTemplateControl"; import type { FormTemplateControlProps } from "components/formControls/FormTemplateControl"; import MultiFilePickerControl from "components/formControls/MultiFilePickerControl"; import type { MultipleFilePickerControlProps } from "components/formControls/MultiFilePickerControl"; +import type { RadioButtonControlProps } from "components/formControls/RadioButtonControl"; +import RadioButtonControl from "components/formControls/RadioButtonControl"; /** * NOTE: If you are adding a component that uses FormControl @@ -183,6 +185,11 @@ class FormControlRegistry { }, }, ); + FormControlFactory.registerControlBuilder(formControlTypes.RADIO_BUTTON, { + buildPropertyControl(controlProps: RadioButtonControlProps): JSX.Element { + return ; + }, + }); } } diff --git a/app/client/src/utils/formControl/formControlTypes.ts b/app/client/src/utils/formControl/formControlTypes.ts index 053f0d6f855..86242a50bdf 100644 --- a/app/client/src/utils/formControl/formControlTypes.ts +++ b/app/client/src/utils/formControl/formControlTypes.ts @@ -18,4 +18,5 @@ export default { PROJECTION: "PROJECTION", FORM_TEMPLATE: "FORM_TEMPLATE", MULTIPLE_FILE_PICKER: "MULTIPLE_FILE_PICKER", + RADIO_BUTTON: "RADIO_BUTTON", }; diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json index d5f7709c310..8e9fe67783e 100644 --- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json @@ -33,7 +33,7 @@ { "label": "Permissions | Scope", "configProperty": "datasourceConfiguration.authentication.scopeString", - "controlType": "DROP_DOWN", + "controlType": "RADIO_BUTTON", "options": [ { "label": "Read / Write / Delete | Selected google sheets", From a4f9905c45c3c09ad687f0eb26d34e0d5992b5d7 Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Wed, 9 Oct 2024 10:09:13 +0530 Subject: [PATCH 03/15] fix: batched evaluate action params to a chunk size (#36482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description In this PR, we fix the payload evaluation issue by adding a batching mechanism. Root cause the issue here was that using yield call in loop tends to call stack overflow which is a known open issue in the redux-saga library. The popular workaround to this is to add `yield delay(0` after n number of batch. Fixes #36383 ## Automation /test all ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: a3a583f880cb9d057a2f4668f1ccb0fc10714321 > Cypress dashboard. > Tags: `@tag.All` > Spec: >
Wed, 02 Oct 2024 01:49:36 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **Improvements** - Enhanced processing efficiency by introducing a batching mechanism, allowing smoother handling of large data sets without blocking the event loop. Co-authored-by: rishabhrathod01 --- .../src/sagas/ActionExecution/PluginActionSaga.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index 5e2d426dc2b..0c4d09c8e8d 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -1,6 +1,7 @@ import { all, call, + delay, put, select, take, @@ -440,7 +441,11 @@ function* evaluateActionParams( const arrDatatype: Array = []; // array of objects containing blob urls that is loops and individual object is checked for resolution of blob urls. - for (const val of value) { + + const BATCH_CHUNK_SIZE = 100; + + for (let j = 0; j < value.length; j++) { + const val = value[j]; // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any const newVal: Record = yield call( @@ -473,6 +478,11 @@ function* evaluateActionParams( filePickerInstrumentation["fileSizes"].push(size); filePickerInstrumentation["fileTypes"].push(type); } + + if ((j + 1) % BATCH_CHUNK_SIZE === 0) { + // Yield control back to the event loop and empty the stack trace + yield delay(0); + } } //Adding array datatype along with the datatype of first element of the array From 85ba0b8193347b50441c53afa761f6037c80dc39 Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Wed, 9 Oct 2024 12:24:24 +0530 Subject: [PATCH 04/15] chore: update code owner for migrations (#36769) ## Description Add Anagh as code. owner for the migration module to keep track of mongo migrations ## Automation /ok-to-test tags="" ### :mag: Cypress test results > [!CAUTION] > If you modify the content in this section, you are likely to disrupt the CI result for your PR. ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **Chores** - Updated ownership assignments for specific project paths, adding new owners without removing existing ones. --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index f1ae93a1aae..6f6f143cc2f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -190,7 +190,7 @@ app/client/src/ce/JSFunctionExecutionSaga.ts @ApekshaBhosale app/client/src/ee/JSFunctionExecutionSaga.ts @ApekshaBhosale # Enterprise Success -app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/**/* @sharat87 @abhvsn +app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/**/* @sharat87 @abhvsn @AnaghHegde # DevOps & Shri deploy/**/* @sharat87 @pratapaprasanna From da8f726eb80363ad87c2adbc52fc1244401f10d3 Mon Sep 17 00:00:00 2001 From: sneha122 Date: Wed, 9 Oct 2024 13:22:27 +0530 Subject: [PATCH 05/15] fix: New query button does not show up issue fixed (#36766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR fixes two issues: 1. When importing older application jsons with applicationVersion 1, they get redirected to `/applications` (older url) as opposed to `/app` (newer slug based url). This results in new query button not showing up on datasource view page, because new query button is hidden behind a condition of editorType and editorType was depending on the new url path. This PR fixes the issue by adding support for older path as well to provide backwards compatibility. 2. Second similar issue was seen intermittently, when we do save and authorise in REST API datasource, then sometimes the redirection was happening to older url (/applications), in that case new query button would not show up. This has been fixed too Fixes #35626 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Sanity" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 015ef2a11ff65ec213dd36593b624ba268494325 > Cypress dashboard. > Tags: `@tag.Sanity` > Spec: >
Tue, 08 Oct 2024 18:19:47 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No Co-authored-by: “sneha122” <“sneha@appsmith.com”> --- app/client/src/ce/hooks/hooks.test.ts | 25 +++++++++++++++++++++++++ app/client/src/ce/hooks/index.ts | 1 + 2 files changed, 26 insertions(+) create mode 100644 app/client/src/ce/hooks/hooks.test.ts diff --git a/app/client/src/ce/hooks/hooks.test.ts b/app/client/src/ce/hooks/hooks.test.ts new file mode 100644 index 00000000000..8259537e9dd --- /dev/null +++ b/app/client/src/ce/hooks/hooks.test.ts @@ -0,0 +1,25 @@ +import { useEditorType, EditorNames } from "./index"; +import { + BUILDER_VIEWER_PATH_PREFIX, + BUILDER_BASE_PATH_DEPRECATED, +} from "constants/routes"; + +describe("useEditorType", () => { + it('should return "app" for BUILDER_VIEWER_PATH_PREFIX', () => { + const result = useEditorType(BUILDER_VIEWER_PATH_PREFIX); + + expect(result).toBe(EditorNames.APPLICATION); + }); + + it('should return "app" for BUILDER_BASE_PATH_DEPRECATED', () => { + const result = useEditorType(BUILDER_BASE_PATH_DEPRECATED); + + expect(result).toBe(EditorNames.APPLICATION); + }); + + it('should default to "app" for unmatched paths', () => { + const result = useEditorType("/some-random-path"); + + expect(result).toBe(EditorNames.APPLICATION); + }); +}); diff --git a/app/client/src/ce/hooks/index.ts b/app/client/src/ce/hooks/index.ts index 2112914bd32..cd84f5ca1df 100644 --- a/app/client/src/ce/hooks/index.ts +++ b/app/client/src/ce/hooks/index.ts @@ -15,6 +15,7 @@ export interface EditorType { export const editorType: EditorType = { [BUILDER_VIEWER_PATH_PREFIX]: EditorNames.APPLICATION, + [BUILDER_BASE_PATH_DEPRECATED]: EditorNames.APPLICATION, }; export const useEditorType = (path: string) => { From d233d7e9c31da49d8fddc17886ac060a6cde1c7b Mon Sep 17 00:00:00 2001 From: Jacques Ikot Date: Wed, 9 Oct 2024 09:09:12 +0100 Subject: [PATCH 06/15] feat: Allow filtering of table select column label (#36755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description **Problem** When filtering a table with a select column type, users expect to filter by the visible label values shown in each cell. Currently, however, filtering is applied to the underlying option values rather than the displayed labels, leading to unexpected filter results for end-users. **Root Cause** In a previous update ([PR #35124](https://github.com/appsmithorg/appsmith/pull/35124)), the table cell display for select columns was changed to show labels instead of values. However, the filtering logic was not updated accordingly, so the table still filtered on the original option values, creating a mismatch between displayed and filtered content. **Solution** This PR modifies the displayedRow property within the table widget to use the label property instead of the value key when filtering or searching select column data. This ensures that table filtering and searching now align with the visible label values in the select columns, providing a more intuitive user experience. Fixes #36635 ## Automation /ok-to-test tags="@tag.Sanity, @tag.Table, @tag.Select, @tag.Binding" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: fd6c179ffb2c61d23cb98fd749c8df49cebcfcdd > Cypress dashboard. > Tags: `@tag.Sanity, @tag.Table, @tag.Select, @tag.Binding` > Spec: >
Tue, 08 Oct 2024 12:48:01 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a new test case to verify filtering functionality for the "role" column in the Table Widget. - Enhanced filtering mechanism to support multiple label values for select columns. - **Bug Fixes** - Removed outdated search functionality to streamline user experience. - **Refactor** - Restructured existing test cases for improved clarity and flow. --- .../Widgets/TableV2/TableV2Filter1_1_Spec.ts | 14 +++++++++----- .../src/widgets/TableWidgetV2/widget/derived.js | 16 +++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2Filter1_1_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2Filter1_1_Spec.ts index 72c7b293966..f1f3c4884c7 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2Filter1_1_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2Filter1_1_Spec.ts @@ -175,13 +175,17 @@ describe( expect(afterSearch).to.eq("Software Engineer"); }); table.RemoveSearchTextNVerify("1", "v2"); + }); - // Search for a value in the table - table.SearchTable("20"); - table.ReadTableRowColumnData(0, 2, "v2").then((afterSearch) => { - expect(afterSearch).to.eq("Product Manager"); + it("12. Verify table filter for select column type", function () { + featureFlagIntercept({ release_table_cell_label_value_enabled: true }); + table.OpenNFilterTable("role", "is exactly", "Product Manager"); + table.ReadTableRowColumnData(0, 2, "v2").then(($cellData) => { + expect($cellData).to.eq("Product Manager"); + }); + table.ReadTableRowColumnData(1, 2, "v2").then(($cellData) => { + expect($cellData).to.eq("Product Manager"); }); - table.RemoveSearchTextNVerify("1", "v2"); }); }, ); diff --git a/app/client/src/widgets/TableWidgetV2/widget/derived.js b/app/client/src/widgets/TableWidgetV2/widget/derived.js index d27e7e84c59..a5ed2af045b 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/derived.js +++ b/app/client/src/widgets/TableWidgetV2/widget/derived.js @@ -682,9 +682,9 @@ export default { /* * For select columns with label and values, we need to include the label value - * in the search + * in the search and filter data */ - let labelValueForSelectCell = ""; + let labelValuesForSelectCell = {}; /* * Initialize an array to store keys for columns that have the 'select' column type * and contain selectOptions. @@ -716,13 +716,15 @@ export default { primaryColumns[key].selectOptions, ); - let selectOptions; + let selectOptions = {}; /* * If selectOptions is an array, check if it contains nested arrays. * This is to handle situations where selectOptons is a javascript object and computes as a nested array. */ if (isSelectOptionsAnArray) { + const selectOptionKey = primaryColumns[key].alias; + if (_.some(primaryColumns[key].selectOptions, _.isArray)) { /* Handle the case where selectOptions contains nested arrays - selectOptions is javascript */ selectOptions = @@ -732,7 +734,7 @@ export default { }); if (option) { - labelValueForSelectCell = option.label; + labelValuesForSelectCell[selectOptionKey] = option.label; } } else { /* Handle the case where selectOptions is a flat array - selectOptions is plain JSON */ @@ -742,7 +744,7 @@ export default { ); if (option) { - labelValueForSelectCell = option.label; + labelValuesForSelectCell[selectOptionKey] = option.label; } } } else { @@ -753,7 +755,7 @@ export default { ); if (option) { - labelValueForSelectCell = option.label; + labelValuesForSelectCell[selectOptionKey] = option.label; } } }); @@ -761,7 +763,7 @@ export default { const displayedRow = { ...row, - labelValueForSelectCell, + ...labelValuesForSelectCell, ...columnWithDisplayText.reduce((acc, column) => { let displayText; From 1c91a8ad63f14c139b2aa7a2c995856f20c0b184 Mon Sep 17 00:00:00 2001 From: albinAppsmith <87797149+albinAppsmith@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:45:27 +0530 Subject: [PATCH 07/15] fix: Airgap: search bar UI was not matching with release (#36772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Global search UI was updated as part of side-by-side layout changes. But since the side-by-side is not yet enabled in Airgap, the global search ui was looking different in release and airgap. This PR address this issue by removing fdeature flag depency for global search UI change. Fixes #36701 ## Automation /ok-to-test tags="@tag.Sanity, @tag.airgap" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 79fc34f90dd52a136136b7476e7193d6b7cf02f3 > Cypress dashboard. > Tags: `@tag.Sanity, @tag.airgap` > Spec: >
Wed, 09 Oct 2024 08:06:43 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **New Features** - Simplified `HelpBar` component for a more consistent user experience. - **Bug Fixes** - Removed unnecessary conditional rendering, ensuring the `Icon` and `Text` elements are always displayed. --- .../editorComponents/GlobalSearch/HelpBar.tsx | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/app/client/src/components/editorComponents/GlobalSearch/HelpBar.tsx b/app/client/src/components/editorComponents/GlobalSearch/HelpBar.tsx index e05c5bdb78d..c52eb62c9ef 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/HelpBar.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/HelpBar.tsx @@ -1,18 +1,16 @@ import React from "react"; import styled from "styled-components"; -import { connect, useSelector } from "react-redux"; +import { connect } from "react-redux"; import { getTypographyByKey, Text, TextType } from "@appsmith/ads-old"; import { Icon } from "@appsmith/ads"; import { setGlobalSearchCategory } from "actions/globalSearchActions"; -import { HELPBAR_PLACEHOLDER } from "ee/constants/messages"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { modText } from "utils/helpers"; import { filterCategories, SEARCH_CATEGORY_ID } from "./utils"; import { protectedModeSelector } from "selectors/gitSyncSelectors"; import type { AppState } from "ee/reducers"; -import { getIsSideBySideEnabled } from "selectors/ideSelectors"; -const StyledHelpBar = styled.button<{ isSideBySideFlagEnabled?: boolean }>` +const StyledHelpBar = styled.button` padding: 0 var(--ads-v2-spaces-3); margin: var(--ads-v2-spaces-2); .placeholder-text { @@ -30,6 +28,10 @@ const StyledHelpBar = styled.button<{ isSideBySideFlagEnabled?: boolean }>` font-family: var(--ads-v2-font-family); font-size: var(--ads-v2-font-size-4); color: var(--ads-v2-color-fg); + flex-grow: 0; + gap: 8px; + min-width: fit-content; + &:hover { border: 1px solid var(--ads-v2-color-border-emphasis-plus); } @@ -38,13 +40,6 @@ const StyledHelpBar = styled.button<{ isSideBySideFlagEnabled?: boolean }>` &[disabled] { cursor: not-allowed; } - ${({ isSideBySideFlagEnabled }) => - isSideBySideFlagEnabled && - ` - flex-grow: 0; - gap: 8px; - min-width: fit-content; - `} `; interface Props { @@ -53,20 +48,14 @@ interface Props { } function HelpBar({ isProtectedMode, toggleShowModal }: Props) { - const isSideBySideFlagEnabled = useSelector(getIsSideBySideEnabled); - return ( - {!isSideBySideFlagEnabled && ( - {HELPBAR_PLACEHOLDER()} - )} - {isSideBySideFlagEnabled && } + {modText()} K From 7f31c9e269899cb946967fbad6053be9a06344b7 Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Wed, 9 Oct 2024 15:33:46 +0530 Subject: [PATCH 08/15] fix: Focus retention on inputs (#36770) ## Description Fix focus retention tests Fixes #36673 ## Summary by CodeRabbit - **Bug Fixes** - Improved focus retention during entity selection in the editor. - Streamlined interaction with the left pane for selecting entities. - **Tests** - Updated test suite for focus retention, enhancing clarity and accuracy of test cases. - Removed redundant test case for tab persistence, renumbered remaining tests for better organization. --------- --- ...pec.js => Focus_retentions_inputs_spec.js} | 107 ++++++++---------- 1 file changed, 46 insertions(+), 61 deletions(-) rename app/client/cypress/e2e/Regression/ClientSide/IDE/{MaintainContext&Focus_spec.js => Focus_retentions_inputs_spec.js} (63%) diff --git a/app/client/cypress/e2e/Regression/ClientSide/IDE/MaintainContext&Focus_spec.js b/app/client/cypress/e2e/Regression/ClientSide/IDE/Focus_retentions_inputs_spec.js similarity index 63% rename from app/client/cypress/e2e/Regression/ClientSide/IDE/MaintainContext&Focus_spec.js rename to app/client/cypress/e2e/Regression/ClientSide/IDE/Focus_retentions_inputs_spec.js index a6d5681ba0c..85b373a4cdd 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/IDE/MaintainContext&Focus_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/IDE/Focus_retentions_inputs_spec.js @@ -1,22 +1,23 @@ import reconnectDatasourceModal from "../../../../locators/ReconnectLocators"; import { agHelper, - apiPage, dataSources, homePage, locators, } from "../../../../support/Objects/ObjectsCore"; import EditorNavigation, { + PageLeftPane, EntityType, + PagePaneSegment, } from "../../../../support/Pages/EditorNavigation"; const apiwidget = require("../../../../locators/apiWidgetslocator.json"); const queryLocators = require("../../../../locators/QueryEditor.json"); -describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { +describe("Focus Retention of Inputs", { tags: ["@tag.IDE"] }, function () { before("Import the test application", () => { - homePage.CreateNewWorkspace("MaintainContext&Focus", true); - homePage.ImportApp("ContextSwitching.json", "MaintainContext"); + homePage.NavigateToHome(); + homePage.ImportApp("ContextSwitching.json"); cy.wait("@importNewApplication").then((interception) => { agHelper.Sleep(); const { isPartialImport } = interception.response.body.data; @@ -46,26 +47,26 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { cy.focusCodeInput(".t--graphql-query-editor", { ch: 4, line: 1 }); - EditorNavigation.SelectEntityByName("Rest_Api_1", EntityType.Api); + PageLeftPane.selectItem("Rest_Api_1"); cy.wait(1000); cy.xpath("//span[contains(text(), 'Params')]").click(); cy.focusCodeInput(apiwidget.queryKey); cy.wait("@saveAction"); - EditorNavigation.SelectEntityByName("Rest_Api_2", EntityType.Api); + PageLeftPane.selectItem("Rest_Api_2"); cy.wait(1000); - cy.xpath("//span[contains(text(), 'Headers')]").click(); + agHelper.GetNClick("//span[contains(text(), 'Headers')]", 0); cy.updateCodeInput(apiwidget.headerValue, "test"); cy.wait("@saveAction"); - EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query); + PageLeftPane.selectItem("SQL_Query"); cy.wait(1000); cy.focusCodeInput(".t--actionConfiguration\\.body", { ch: 5, line: 0 }); cy.wait("@saveAction"); - EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query); + PageLeftPane.selectItem("S3_Query"); cy.wait(1000); cy.focusCodeInput(".t--actionConfiguration\\.formData\\.bucket\\.data", { @@ -75,25 +76,18 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { cy.wait(1000); cy.wait("@saveAction"); - EditorNavigation.SelectEntityByName("JSObject1", EntityType.JSObject); + PageLeftPane.switchSegment(PagePaneSegment.JS); + + PageLeftPane.selectItem("JSObject1"); cy.wait(1000); cy.focusCodeInput(".js-editor", { ch: 4, line: 4 }); cy.wait("@saveAction"); - EditorNavigation.SelectEntityByName("JSObject2", EntityType.JSObject); + PageLeftPane.selectItem("JSObject2"); cy.wait(1000); cy.focusCodeInput(".js-editor", { ch: 2, line: 2 }); - - EditorNavigation.SelectEntityByName("Mongo_Query", EntityType.Query); - - cy.wait(1000); - dataSources.EnterJSContext({ - fieldLabel: "Collection", - fieldValue: "TestCollection", - }); - cy.wait("@saveAction"); }); it("2. Maintains focus on property/Api/Query/Js Pane", () => { @@ -106,8 +100,14 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { line: 0, }); + PageLeftPane.switchSegment(PagePaneSegment.Queries); + //Maintains focus on the API pane - EditorNavigation.SelectEntityByName("Graphql_Query", EntityType.Api); + PageLeftPane.selectItem("Graphql_Query"); + + agHelper + .GetElement(locators._queryName) + .should("have.text", "Graphql_Query"); cy.xpath("//span[contains(text(), 'Body')]/parent::button").should( "have.attr", @@ -116,10 +116,20 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { ); cy.assertCursorOnCodeInput(".t--graphql-query-editor", { ch: 4, line: 1 }); - EditorNavigation.SelectEntityByName("Rest_Api_1", EntityType.Api); - // cy.assertCursorOnCodeInput(apiwidget.headerValue); + PageLeftPane.selectItem("Rest_Api_1"); + + agHelper.GetElement(locators._queryName).should("have.text", "Rest_Api_1"); - EditorNavigation.SelectEntityByName("Rest_Api_2", EntityType.Api); + cy.xpath("//span[contains(text(), 'Params')]/parent::button").should( + "have.attr", + "aria-selected", + "true", + ); + cy.assertCursorOnCodeInput(apiwidget.queryKey, { ch: 0, line: 0 }); + + PageLeftPane.selectItem("Rest_Api_2"); + + agHelper.GetElement(locators._queryName).should("have.text", "Rest_Api_2"); cy.xpath("//span[contains(text(), 'Headers')]/parent::button").should( "have.attr", @@ -129,61 +139,35 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { cy.assertCursorOnCodeInput(apiwidget.headerValue); //Maintains focus on Query panes - EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query); + PageLeftPane.selectItem("SQL_Query"); + + agHelper.GetElement(locators._queryName).should("have.text", "SQL_Query"); cy.assertCursorOnCodeInput(".t--actionConfiguration\\.body", { ch: 5, line: 0, }); - EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query); + PageLeftPane.selectItem("S3_Query"); cy.assertCursorOnCodeInput( ".t--actionConfiguration\\.formData\\.bucket\\.data", { ch: 2, line: 0 }, ); - // Removing as the Mongo collection is now converted to dropdown - // entityExplorer.SelectEntityByName("Mongo_Query"); - - // cy.assertCursorOnCodeInput( - // ".t--actionConfiguration\\.formData\\.collection\\.data", - // ); + PageLeftPane.switchSegment(PagePaneSegment.JS); //Maintains focus on JS Objects - EditorNavigation.SelectEntityByName("JSObject1", EntityType.JSObject); + PageLeftPane.selectItem("JSObject1"); cy.assertCursorOnCodeInput(".js-editor", { ch: 2, line: 4 }); - EditorNavigation.SelectEntityByName("JSObject2", EntityType.JSObject); + PageLeftPane.selectItem("JSObject2"); cy.assertCursorOnCodeInput(".js-editor", { ch: 2, line: 2 }); }); - it("3. Check if selected tab on right tab persists", () => { - EditorNavigation.SelectEntityByName("Rest_Api_1", EntityType.Api); - apiPage.SelectRightPaneTab("Connections"); - EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query); - EditorNavigation.SelectEntityByName("Rest_Api_1", EntityType.Api); - apiPage.AssertRightPaneSelectedTab("Connections"); - - //Check if the URL is persisted while switching pages - cy.Createpage("Page2"); - - EditorNavigation.SelectEntityByName("Page1", EntityType.Page); - EditorNavigation.SelectEntityByName("Rest_Api_1", EntityType.Api); - - EditorNavigation.SelectEntityByName("Page2", EntityType.Page); - cy.dragAndDropToCanvas("textwidget", { x: 300, y: 200 }); - - EditorNavigation.SelectEntityByName("Page1", EntityType.Page); - cy.get(".t--nameOfApi .bp3-editable-text-content").should( - "contain", - "Rest_Api_1", - ); - }); - - it("4. Datasource edit mode has to be maintained", () => { + it("3. Datasource edit mode has to be maintained", () => { EditorNavigation.SelectEntityByName("Appsmith", EntityType.Datasource); dataSources.EditDatasource(); EditorNavigation.SelectEntityByName("Github", EntityType.Datasource); @@ -192,7 +176,7 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { dataSources.AssertDSEditViewMode("Edit"); }); - it("5. Maintain focus of form control inputs", () => { + it("4. Maintain focus of form control inputs", () => { EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query); dataSources.ToggleUsePreparedStatement(false); EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query); @@ -202,14 +186,15 @@ describe("MaintainContext&Focus", { tags: ["@tag.IDE"] }, function () { EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query); cy.get(".bp3-editable-text-content").should("contain.text", "SQL_Query"); - cy.get(".t--form-control-SWITCH input").should("be.focused"); + cy.xpath(queryLocators.querySettingsTab).click(); + agHelper.GetElement(dataSources._usePreparedStatement).should("be.focused"); EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query); agHelper.Sleep(); cy.xpath(queryLocators.querySettingsTab).click(); cy.xpath(queryLocators.queryTimeout).should("be.focused"); }); - it("6. Bug 21999 Maintain focus of code editor when Escape is pressed with autcomplete open + Bug 22960", () => { + it("5. Bug 21999 Maintain focus of code editor when Escape is pressed with autcomplete open + Bug 22960", () => { EditorNavigation.SelectEntityByName("JSObject1", EntityType.JSObject); cy.assertCursorOnCodeInput(".js-editor", { ch: 2, line: 4 }); From 6e59db227d8721aa0dd0a4dbee6316a1cfaf33dd Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 9 Oct 2024 16:22:44 +0530 Subject: [PATCH 09/15] chore: refactor inputs (#36680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit - **New Features** - Introduced a new `ChatInput` component for enhanced user input in chat interfaces, featuring structured input fields and dynamic height adjustments. - Added a customizable `Input` component for both single-line and multi-line text entry, with support for password visibility and loading states. - Launched a `TextAreaInput` component that integrates loading states and optional prefix/suffix elements for a versatile text area experience. - **Improvements** - Enhanced input components with new interfaces for better customization options and shared properties. - **Bug Fixes** - Resolved issues related to input sizing and alignment, ensuring a smoother user experience. - **Documentation** - Expanded documentation to include details on the new input components and their usage examples. > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: dc87d2de1213d23fdda1f52cee5b346d68627263 > Cypress dashboard. > Tags: `@tag.Anvil` > Spec: >
Wed, 09 Oct 2024 10:49:28 UTC --- .../src/components/Field/src/Field.tsx | 123 ---------- .../src/components/Field/src/HelpText.tsx | 48 ---- .../src/components/Field/src/Label.tsx | 76 ------ .../src/components/Field/src/index.ts | 3 - .../src/components/TextArea/src/TextArea.tsx | 120 ---------- .../src/components/TextArea/src/index.ts | 3 - .../src/components/TextArea/src/types.ts | 19 -- .../components/TextInput/src/TextInput.tsx | 55 ----- .../src/components/TextInput/src/index.ts | 3 - .../src/components/TextInput/src/types.ts | 28 --- .../TextInput/stories/TextInput.stories.ts | 19 -- .../TextInputBase/src/TextInputBase.tsx | 105 --------- .../src/components/TextInputBase/src/index.ts | 2 - .../src/components/TextInputBase/src/types.ts | 25 -- .../design-system/headless/src/index.ts | 6 +- .../src/components/Button/src/Button.tsx | 9 +- .../src/components/ChatInput}/index.ts | 0 .../components/ChatInput/src/ChatInput.tsx | 164 +++++++++++++ .../src/components/ChatInput/src/index.ts | 1 + .../src/components/ChatInput/src/types.ts | 5 + .../ChatInput/stories/ChatInput.stories.tsx | 68 ++++++ .../src/components/Checkbox/src/Checkbox.tsx | 5 +- .../src/components/ComboBox/src/ComboBox.tsx | 68 +++--- .../ComboBox/src/ComboBoxTrigger.tsx | 38 +++ .../components/ComboBox/src/styles.module.css | 84 ------- .../src/components/ComboBox/src/types.ts | 30 +-- .../ComboBox/stories/ComboBox.stories.tsx | 108 ++++----- .../src/components/ComboBox/stories/items.ts | 6 +- .../ContextualHelp/src/ContextualHelp.tsx | 6 +- .../src/components/Field}/index.ts | 0 .../widgets/src/components/Field/src/index.ts | 2 + .../components/Field/src/styles.module.css | 5 + .../widgets/src/components/Field/src/types.ts | 10 + .../FieldDescription/src/FieldDescription.tsx | 16 -- .../components/FieldDescription/src/index.ts | 2 - .../FieldDescription/src/styles.module.css | 4 - .../components/FieldDescription/src/types.ts | 6 - .../components/FieldError/src/FieldError.tsx | 20 +- .../FieldError/src/styles.module.css | 3 +- .../src/components/FieldError/src/types.ts | 2 +- .../components/FieldLabel/src/FieldLabel.tsx | 43 ++-- .../FieldLabel/src/styles.module.css | 20 +- .../src/components/FieldLabel/src/types.ts | 9 +- .../FieldListPopover/src/FieldListPopover.tsx | 36 --- .../components/FieldListPopover/src/index.ts | 2 - .../FieldListPopover/src/styles.module.css | 12 - .../components/FieldListPopover/src/types.ts | 13 - .../src/components/Input}/index.ts | 0 .../src/components/Input/src/Input.tsx | 65 +++++ .../components/Input/src/TextAreaInput.tsx | 51 ++++ .../widgets/src/components/Input/src/index.ts | 3 + .../components/Input/src/styles.module.css | 223 ++++++++++++++++++ .../widgets/src/components/Input/src/types.ts | 24 ++ .../widgets/src/components/Link/src/types.ts | 13 +- .../src/components/ListBox}/index.ts | 0 .../src/components/ListBox/src/ListBox.tsx | 15 ++ .../src/components/ListBox/src/index.ts | 2 + .../components/ListBox/src/styles.module.css | 5 + .../src/components/ListBox/src/types.ts | 3 + .../index.ts | 0 .../ListBoxItem/src/ListBoxItem.tsx | 17 ++ .../src/components/ListBoxItem/src/index.ts | 2 + .../ListBoxItem/src/styles.module.css | 83 +++++++ .../src/components/ListBoxItem/src/types.ts | 6 + .../widgets/src/components/Menu/src/Menu.tsx | 79 ++----- .../widgets/src/components/Menu/src/index.ts | 2 +- .../widgets/src/components/Menu/src/types.ts | 30 +-- .../components/Menu/stories/Menu.stories.tsx | 68 ++++-- .../src/components/Menu/stories/menuData.ts | 8 +- .../{FieldListPopover => MenuItem}/index.ts | 0 .../src/components/MenuItem/src/MenuItem.tsx | 26 ++ .../src/components/MenuItem/src/index.ts | 1 + .../src/components/MenuItem/src/types.ts | 7 + .../src/components/Popover/src/Popover.tsx | 5 +- .../src/{styles => components/Radio}/index.ts | 0 .../src/components/Radio/src/Radio.tsx | 24 ++ .../widgets/src/components/Radio/src/index.ts | 2 + .../src/styles.module.css | 18 -- .../widgets/src/components/Radio/src/types.ts | 6 + .../RadioGroup.chromatic.stories.tsx | 18 +- .../components/RadioGroup/src/RadioGroup.tsx | 54 ++--- .../src/components/RadioGroup/src/types.ts | 31 +-- .../RadioGroup/stories/RadioGroup.stories.tsx | 89 ++++--- .../RadioGroup/tests/RadioGroup.test.tsx | 52 ++-- .../src/components/Select/src/Select.tsx | 68 +++--- .../components/Select/src/SelectTrigger.tsx | 41 ++++ .../components/Select/src/styles.module.css | 61 ----- .../src/components/Select/src/types.ts | 22 +- .../Select/stories/Select.stories.tsx | 53 +++-- .../components/Select/stories/selectData.ts | 8 +- .../src/components/Switch/src/Switch.tsx | 5 +- .../src/components/TagGroup/src/TagGroup.tsx | 20 +- .../TagGroup/stories/TagGroup.stories.tsx | 9 - .../widgets/src/components/Text/src/types.ts | 2 +- .../src/components/TextArea/src/TextArea.tsx | 137 +++++++---- .../src/components/TextArea/src/index.ts | 2 +- .../components/TextArea/src/styles.module.css | 12 - .../src/components/TextArea/src/types.ts | 11 + .../TextArea/stories/TextArea.stories.tsx | 73 +++--- .../components/TextInput/src/TextInput.tsx | 115 +++------ .../src/components/TextInput/src/index.ts | 4 +- .../src/components/TextInput/src/types.ts | 10 + .../TextInput/stories/TextInput.stories.tsx | 166 +++++-------- .../chromatic/Group.chromatic.stories.tsx | 31 ++- .../ToggleGroup/src/ToggleGroup.tsx | 44 ++-- .../src/components/ToggleGroup/src/index.ts | 1 + .../ToggleGroup/src/styles.module.css | 21 +- .../src/components/ToggleGroup/src/types.ts | 33 +-- .../stories/ToggleGroup.stories.tsx | 156 +++++------- .../ToggleGroup/tests/ToggleGroup.test.tsx | 49 ++-- .../ToolbarButtons/src/ToolbarButtons.tsx | 12 +- .../chromatic/Tooltip.chromatic.stories.tsx | 35 +-- .../src/components/Tooltip/src/Tooltip.tsx | 12 +- .../components/Tooltip/src/TooltipContent.tsx | 5 +- .../src/components/Tooltip/src/index.ts | 4 +- .../Tooltip/stories/Tooltip.stories.tsx | 49 ++-- .../design-system/widgets/src/index.ts | 14 +- .../widgets/src/styles/src/field.module.css | 114 --------- .../widgets/src/styles/src/index.ts | 4 - .../src/styles/src/inline-label.module.css | 25 -- .../src/styles/src/list-item.module.css | 62 ----- .../src/styles/src/text-input.module.css | 178 -------------- .../widgets/src/testing/ComplexForm.tsx | 76 +++--- .../wds/WDSBaseInputWidget/component/types.ts | 3 +- .../WDSCheckboxGroupWidget/widget/index.tsx | 14 +- .../ui/wds/WDSComboBoxWidget/widget/index.tsx | 16 +- .../component/index.tsx | 2 +- .../ui/wds/WDSInputWidget/component/index.tsx | 4 +- .../WDSPhoneInputWidget/component/index.tsx | 2 +- .../wds/WDSRadioGroupWidget/widget/index.tsx | 11 +- .../wds/WDSRadioGroupWidget/widget/types.ts | 5 +- .../ui/wds/WDSSelectWidget/widget/index.tsx | 16 +- .../wds/WDSSwitchGroupWidget/widget/index.tsx | 13 +- .../component/cellComponents/HeaderCell.tsx | 23 +- 134 files changed, 1929 insertions(+), 2388 deletions(-) delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/Field.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/Label.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/TextArea/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextArea/src/types.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/src/types.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts rename app/client/packages/design-system/{headless/src/components/Field => widgets/src/components/ChatInput}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css rename app/client/packages/design-system/{headless/src/components/TextArea => widgets/src/components/Field}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Field/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Field/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/Field/src/types.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/FieldDescription.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts rename app/client/packages/design-system/{headless/src/components/TextInput => widgets/src/components/Input}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/types.ts rename app/client/packages/design-system/{headless/src/components/TextInputBase => widgets/src/components/ListBox}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts rename app/client/packages/design-system/widgets/src/components/{FieldDescription => ListBoxItem}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts rename app/client/packages/design-system/widgets/src/components/{FieldListPopover => MenuItem}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts rename app/client/packages/design-system/widgets/src/{styles => components/Radio}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Radio/src/index.ts rename app/client/packages/design-system/widgets/src/components/{RadioGroup => Radio}/src/styles.module.css (92%) create mode 100644 app/client/packages/design-system/widgets/src/components/Radio/src/types.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css delete mode 100644 app/client/packages/design-system/widgets/src/components/TextArea/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/TextArea/src/types.ts create mode 100644 app/client/packages/design-system/widgets/src/components/TextInput/src/types.ts delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/field.module.css delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/index.ts delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/inline-label.module.css delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/list-item.module.css delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/text-input.module.css diff --git a/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx b/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx deleted file mode 100644 index 0671befc430..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import type { ReactNode, Ref } from "react"; -import React, { forwardRef } from "react"; -import type { SpectrumFieldProps } from "@react-types/label"; - -import { Label } from "./Label"; -import { HelpText } from "./HelpText"; -export type FieldProps = Pick< - SpectrumFieldProps, - | "contextualHelp" - | "description" - | "descriptionProps" - | "elementType" - | "errorMessage" - | "errorMessageProps" - | "includeNecessityIndicatorInAccessibilityName" - | "isDisabled" - | "isRequired" - | "label" - | "labelProps" - | "necessityIndicator" - | "wrapperClassName" - | "wrapperProps" -> & { - fieldType?: "field" | "field-group"; - labelClassName?: string; - helpTextClassName?: string; - validationState?: ValidationState; - children: ReactNode; - isReadOnly?: boolean; -}; - -import type { ValidationState } from "@react-types/shared"; - -export type FieldRef = Ref; - -const _Field = (props: FieldProps, ref: FieldRef) => { - const { - children, - contextualHelp, - description, - descriptionProps, - elementType, - errorMessage, - errorMessageProps = {}, - fieldType = "field", - helpTextClassName, - includeNecessityIndicatorInAccessibilityName, - isDisabled = false, - isReadOnly = false, - isRequired, - label, - labelClassName, - labelProps, - necessityIndicator, - validationState, - wrapperClassName, - wrapperProps = {}, - } = props; - - // Readonly has a higher priority than disabled. - const getDisabledState = () => Boolean(isDisabled) && !Boolean(isReadOnly); - - const hasHelpText = - Boolean(description) || - (Boolean(errorMessage) && validationState === "invalid"); - - const renderHelpText = () => { - return ( - - ); - }; - - const labelAndContextualHelp = (Boolean(label) || - Boolean(contextualHelp)) && ( -
- {Boolean(label) && ( - - )} - {contextualHelp} -
- ); - - return ( -
- {labelAndContextualHelp} -
- {children} - {hasHelpText && renderHelpText()} -
-
- ); -}; - -export const Field = forwardRef(_Field); diff --git a/app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx b/app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx deleted file mode 100644 index 9ad27701202..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { forwardRef } from "react"; -import type { HTMLAttributes } from "react"; -import { useDOMRef } from "@react-spectrum/utils"; -import type { - DOMRef, - SpectrumHelpTextProps, - ValidationState, -} from "@react-types/shared"; - -interface HelpTextProps extends Omit { - /** Props for the help text description element. */ - descriptionProps?: HTMLAttributes; - /** Props for the help text error message element. */ - errorMessageProps?: HTMLAttributes; - /** classname */ - className?: string; - /** validation state for help text */ - validationState?: ValidationState; -} - -function _HelpText(props: HelpTextProps, ref: DOMRef) { - const { - className, - description, - descriptionProps, - errorMessage, - errorMessageProps, - validationState, - } = props; - const domRef = useDOMRef(ref); - const isErrorMessage = Boolean(errorMessage) && validationState === "invalid"; - - return ( -
- {isErrorMessage ? ( -
- {errorMessage} -
- ) : ( -
- {description} -
- )} -
- ); -} - -export const HelpText = forwardRef(_HelpText); diff --git a/app/client/packages/design-system/headless/src/components/Field/src/Label.tsx b/app/client/packages/design-system/headless/src/components/Field/src/Label.tsx deleted file mode 100644 index 522763a2c1d..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/Label.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { forwardRef } from "react"; -import { useDOMRef } from "@react-spectrum/utils"; -import { filterDOMProps } from "@react-aria/utils"; -import type { DOMRef, StyleProps } from "@react-types/shared"; -import type { SpectrumLabelProps } from "@react-types/label"; - -export type LabelProps = Omit< - SpectrumLabelProps, - keyof StyleProps | "labelPosition" | "labelAlign" ->; - -const _Label = (props: LabelProps, ref: DOMRef) => { - const { - children, - className, - elementType: ElementType = "label", - for: labelFor, - htmlFor, - includeNecessityIndicatorInAccessibilityName, - isRequired, - necessityIndicator = "icon", - onClick, - ...otherProps - } = props; - - const domRef = useDOMRef(ref); - - const necessityLabel = Boolean(isRequired) ? "(required)" : "(optional)"; - const icon = ( - - * - - ); - - return ( - - {children} - {/* necessityLabel is hidden to screen readers if the field is required because - * aria-required is set on the field in that case. That will already be announced, - * so no need to duplicate it here. If optional, we do want it to be announced here. */} - {(necessityIndicator === "label" || - (necessityIndicator === "icon" && Boolean(isRequired))) && - " \u200b"} - {necessityIndicator === "label" && ( - - {necessityLabel} - - )} - {necessityIndicator === "icon" && Boolean(isRequired) && icon} - - ); -}; - -export const Label = forwardRef(_Label); diff --git a/app/client/packages/design-system/headless/src/components/Field/src/index.ts b/app/client/packages/design-system/headless/src/components/Field/src/index.ts deleted file mode 100644 index 7f94c4c65ac..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./Field"; -export type { LabelProps } from "./Label"; -export type { FieldProps, FieldRef } from "./Field"; diff --git a/app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx b/app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx deleted file mode 100644 index 691d8d8c7b8..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import type { Ref } from "react"; -import React, { useCallback, useRef } from "react"; -import { useTextField } from "@react-aria/textfield"; -import { chain, useLayoutEffect } from "@react-aria/utils"; -import { useControlledState } from "@react-stately/utils"; - -import type { TextAreaProps } from "./types"; -import { TextInputBase } from "../../TextInputBase"; - -export type TextAreaRef = Ref; - -function TextArea(props: TextAreaProps, ref: TextAreaRef) { - const { - defaultValue, - isDisabled = false, - isReadOnly = false, - isRequired = false, - onChange, - value, - ...otherProps - } = props; - - const isEmpty = isReadOnly && !Boolean(value) && !Boolean(defaultValue); - - // not in stately because this is so we know when to re-measure, which is a spectrum design - const [inputValue, setInputValue] = useControlledState( - props.value, - props.defaultValue ?? "", - () => { - // - }, - ); - const inputRef = useRef(null); - - const onHeightChange = useCallback(() => { - // Quiet textareas always grow based on their text content. - // Standard textareas also grow by default, unless an explicit height is set. - if (props.height == null && inputRef.current) { - const input = inputRef.current; - const prevAlignment = input.style.alignSelf; - const prevOverflow = input.style.overflow; - // Firefox scroll position is lost when overflow: 'hidden' is applied so we skip applying it. - // The measure/applied height is also incorrect/reset if we turn on and off - // overflow: hidden in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1787062 - const isFirefox = "MozAppearance" in input.style; - - if (!isFirefox) { - input.style.overflow = "hidden"; - } - - input.style.alignSelf = "start"; - input.style.height = "auto"; - - const computedStyle = getComputedStyle(input); - const paddingTop = parseFloat(computedStyle.paddingTop); - const paddingBottom = parseFloat(computedStyle.paddingBottom); - - input.style.height = `${ - // subtract comptued padding and border to get the actual content height - input.scrollHeight - - paddingTop - - paddingBottom + - // Also, adding 1px to fix a bug in browser where there is a scrolllbar on certain heights - 1 - }px`; - input.style.overflow = prevOverflow; - input.style.alignSelf = prevAlignment; - } - }, [inputRef, props.height]); - - useLayoutEffect(() => { - if (inputRef.current) { - onHeightChange(); - } - }, [onHeightChange, inputValue, inputRef.current]); - - if (props.placeholder != null) { - // eslint-disable-next-line no-console - console.warn( - "Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextArea.html#help-text", - ); - } - - const { descriptionProps, errorMessageProps, inputProps, labelProps } = - useTextField( - { - ...props, - value: isEmpty ? "—" : value, - defaultValue, - onChange: chain(onChange, setInputValue), - inputElementType: "textarea", - }, - inputRef, - ); - - return ( - - ); -} - -/** - * TextAreas are multiline text inputs, useful for cases where users have - * a sizable amount of text to enter. They allow for all customizations that - * are available to text fields. - */ -const _TextArea = React.forwardRef(TextArea); - -export { _TextArea as TextArea }; diff --git a/app/client/packages/design-system/headless/src/components/TextArea/src/index.ts b/app/client/packages/design-system/headless/src/components/TextArea/src/index.ts deleted file mode 100644 index 5d86eab325a..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextArea/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export { TextArea } from "./TextArea"; -export type { TextAreaRef } from "./TextArea"; diff --git a/app/client/packages/design-system/headless/src/components/TextArea/src/types.ts b/app/client/packages/design-system/headless/src/components/TextArea/src/types.ts deleted file mode 100644 index 59c353da581..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextArea/src/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { TextInputBaseProps } from "../../TextInputBase"; -import type { OmitedSpectrumTextFieldProps } from "../../TextInput"; - -export interface TextAreaProps - extends OmitedSpectrumTextFieldProps, - Pick { - height?: number | string; - inputClassName?: string; - /** spell check attribute */ - spellCheck?: boolean; - /** classname for label */ - labelClassName?: string; - /** classname for errorMessage or description */ - helpTextClassName?: string; - /** classname for the field */ - fieldClassName?: string; - /** className for the text input. */ - className?: string; -} diff --git a/app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx b/app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx deleted file mode 100644 index 12687d1c282..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { Ref } from "react"; -import React, { forwardRef, useRef } from "react"; -import { useTextField } from "@react-aria/textfield"; - -import type { TextInputProps } from "./types"; -import { TextInputBase } from "../../TextInputBase"; - -export type TextInputRef = Ref; - -function TextInput(props: TextInputProps, ref: TextInputRef) { - const inputRef = useRef(null); - const { - defaultValue, - isReadOnly = false, - spellCheck, - type: typeProp, - value, - ...rest - } = props; - - const isEmpty = isReadOnly && !Boolean(value) && !Boolean(defaultValue); - const type = typeProp === "password" && isEmpty ? "text" : typeProp; - - const { descriptionProps, errorMessageProps, inputProps, labelProps } = - useTextField( - { ...rest, type, defaultValue, value: isEmpty ? "—" : value }, - inputRef, - ); - - if (props.placeholder != null) { - // eslint-disable-next-line no-console - console.warn( - "Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextField.html#help-text", - ); - } - - return ( - - ); -} - -const _TextInput = forwardRef(TextInput); - -export { _TextInput as TextInput }; diff --git a/app/client/packages/design-system/headless/src/components/TextInput/src/index.ts b/app/client/packages/design-system/headless/src/components/TextInput/src/index.ts deleted file mode 100644 index f72f0a0c706..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export { TextInput } from "./TextInput"; -export type { TextInputRef } from "./TextInput"; diff --git a/app/client/packages/design-system/headless/src/components/TextInput/src/types.ts b/app/client/packages/design-system/headless/src/components/TextInput/src/types.ts deleted file mode 100644 index 22eaf29aafb..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/src/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { StyleProps } from "@react-types/shared"; -import type { SpectrumTextFieldProps } from "@react-types/textfield"; - -export type OmitedSpectrumTextFieldProps = Omit< - SpectrumTextFieldProps, - keyof StyleProps | "icon" | "isQuiet" | "labelPosition" | "labelAlign" ->; - -export interface TextInputProps extends OmitedSpectrumTextFieldProps { - /** classname for the input element */ - inputClassName?: string; - /** spell check attribute */ - spellCheck?: boolean; - /** classname for label */ - labelClassName?: string; - /** classname for errorMessage or description */ - helpTextClassName?: string; - /** classname for the field */ - fieldClassName?: string; - /** className for the text input. */ - className?: string; - /** indicates loading state of the text input */ - isLoading?: boolean; - /** suffix component */ - prefix?: React.ReactNode; - /** prefix component */ - suffix?: React.ReactNode; -} diff --git a/app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts b/app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts deleted file mode 100644 index 454be2714f7..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { TextInput } from "@appsmith/wds-headless"; - -/** - * TextInput component allows users to input text. It is mostly used in forms. - */ -const meta: Meta = { - component: TextInput, - title: "WDS/headless/TextInput", - args: { - label: "Label", - placeholder: "Placeholder", - }, -}; - -export default meta; -type Story = StoryObj; - -export const Main: Story = {}; diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx b/app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx deleted file mode 100644 index 25d0f401627..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { Ref } from "react"; -import { mergeProps } from "@react-aria/utils"; -import React, { forwardRef, useRef } from "react"; -import { useHover } from "@react-aria/interactions"; -import { useFocusRing, useFocusable } from "@react-aria/focus"; - -import { Field } from "../../Field"; -import type { TextInputBaseProps } from "./types"; - -function TextInputBase(props: TextInputBaseProps, ref: Ref) { - const { - autoFocus, - descriptionProps, - errorMessageProps, - fieldClassName, - inputClassName, - inputProps, - inputRef: userInputRef, - isDisabled = false, - isLoading = false, - isReadOnly = false, - labelProps, - multiLine = false, - onBlur, - onFocus, - prefix, - suffix, - validationState, - } = props; - - // Readonly has a higher priority than disabled. - const getDisabledState = () => isDisabled && !isReadOnly; - - const { hoverProps, isHovered } = useHover({ - isDisabled: getDisabledState(), - }); - const domRef = useRef(null); - const defaultInputRef = useRef(null); - const inputRef = userInputRef ?? defaultInputRef; - - const ElementType: React.ElementType = Boolean(multiLine) - ? "textarea" - : "input"; - const isInvalid = - validationState === "invalid" && - !Boolean(isDisabled) && - !Boolean(isReadOnly); - - const { focusProps, isFocused, isFocusVisible } = useFocusRing({ - isTextInput: true, - autoFocus, - }); - - const { focusableProps } = useFocusable( - { isDisabled: getDisabledState(), onFocus: onFocus, onBlur: onBlur }, - inputRef, - ); - - // When user clicks on the startIcon or endIcon, we want to focus the input. - const focusInput: React.MouseEventHandler = () => { - inputRef.current?.focus(); - }; - - return ( - -
- {Boolean(prefix) && {prefix}} - - {Boolean(suffix) && {suffix}} -
-
- ); -} - -const _TextInputBase = forwardRef(TextInputBase); - -export { _TextInputBase as TextInputBase }; diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts b/app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts deleted file mode 100644 index 88f7c42bce1..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./types"; -export { TextInputBase } from "./TextInputBase"; diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts b/app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts deleted file mode 100644 index 51f23ee4f58..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { RefObject } from "react"; -import type { TextFieldAria } from "@react-aria/textfield"; - -import type { PressEvents } from "@react-types/shared"; - -import type { TextInputProps } from "../../TextInput"; - -export interface TextInputBaseProps - extends Omit, - PressEvents { - /** indicates if the component is textarea */ - multiLine?: boolean; - /** props to be passed to label component */ - labelProps?: TextFieldAria["labelProps"]; - /** props to be passed to input component */ - inputProps: TextFieldAria<"input" | "textarea">["inputProps"]; - /** props to be passed to description component */ - descriptionProps?: TextFieldAria["descriptionProps"]; - /** props to be passed to error component */ - errorMessageProps?: TextFieldAria["errorMessageProps"]; - /** ref for input component */ - inputRef?: RefObject; - /** Whether the input can be selected but not changed by the user. Readonly has a higher priority than disabled. */ - isReadOnly?: boolean; -} diff --git a/app/client/packages/design-system/headless/src/index.ts b/app/client/packages/design-system/headless/src/index.ts index 579239bb67e..a6b2628f3f8 100644 --- a/app/client/packages/design-system/headless/src/index.ts +++ b/app/client/packages/design-system/headless/src/index.ts @@ -1,6 +1,2 @@ -// components -export * from "./components/Field"; -export * from "./components/Tooltip"; -export * from "./components/TextInput"; -export * from "./components/TextArea"; export * from "./components/Popover"; +export * from "./components/Tooltip"; diff --git a/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx b/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx index 42be29b1ece..8aeab5dee07 100644 --- a/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx +++ b/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx @@ -1,14 +1,13 @@ +import clsx from "clsx"; +import type { SIZES } from "@appsmith/wds"; import type { ForwardedRef } from "react"; import React, { forwardRef } from "react"; +import { Text, Spinner, Icon } from "@appsmith/wds"; import { useVisuallyHidden } from "@react-aria/visually-hidden"; import { Button as HeadlessButton } from "react-aria-components"; -import type { SIZES } from "../../../shared"; -import clsx from "clsx"; -import { Text } from "../../Text"; -import { Spinner } from "../../Spinner"; + import styles from "./styles.module.css"; import type { ButtonProps } from "./types"; -import { Icon } from "../../Icon"; const _Button = (props: ButtonProps, ref: ForwardedRef) => { props = useVisuallyDisabled(props); diff --git a/app/client/packages/design-system/headless/src/components/Field/index.ts b/app/client/packages/design-system/widgets/src/components/ChatInput/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/Field/index.ts rename to app/client/packages/design-system/widgets/src/components/ChatInput/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx b/app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx new file mode 100644 index 00000000000..98feb3794d2 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx @@ -0,0 +1,164 @@ +import clsx from "clsx"; +import { + FieldError, + FieldLabel, + inputFieldStyles, + IconButton, + TextAreaInput, +} from "@appsmith/wds"; +import React, { useCallback, useRef, useEffect, useState } from "react"; +import { useControlledState } from "@react-stately/utils"; +import { chain, useLayoutEffect } from "@react-aria/utils"; +import { TextField as HeadlessTextField } from "react-aria-components"; + +import type { ChatInputProps } from "./types"; + +export function ChatInput(props: ChatInputProps) { + const { + contextualHelp, + errorMessage, + isDisabled, + isInvalid, + isLoading, + isReadOnly, + isRequired, + label, + onChange, + onSubmit, + prefix, + suffix: suffixProp, + value, + ...rest + } = props; + + const inputRef = useRef(null); + const [initialHeight, setInitialHeight] = useState(null); + const [inputValue, setInputValue] = useControlledState( + props.value, + props.defaultValue ?? "", + () => { + // + }, + ); + + useEffect(() => { + if (inputRef.current && initialHeight === null) { + const input = inputRef.current; + const computedStyle = window.getComputedStyle(input); + const height = parseFloat(computedStyle.height) || 0; + const paddingTop = parseFloat(computedStyle.paddingTop) || 0; + const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; + + setInitialHeight(height + paddingTop + paddingBottom); + } + }, [initialHeight]); + + const onHeightChange = useCallback(() => { + // Quiet textareas always grow based on their text content. + // Standard textareas also grow by default, unless an explicit height is set. + if (props.height == null && inputRef.current) { + const input = inputRef.current; + const prevAlignment = input.style.alignSelf; + const prevOverflow = input.style.overflow; + // Firefox scroll position is lost when overflow: 'hidden' is applied so we skip applying it. + // The measure/applied height is also incorrect/reset if we turn on and off + // overflow: hidden in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1787062 + const isFirefox = "MozAppearance" in input.style; + + if (!isFirefox) { + input.style.overflow = "hidden"; + } + + input.style.alignSelf = "start"; + input.style.height = "auto"; + + const computedStyle = window.getComputedStyle(input); + const height = parseFloat(computedStyle.height) || 0; + const paddingTop = parseFloat(computedStyle.paddingTop) || 0; + const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; + const textHeight = input.scrollHeight - paddingTop - paddingBottom + 1; + + if (Math.abs(textHeight - height) > 10) { + input.style.height = `${textHeight}px`; + } else { + input.style.height = "auto"; + } + + input.style.overflow = prevOverflow; + input.style.alignSelf = prevAlignment; + } + }, [inputRef, props.height]); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + onSubmit?.(); + } + }, + [onSubmit], + ); + + useLayoutEffect(() => { + if (inputRef.current) { + onHeightChange(); + } + }, [onHeightChange, inputValue]); + + const suffix = (function () { + if (Boolean(suffixProp)) return suffixProp; + + if (Boolean(isLoading)) { + return ( + + ); + } + + return ( + + ); + })(); + + const styles = { + // The --input-height is required to make the icon button vertically centered. + // Why can't we do this with CSS? Reason is that the height of the input is calculated based on the content. + "--input-height": Boolean(initialHeight) ? `${initialHeight}px` : "auto", + } as React.CSSProperties; + + return ( + + + {label} + + + {errorMessage} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts b/app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts new file mode 100644 index 00000000000..6499ef6fdf5 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts @@ -0,0 +1 @@ +export { ChatInput } from "./ChatInput"; diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts b/app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts new file mode 100644 index 00000000000..936fc5bbf21 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts @@ -0,0 +1,5 @@ +import type { TextAreaProps } from "@appsmith/wds"; + +export interface ChatInputProps extends TextAreaProps { + onSubmit?: () => void; +} diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx b/app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx new file mode 100644 index 00000000000..634451082af --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Form } from "react-aria-components"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Flex, ChatInput, Button } from "@appsmith/wds"; + +const meta: Meta = { + title: "WDS/Widgets/ChatInput", + component: ChatInput, + tags: ["autodocs"], + args: { + placeholder: "Write something...", + onSubmit: () => alert("Action triggered"), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Main: Story = { + args: { + label: "Description", + placeholder: "Write something...", + }, +}; + +export const WithLabel: Story = { + args: { + label: "Description", + }, +}; + +export const WithContextualHelp: Story = { + args: { + label: "Description", + contextualHelp: "This is a contextual help", + }, +}; + +export const Disabled: Story = { + args: { + isDisabled: true, + label: "Disabled", + }, +}; + +export const Loading: Story = { + args: { + isLoading: true, + label: "Loading", + placeholder: "Loading...", + }, +}; + +export const Validation: Story = { + render: (args) => ( +
+ + + + +
+ ), +}; diff --git a/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx b/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx index 5dc843fa84a..685f7b26234 100644 --- a/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx +++ b/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx @@ -1,8 +1,9 @@ import React, { forwardRef } from "react"; -import { Checkbox as HeadlessCheckbox } from "react-aria-components"; +import type { ForwardedRef } from "react"; import { Text, Icon } from "@appsmith/wds"; +import { Checkbox as HeadlessCheckbox } from "react-aria-components"; + import styles from "./styles.module.css"; -import type { ForwardedRef } from "react"; import type { CheckboxProps } from "./types"; const _Checkbox = ( diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx index 88bbcf7c229..738432dd4b3 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx @@ -1,65 +1,59 @@ import { - FieldError, - FieldDescription, + Popover, + ListBox, FieldLabel, - FieldListPopover, - Button, + FieldError, + inputFieldStyles, } from "@appsmith/wds"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import clsx from "clsx"; import React from "react"; -import { ComboBox as HeadlessCombobox, Input } from "react-aria-components"; -import styles from "./styles.module.css"; +import { ComboBox as HeadlessCombobox } from "react-aria-components"; + import type { ComboBoxProps } from "./types"; +import { ComboBoxTrigger } from "./ComboBoxTrigger"; export const ComboBox = (props: ComboBoxProps) => { const { + children, contextualHelp, - description, errorMessage, + isDisabled, isLoading, isRequired, - items, label, placeholder, size = "medium", ...rest } = props; + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; return ( - {({ isInvalid }) => ( - <> - -
- -
- - - - - )} + + {label} + + + {errorMessage} + + {children} +
); }; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx new file mode 100644 index 00000000000..c17bcb4cca8 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx @@ -0,0 +1,38 @@ +import clsx from "clsx"; +import React, { useMemo } from "react"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { Spinner, textInputStyles, Input, IconButton } from "@appsmith/wds"; + +import type { ComboBoxProps } from "./types"; + +interface ComboBoxTriggerProps { + size?: ComboBoxProps["size"]; + isLoading?: boolean; + isDisabled?: boolean; + placeholder?: string; +} + +export const ComboBoxTrigger: React.FC = (props) => { + const { isDisabled, isLoading, placeholder, size } = props; + + const suffix = useMemo(() => { + if (Boolean(isLoading)) return ; + + return ( + + ); + }, [isLoading, size, isDisabled]); + + return ( + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css deleted file mode 100644 index 7a9ecadc7e3..00000000000 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css +++ /dev/null @@ -1,84 +0,0 @@ -.formField { - display: flex; - flex-direction: column; - width: 100%; -} - -.inputWrapper { - position: relative; - display: flex; - align-items: center; - border-radius: var(--border-radius-elevation-3); - background-color: var(--color-bg-neutral-subtle); - flex: 1; - max-inline-size: 100%; - isolation: isolate; - box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle); - padding-inline-end: var(--inner-spacing-2); -} - -.input { - border: 0; - background-color: transparent; - font-family: inherit; - flex-grow: 1; - color: var(--color-fg); - text-overflow: ellipsis; - max-inline-size: 100%; - width: 100%; - box-sizing: content-box; - padding-block: var(--inner-spacing-1); - padding-inline: var(--inner-spacing-2); -} - -.inputWrapper:has([data-hovered]) { - background-color: var(--color-bg-neutral-subtle-hover); - box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle-hover); -} - -.inputWrapper:has([data-focused]) { - background-color: transparent; - box-shadow: none; -} - -.inputWrapper:has([data-focused]):before { - content: ""; - left: 0; - width: 100%; - height: 100%; - position: absolute; - box-shadow: 0 0 0 2px var(--color-bd-focus); - border-radius: var(--border-radius-elevation-3); - z-index: -1; -} - -.inputWrapper:has([data-invalid]) { - box-shadow: 0 0 0 1px var(--color-bd-negative); -} - -.inputWrapper:has([data-invalid][data-hovered]) { - box-shadow: 0 0 0 1px var(--color-bd-negative-hover); -} - -.formField[data-size="small"] .input { - block-size: calc( - var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) - ); - padding-block: var(--inner-spacing-2); -} - -.formField[data-size="small"] .inputWrapper { - padding-inline-end: var(--inner-spacing-1); -} - -.formField .inputWrapper [data-button] { - border-radius: calc( - var(--border-radius-elevation-3) - var(--inner-spacing-1) - ); -} - -.formField[data-size="small"] .inputWrapper { - border-radius: calc( - var(--border-radius-elevation-3) - var(--inner-spacing-2) - ); -} diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts b/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts index d2ae2a6341b..30e4c2de7ec 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts @@ -1,35 +1,15 @@ -import type { Key } from "@react-types/shared"; -import type { - ComboBoxProps as SpectrumComboBoxProps, - ValidationResult, -} from "react-aria-components"; -import type { IconProps, SIZES } from "@appsmith/wds"; +import type { SIZES, FieldProps } from "@appsmith/wds"; +import type { ComboBoxProps as SpectrumComboBoxProps } from "react-aria-components"; export interface ComboBoxProps - extends Omit, "slot"> { - /** Item objects in the collection. */ - items: ComboBoxItem[]; - /** The content to display as the label. */ - label?: string; - /** The content to display as the description. */ - description?: string; - /** The content to display as the error message. */ - errorMessage?: string | ((validation: ValidationResult) => string); + extends Omit, "slot">, + FieldProps { /** size of the select * * @default medium */ size?: Omit; - /** loading state for the input */ - isLoading?: boolean; - /** A ContextualHelp element to place next to the label. */ - contextualHelp?: string; + /** The content to display as the placeholder. */ placeholder?: string; } - -export interface ComboBoxItem { - id: Key; - label: string; - icon?: IconProps["name"]; -} diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx index 513dcd42824..5f01ae804ca 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx @@ -1,14 +1,21 @@ -import { Button, ComboBox, Flex, SIZES } from "@appsmith/wds"; -import type { Meta, StoryObj } from "@storybook/react"; import React from "react"; -import { items, itemsWithIcons } from "./items"; +import { Form } from "react-aria-components"; +import type { Meta, StoryObj } from "@storybook/react"; +import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds"; + +import { items } from "./items"; -/** - * A select displays a collapsible list of options and allows a user to select one of them. - */ const meta: Meta = { - component: ComboBox, title: "WDS/Widgets/ComboBox", + component: ComboBox, + tags: ["autodocs"], + args: { + children: items.map((item) => ( + + {item.label} + + )), + }, }; export default meta; @@ -16,71 +23,60 @@ type Story = StoryObj; export const Main: Story = { args: { - items: items, + label: "Select an option", + placeholder: "Choose...", }, - render: (args) => ( - - - - ), }; -/** - * The component supports two sizes `small` and `medium`. Default size is `medium`. - */ -export const Sizes: Story = { - render: () => ( - - {Object.keys(SIZES) - .filter((size) => !["xSmall", "large"].includes(size)) - .map((size) => ( - - ))} - - ), +export const WithLabel: Story = { + args: { + label: "Favorite Fruit", + }, +}; + +export const WithContextualHelp: Story = { + args: { + label: "Country", + contextualHelp: "Select the country you currently reside in", + }, +}; + +export const Disabled: Story = { + args: { + isDisabled: true, + label: "Disabled ComboBox", + }, }; export const Loading: Story = { args: { - placeholder: "Loading", isLoading: true, - items: items, + label: "Loading ComboBox", + placeholder: "Loading options...", }, }; +export const Size: Story = { + render: (args) => ( + + + + + ), +}; + export const Validation: Story = { - render: () => ( -
{ - e.preventDefault(); - alert("Form submitted"); - }} - > - + render: (args) => ( + e.preventDefault()}> + - + ), }; - -export const ContextualHelp: Story = { - args: { - label: "Label", - placeholder: "Contextual Help Text", - contextualHelp: "This is a contextual help text", - items: items, - }, -}; - -export const WithIcons: Story = { - args: { - label: "With icons", - items: itemsWithIcons, - }, -}; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts index 7319bf939cb..3c465f7b8c5 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts @@ -1,6 +1,4 @@ -import type { ComboBoxItem } from "../src/types"; - -export const items: ComboBoxItem[] = [ +export const items = [ { id: 1, label: "Aerospace" }, { id: 2, @@ -15,7 +13,7 @@ export const items: ComboBoxItem[] = [ { id: 9, label: "Electrical" }, ]; -export const itemsWithIcons: ComboBoxItem[] = [ +export const itemsWithIcons = [ { id: 1, label: "Aerospace", icon: "galaxy" }, { id: 2, diff --git a/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx index bf78d5578b6..b079ad5e3e8 100644 --- a/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx +++ b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx @@ -1,11 +1,13 @@ import React from "react"; -import { Tooltip } from "../../Tooltip"; -import { IconButton } from "../../IconButton"; +import { Tooltip, IconButton } from "@appsmith/wds"; + import type { ContextualProps } from "./types"; const _ContextualHelp = (props: ContextualProps) => { const { contextualHelp } = props; + if (!Boolean(contextualHelp)) return null; + return ( { - const { description, isInvalid } = props; - - if (!Boolean(description) || Boolean(isInvalid)) return null; - - return ( - - {description} - - ); -}; diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts b/app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts deleted file mode 100644 index 2c6b2d3578d..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./FieldDescription"; -export type { FieldDescriptionProps } from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css deleted file mode 100644 index 66f4818bdac..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.description { - margin-block-start: var(--inner-spacing-3); - color: var(--color-fg-neutral); -} diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts deleted file mode 100644 index 733ea6b20b8..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface FieldDescriptionProps { - /** The content to display as the description. */ - description?: string; - /** Whether the input value is invalid. */ - isInvalid?: boolean; -} diff --git a/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx b/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx index 6b62b21869e..66548149eb0 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx +++ b/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx @@ -1,18 +1,20 @@ import React from "react"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import clsx from "clsx"; -import { FieldError as HeadlessFieldError } from "react-aria-components"; +import { Text } from "@appsmith/wds"; +import { FieldError as AriaFieldError } from "react-aria-components"; + import styles from "./styles.module.css"; import type { FieldErrorProps } from "./types"; export const FieldError = (props: FieldErrorProps) => { - const { errorMessage } = props; + const { children } = props; + + if (!Boolean(children)) return null; return ( - - {errorMessage} - + + + {children} + + ); }; diff --git a/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css index bb2636b64de..f94e20aaf97 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css @@ -1,4 +1,3 @@ .errorText { - margin-block-start: var(--inner-spacing-3); - color: var(--color-fg-negative); + margin-block-start: var(--inner-spacing-2); } diff --git a/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts index 67de97f9072..8849a7b5b6d 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts @@ -2,5 +2,5 @@ import type { ValidationResult } from "react-aria-components"; export interface FieldErrorProps { /** The content to display as the error message. */ - errorMessage?: string | ((validation: ValidationResult) => string); + children?: string | ((validation: ValidationResult) => string); } diff --git a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx index 18ce87eee19..85126a123b9 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx +++ b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx @@ -1,36 +1,37 @@ import clsx from "clsx"; import React from "react"; -import { Text, ContextualHelp } from "@appsmith/wds"; -import { Label as HeadlessLabel } from "react-aria-components"; +import { ContextualHelp, Text } from "@appsmith/wds"; +import { Label as HeadlessLabel, Group } from "react-aria-components"; + import styles from "./styles.module.css"; import type { LabelProps } from "./types"; -export const FieldLabel = (props: LabelProps) => { - const { className, contextualHelp, isDisabled, isRequired, text, ...rest } = - props; +export function FieldLabel(props: LabelProps) { + const { children, contextualHelp, isDisabled, isRequired, ...rest } = props; - if (!Boolean(text) && !Boolean(contextualHelp)) return null; + if (!Boolean(children) && !Boolean(contextualHelp)) return null; return ( - - - {text} + + + {children} + {Boolean(isRequired) && ( * )} - - {Boolean(contextualHelp) && ( - - )} - + + + ); -}; +} diff --git a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css index a41305e0ab9..e286abff38c 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css @@ -1,17 +1,23 @@ -.label { +.labelGroup { display: flex; align-items: center; + gap: var(--inner-spacing-1); height: var(--sizing-3); margin-block-end: var(--inner-spacing-3); - gap: var(--inner-spacing-1); +} + +.labelGroup[data-disabled] { + opacity: var(--opacity-disabled); +} - &[data-disabled="true"] { - cursor: not-allowed; - opacity: var(--opacity-disabled); - } +.label { + display: flex; + align-items: center; + height: fit-content; + max-width: 100%; + gap: var(--inner-spacing-1); } .necessityIndicator { color: var(--color-fg-negative); - margin-inline-start: var(--inner-spacing-1); } diff --git a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts index 1d8f083b736..10f6d021c0b 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts @@ -1,8 +1,7 @@ -import type { LabelProps as HeadlessLabelProps } from "react-aria-components"; +import type { LabelProps as AriaLabelProps } from "react-aria-components"; -export interface LabelProps extends HeadlessLabelProps { - text?: string; - contextualHelp?: string; +export type LabelProps = AriaLabelProps & { + contextualHelp?: React.ReactNode; isRequired?: boolean; isDisabled?: boolean; -} +}; diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx deleted file mode 100644 index d9d7921a827..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import { listItemStyles, Popover, Icon } from "@appsmith/wds"; -import { ListBox, ListBoxItem } from "react-aria-components"; -import styles from "./styles.module.css"; -import type { FieldListPopoverProps } from "./types"; - -export const FieldListPopover = (props: FieldListPopoverProps) => { - const { items } = props; - - // place Popover in the root theme provider to get access to the CSS tokens - const root = document.body.querySelector( - "[data-theme-provider]", - ) as HTMLButtonElement; - - return ( - - - {(item) => ( - - {item.icon && } - {item.label} - - )} - - - ); -}; diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts deleted file mode 100644 index be087d853eb..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./FieldListPopover"; -export type { FieldListPopoverProps, FieldListPopoverItem } from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css deleted file mode 100644 index 5d7dc5e49ab..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.listBox { - min-inline-size: var(--trigger-width); - max-height: inherit; - overflow-y: auto; -} - -/** If at least one select item has an icon, we need to add extra padding for items that doesn't have an icon. */ -.listBox:has([data-icon]) [role="option"]:not(:has([data-icon])) { - padding-inline-start: calc( - var(--icon-size-4) + var(--inner-spacing-3) + var(--inner-spacing-2) - ); -} diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts deleted file mode 100644 index 3c98ccf204a..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Key } from "@react-types/shared"; -import type { IconProps } from "@appsmith/wds"; - -export interface FieldListPopoverProps { - /** Item objects in the collection. */ - items: FieldListPopoverItem[]; -} - -export interface FieldListPopoverItem { - id: Key; - label: string; - icon?: IconProps["name"]; -} diff --git a/app/client/packages/design-system/headless/src/components/TextInput/index.ts b/app/client/packages/design-system/widgets/src/components/Input/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/TextInput/index.ts rename to app/client/packages/design-system/widgets/src/components/Input/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx b/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx new file mode 100644 index 00000000000..3472e2b53ac --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx @@ -0,0 +1,65 @@ +import clsx from "clsx"; +import React, { forwardRef, useState } from "react"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { IconButton, Spinner, type IconProps } from "@appsmith/wds"; +import { Group, Input as HeadlessInput } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { InputProps } from "./types"; + +function _Input(props: InputProps, ref: React.Ref) { + const { + defaultValue, + isLoading, + isReadOnly, + prefix, + size, + suffix: suffixProp, + type, + value, + ...rest + } = props; + const [showPassword, setShowPassword] = useState(false); + const togglePasswordVisibility = () => setShowPassword((prev) => !prev); + const isEmpty = !Boolean(value) && !Boolean(defaultValue); + + const suffix = (() => { + if (Boolean(isLoading)) return ; + + if (type === "password") { + const icon: IconProps["name"] = showPassword ? "eye-off" : "eye"; + + return ( + + ); + } + + return suffixProp; + })(); + + return ( + + + {Boolean(prefix) && {prefix}} + {Boolean(suffix) && {suffix}} + + ); +} + +export const Input = forwardRef(_Input); diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx b/app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx new file mode 100644 index 00000000000..13dab8294f5 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx @@ -0,0 +1,51 @@ +import clsx from "clsx"; +import { Spinner } from "@appsmith/wds"; +import React, { forwardRef } from "react"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { Group, TextArea as HeadlessTextArea } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { TextAreaInputProps } from "./types"; + +function _TextAreaInput( + props: TextAreaInputProps, + ref: React.Ref, +) { + const { + defaultValue, + isLoading, + isReadOnly, + prefix, + rows, + size, + suffix: suffixProp, + value, + ...rest + } = props; + const isEmpty = !Boolean(value) && !Boolean(defaultValue); + + const suffix = (() => { + if (Boolean(isLoading)) return ; + + return suffixProp; + })(); + + return ( + + + {Boolean(prefix) && {prefix}} + {Boolean(suffix) && {suffix}} + + ); +} + +export const TextAreaInput = forwardRef(_TextAreaInput); diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/index.ts b/app/client/packages/design-system/widgets/src/components/Input/src/index.ts new file mode 100644 index 00000000000..c8628775096 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/index.ts @@ -0,0 +1,3 @@ +export * from "./Input"; +export * from "./TextAreaInput"; +export { default as textInputStyles } from "./styles.module.css"; diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css new file mode 100644 index 00000000000..2f5dd660c0f --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css @@ -0,0 +1,223 @@ +.inputGroup { + position: relative; + display: flex; + align-items: center; + gap: var(--inner-spacing-1); + width: 100%; +} + +.input { + position: relative; + display: flex; + flex: 1; + align-items: center; + box-sizing: content-box; + max-inline-size: 100%; + padding-block: var(--inner-spacing-1); + padding-inline: var(--inner-spacing-2); + gap: var(--inner-spacing-1); + border: 0; + border-radius: var(--border-radius-elevation-3); + background-color: var(--color-bg-neutral-subtle); + box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle); + isolation: isolate; +} + +.input:has(> [data-select-text]) { + block-size: var(--body-line-height); +} + +.input:is(textarea) { + block-size: auto; + min-block-size: var(--sizing-16); + align-items: flex-start; + resize: none; + font-family: inherit; +} + +.input:is(textarea)[rows="1"] { + min-block-size: initial; +} + +.input:autofill, +.input:autofill:hover, +.input:autofill:focus, +.input:autofill:active { + font-size: initial; + -webkit-text-fill-color: var(--color-fg); + -webkit-box-shadow: 0 0 0 40rem var(--color-bg-neutral-subtle) inset; +} + +/** + * ---------------------------------------------------------------------------- + * SUFFIX and PREFIX + * ---------------------------------------------------------------------------- + */ +.inputGroup [data-input-suffix] button { + border-radius: calc( + var(--border-radius-elevation-3) - var(--inner-spacing-1) + ); +} + +.inputGroup:has(> [data-input-prefix]) .input { + padding-left: var(--sizing-8); +} + +.inputGroup:has(> [data-input-prefix]) .input[data-size="small"] { + padding-left: var(--sizing-6); +} + +.inputGroup:has(> [data-input-prefix]) [data-input-prefix] { + left: var(--inner-spacing-1); + position: absolute; +} + +.inputGroup:has(> [data-input-suffix]) .input { + padding-right: var(--sizing-8); +} + +.inputGroup:has(> [data-input-suffix]) .input[data-size="small"] { + padding-right: var(--sizing-6); +} + +.inputGroup:has(> [data-input-suffix]) [data-input-suffix] { + right: var(--inner-spacing-1); + position: absolute; +} + +/* Note: the following calculations are done so that icon button isn chat input is centered vertically */ +.inputGroup:has(.input[rows="1"]) [data-input-suffix] { + --icon-size: calc( + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + + var(--inner-spacing-3) * 2 + ); + --icon-offset: calc((var(--input-height) - var(--icon-size)) / 2); + + bottom: var(--icon-offset); + right: var(--icon-offset); +} + +.inputGroup :is([data-input-suffix], [data-input-prefix]) { + display: flex; + justify-content: center; + align-items: center; +} + +/** + * ---------------------------------------------------------------------------- + * HOVERED + * ---------------------------------------------------------------------------- + */ +.inputGroup[data-hovered] + .input:not(:is([data-focused], [data-readonly], [data-disabled])) { + background-color: var(--color-bg-neutral-subtle-hover); + box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle-hover); +} + +/** + * ---------------------------------------------------------------------------- + * READONLY + * ---------------------------------------------------------------------------- + */ +.input[data-readonly] { + background-color: transparent; + box-shadow: none; + padding-inline: 0; +} + +/** Reason for doing this is because for readonly inputs, we want focus state to be wider than the component width */ +.inputGroup:has(> .input[data-readonly][data-focus-visible])::before { + content: ""; + left: calc(-0.5 * var(--inner-spacing-1)); + width: calc(100% + var(--inner-spacing-1)); + height: 100%; + position: absolute; + box-shadow: 0 0 0 2px var(--color-bd-focus); + border-radius: var(--border-radius-elevation-3); +} + +/** + * ---------------------------------------------------------------------------- + * PLACEHOLDER + * ---------------------------------------------------------------------------- + */ +.input::placeholder { + color: var(--color-fg-neutral-subtle) !important; + opacity: 1; +} + +.input:placeholder-shown { + text-overflow: ellipsis; +} + +/** + * ---------------------------------------------------------------------------- + * DISABLED + * ---------------------------------------------------------------------------- + */ +.input[data-disabled], +.input[data-disabled] :is(input, textarea), +.input[data-disabled] label { + cursor: not-allowed; + box-shadow: none; +} + +/** + * ---------------------------------------------------------------------------- + * INVALID + * ---------------------------------------------------------------------------- + */ +.input[data-invalid] { + box-shadow: 0 0 0 1px var(--color-bd-negative); +} + +.inputGroup[data-hovered] + .input[data-invalid]:not( + :is([data-focused], [data-readonly], [data-disabled]) + ) { + box-shadow: 0 0 0 1px var(--color-bd-negative-hover); +} + +/** + * ---------------------------------------------------------------------------- + * FOCUSSED + * ---------------------------------------------------------------------------- + */ +.input[data-focused]:not([data-readonly]) { + background-color: transparent; + box-shadow: 0 0 0 2px var(--color-bd-focus); +} + +/** + * ---------------------------------------------------------------------------- + * SIZE + * ---------------------------------------------------------------------------- + */ +.input[data-size="small"] { + block-size: calc( + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + ); + padding-block: var(--inner-spacing-2); +} + +.input[data-size="large"] { + block-size: calc( + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + ); + padding-block: var(--inner-spacing-3); + padding-inline: var(--inner-spacing-3); +} + +/** + * ---------------------------------------------------------------------------- + * SELECT BUTTON's TEXT + * ---------------------------------------------------------------------------- + */ +.input [data-select-text] { + display: flex; + align-items: center; +} + +.input [data-select-text] [data-icon] { + margin-inline-end: var(--inner-spacing-1); +} diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/types.ts b/app/client/packages/design-system/widgets/src/components/Input/src/types.ts new file mode 100644 index 00000000000..97ce876f1e6 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/types.ts @@ -0,0 +1,24 @@ +import type { SIZES } from "@appsmith/wds"; +import type { + InputProps as HeadlessInputProps, + TextAreaProps as HeadlessTextAreaProps, +} from "react-aria-components"; + +// Common properties for both Input and TextArea +interface CommonInputProps { + prefix?: React.ReactNode; + suffix?: React.ReactNode; + isLoading?: boolean; + isReadOnly?: boolean; + size?: Omit; +} + +export interface InputProps + extends Omit, + CommonInputProps {} + +export interface TextAreaInputProps + extends Omit, + CommonInputProps { + rows?: number; +} diff --git a/app/client/packages/design-system/widgets/src/components/Link/src/types.ts b/app/client/packages/design-system/widgets/src/components/Link/src/types.ts index cc335817b93..ba3d2fe5c1d 100644 --- a/app/client/packages/design-system/widgets/src/components/Link/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Link/src/types.ts @@ -4,4 +4,15 @@ import type { TextProps } from "../../Text"; export interface LinkProps extends Omit, - Omit {} + Omit< + AriaLinkProps, + | "style" + | "className" + | "children" + | "isDisabled" + | "onBlur" + | "onFocus" + | "onKeyDown" + | "onKeyUp" + | "slot" + > {} diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/index.ts b/app/client/packages/design-system/widgets/src/components/ListBox/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/TextInputBase/index.ts rename to app/client/packages/design-system/widgets/src/components/ListBox/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx b/app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx new file mode 100644 index 00000000000..75876ebaa68 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import styles from "./styles.module.css"; +import { ListBox as HeadlessListBox } from "react-aria-components"; + +import type { ListBoxProps } from "./types"; + +export function ListBox(props: ListBoxProps) { + const { children, ...rest } = props; + + return ( + + {children} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts b/app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts new file mode 100644 index 00000000000..ae601b28db2 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts @@ -0,0 +1,2 @@ +export { ListBox } from "./ListBox"; +export { default as listStyles } from "./styles.module.css"; diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css new file mode 100644 index 00000000000..b253d06f7e0 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css @@ -0,0 +1,5 @@ +.listBox { + min-inline-size: var(--trigger-width); + max-height: inherit; + overflow-y: auto; +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts b/app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts new file mode 100644 index 00000000000..ec7229b2aed --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts @@ -0,0 +1,3 @@ +import type { ListBoxProps as AriaListBoxProps } from "react-aria-components"; + +export interface ListBoxProps extends AriaListBoxProps {} diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/index.ts b/app/client/packages/design-system/widgets/src/components/ListBoxItem/index.ts similarity index 100% rename from app/client/packages/design-system/widgets/src/components/FieldDescription/index.ts rename to app/client/packages/design-system/widgets/src/components/ListBoxItem/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx new file mode 100644 index 00000000000..353f2444a92 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Icon, Text } from "@appsmith/wds"; +import { ListBoxItem as HeadlessListBoxItem } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { ListBoxItemProps } from "./types"; + +export function ListBoxItem(props: ListBoxItemProps) { + const { children, icon, ...rest } = props; + + return ( + + {icon && } + {children} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts new file mode 100644 index 00000000000..9554394e8f4 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts @@ -0,0 +1,2 @@ +export { ListBoxItem } from "./ListBoxItem"; +export { default as listBoxItemStyles } from "./styles.module.css"; diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css new file mode 100644 index 00000000000..f527b5ddff7 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css @@ -0,0 +1,83 @@ +@import "../../../shared/colors/colors.module.css"; + +.listBoxItem { + display: flex; + align-items: center; + padding-inline: var(--inner-spacing-3); + block-size: var(--sizing-11); +} + +.listBoxItem:first-of-type { + border-top-left-radius: var(--border-radius-elevation-3); + border-top-right-radius: var(--border-radius-elevation-3); +} + +.listBoxItem:last-of-type { + border-bottom-left-radius: var(--border-radius-elevation-3); + border-bottom-right-radius: var(--border-radius-elevation-3); +} + +/** +* ---------------------------------------------------------------------------- +* ICON STYLES +*----------------------------------------------------------------------------- +*/ +.listBoxItem [data-icon] { + margin-inline-end: var(--inner-spacing-2); +} + +.listBoxItem [data-submenu-icon] { + margin-inline-end: 0; + margin-inline-start: auto; +} + +/** +* ---------------------------------------------------------------------------- +* HOVER AND ACTIVE STATES +*----------------------------------------------------------------------------- +*/ +.listBoxItem:not([data-disabled]) { + cursor: pointer; +} + +.listBoxItem[data-hovered] { + background-color: var(--color-bg-accent-subtle-hover); +} + +.listBoxItem[data-selected] { + background-color: var(--color-bg-accent-subtle-active); +} + +/** +* ---------------------------------------------------------------------------- +* DISABLED STATE +*----------------------------------------------------------------------------- +*/ +.listBoxItem[data-disabled] { + opacity: var(--opacity-disabled); + cursor: not-allowed; +} + +/** +* ---------------------------------------------------------------------------- +* FOCUS VISIBLE +*----------------------------------------------------------------------------- +*/ +.listBoxItem[data-focus-visible] { + box-shadow: inset 0 0 0 2px var(--color-bd-focus); +} + +/** +* ---------------------------------------------------------------------------- +* SEPARATOR +*----------------------------------------------------------------------------- +*/ +.separator { + border-top: var(--border-width-1) solid var(--color-bd); + padding: 0; +} + +/* making sure the first and last child are not displayed when they have the data-separator attribute */ +.separator:is(:first-child, :last-child) { + display: none; +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts new file mode 100644 index 00000000000..613c3a88bbf --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts @@ -0,0 +1,6 @@ +import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components"; +import type { IconProps } from "@appsmith/wds"; + +export interface ListBoxItemProps extends AriaListBoxItemProps { + icon?: IconProps["name"]; +} diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx b/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx index 830d838d02b..223ed03d7c1 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx @@ -1,71 +1,28 @@ -import React from "react"; -import { Icon, listItemStyles, Popover, Text } from "@appsmith/wds"; -import { - Menu as HeadlessMenu, - MenuItem, - Separator, - SubmenuTrigger, -} from "react-aria-components"; -import styles from "./styles.module.css"; -import type { MenuProps, MenuItemProps } from "./types"; -import type { Key } from "@react-types/shared"; +import React, { createContext, useContext } from "react"; +import { listStyles, Popover } from "@appsmith/wds"; +import { Menu as HeadlessMenu } from "react-aria-components"; + +import type { MenuProps } from "./types"; + +const MenuNestingContext = createContext(0); export const Menu = (props: MenuProps) => { - const { hasSubmenu = false } = props; - // place Popover in the root theme provider to get access to the CSS tokens + const { children } = props; const root = document.body.querySelector( "[data-theme-provider]", ) as HTMLButtonElement; - return ( - // We should put only parent Popover in the root, if we put the child ones, then Menu will work incorrectly - - - {(item) => renderFunc(item, props)} - - - ); -}; - -const renderFunc = (item: MenuItemProps, props: MenuProps) => { - const { childItems, icon, id, isDisabled, isSeparator = false, label } = item; - - const isItemDisabled = () => - Boolean((props.disabledKeys as Key[])?.includes(id)) || isDisabled; - - if (childItems != null) - return ( - - - {icon && } - - {label} - - - - - {(item) => renderFunc(item, props)} - - - ); - - if (isSeparator) - return ; + const nestingLevel = useContext(MenuNestingContext); + const isRootMenu = nestingLevel === 0; return ( - - {icon && } - - {label} - - + + {/* Only the parent Popover should be placed in the root. Placing child popoves in root would cause the menu to function incorrectly */} + + + {children} + + + ); }; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts b/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts index 6fd1d55a3c6..8c312410b43 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts @@ -1,3 +1,3 @@ export * from "./Menu"; -export { MenuTrigger } from "react-aria-components"; export * from "./types"; +export { MenuTrigger, SubmenuTrigger } from "react-aria-components"; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts index 057600671d3..4c7b10e72f2 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts @@ -1,31 +1,7 @@ -import type { - MenuProps as HeadlessMenuProps, - MenuItemProps as HeadlessMenuItemProps, -} from "react-aria-components"; -import type { Key } from "@react-types/shared"; -import type { IconProps } from "../../Icon"; +import type { MenuProps as AriaMenuProps } from "react-aria-components"; export interface MenuProps extends Omit< - HeadlessMenuProps, + AriaMenuProps, "slot" | "selectionMode" | "selectedKeys" - > { - /** - * Whether the item has a submenu. - */ - hasSubmenu?: boolean; -} - -export interface MenuItem { - id: Key; - label?: string; - icon?: IconProps["name"]; - isDisabled?: boolean; - isSeparator?: boolean; - childItems?: Iterable; - hasSubmenu?: boolean; -} - -export interface MenuItemProps - extends Omit, - MenuItem {} + > {} diff --git a/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx b/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx index 4c741b9c6c9..1e635bc377b 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx @@ -1,7 +1,12 @@ import React from "react"; -import { Button, Menu, MenuTrigger } from "@appsmith/wds"; +import { + Button, + Menu, + MenuTrigger, + MenuItem, + SubmenuTrigger, +} from "@appsmith/wds"; import type { Meta, StoryObj } from "@storybook/react"; -import { menuItems, submenusItems, submenusItemsWithIcons } from "./menuData"; /** * A menu displays a list of actions or options that a user can choose. @@ -17,14 +22,24 @@ export default meta; type Story = StoryObj; export const Main: Story = { - render: (args) => ( + render: () => ( - alert(`Selected key: ${key}`)} - {...args} - /> + + Item 1 + Item 2 + Item 3 + Item 4 + + Submenu + + Submenu Item 1 + Submenu Item 2 + Submenu Item 3 + Submenu Item 4 + + + ), }; @@ -33,20 +48,38 @@ export const Submenus: Story = { render: () => ( - + + Item 1 + Item 2 + + Submenu 1 + + Submenu 1 Item 1 + Submenu 1 Item 2 + + Submenu 2 + + Submenu 2 Item 1 + Submenu 2 Item 2 + + + + + ), }; -/** - * The items can be disabled by passing `disabledKeys` or `isDisabled` in the item configuration. - */ - export const DisabledItems: Story = { render: () => ( - + + Enabled Item + Disabled Item 1 + Disabled Item 2 + Enabled Item + ), }; @@ -55,7 +88,12 @@ export const WithIcons: Story = { render: () => ( - + + Home + Files + Settings + Help + ), }; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts b/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts index 1ed8598cd4b..b15083cbfa8 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts +++ b/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts @@ -1,6 +1,4 @@ -import type { MenuItem } from "../src"; - -export const menuItems: MenuItem[] = [ +export const menuItems = [ { id: 1, label: "Aerospace" }, { id: 2, label: "Mechanical" }, { id: 3, label: "Civil" }, @@ -12,7 +10,7 @@ export const menuItems: MenuItem[] = [ { id: 9, label: "Electrical" }, ]; -export const submenusItems: MenuItem[] = [ +export const submenusItems = [ { id: 1, label: "Level 1-1" }, { id: 2, @@ -37,7 +35,7 @@ export const submenusItems: MenuItem[] = [ { id: 8, label: "Level 1-8" }, ]; -export const submenusItemsWithIcons: MenuItem[] = [ +export const submenusItemsWithIcons = [ { id: 1, label: "Level 1-1", icon: "galaxy" }, { id: 2, diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/index.ts b/app/client/packages/design-system/widgets/src/components/MenuItem/index.ts similarity index 100% rename from app/client/packages/design-system/widgets/src/components/FieldListPopover/index.ts rename to app/client/packages/design-system/widgets/src/components/MenuItem/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx b/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx new file mode 100644 index 00000000000..edd69085669 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { + composeRenderProps, + MenuItem as HeadlessMenuItem, +} from "react-aria-components"; +import { Icon, Text, listBoxItemStyles } from "@appsmith/wds"; + +import type { MenuItemProps } from "./types"; + +export function MenuItem(props: MenuItemProps) { + const { children, icon, ...rest } = props; + + return ( + + {composeRenderProps(children, (children, { hasSubmenu }) => ( + <> + {icon && } + {children} + {Boolean(hasSubmenu) && ( + + )} + + ))} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts b/app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts new file mode 100644 index 00000000000..4ab04ea84cd --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts @@ -0,0 +1 @@ +export { MenuItem } from "./MenuItem"; diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts b/app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts new file mode 100644 index 00000000000..0d52e93be02 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts @@ -0,0 +1,7 @@ +import type { IconProps } from "@appsmith/wds"; +import type { MenuItemProps as AriaMenuItemProps } from "react-aria-components"; + +export interface MenuItemProps extends AriaMenuItemProps { + icon?: IconProps["name"]; + isSubMenuItem?: boolean; +} diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx b/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx index d09ecdef084..2eb2a15e212 100644 --- a/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx @@ -1,13 +1,14 @@ import React from "react"; +import type { PopoverProps } from "react-aria-components"; import { Popover as HeadlessPopover } from "react-aria-components"; + import styles from "./styles.module.css"; -import type { PopoverProps } from "react-aria-components"; export const Popover = (props: PopoverProps) => { const { children, ...rest } = props; return ( - + {children} ); diff --git a/app/client/packages/design-system/widgets/src/styles/index.ts b/app/client/packages/design-system/widgets/src/components/Radio/index.ts similarity index 100% rename from app/client/packages/design-system/widgets/src/styles/index.ts rename to app/client/packages/design-system/widgets/src/components/Radio/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx b/app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx new file mode 100644 index 00000000000..e3dfd8668c8 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx @@ -0,0 +1,24 @@ +import { Text } from "@appsmith/wds"; +import React, { forwardRef } from "react"; +import type { ForwardedRef } from "react"; +import { Radio as AriaRadio } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { RadioProps } from "./types"; + +const _Radio = (props: RadioProps, ref: ForwardedRef) => { + const { children, labelPosition = "end", ...rest } = props; + + return ( + + {Boolean(children) && {children}} + + ); +}; + +export const Radio = forwardRef(_Radio); diff --git a/app/client/packages/design-system/widgets/src/components/Radio/src/index.ts b/app/client/packages/design-system/widgets/src/components/Radio/src/index.ts new file mode 100644 index 00000000000..867c7ecaa65 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/index.ts @@ -0,0 +1,2 @@ +export { Radio } from "./Radio"; +export type { RadioProps } from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Radio/src/styles.module.css similarity index 92% rename from app/client/packages/design-system/widgets/src/components/RadioGroup/src/styles.module.css rename to app/client/packages/design-system/widgets/src/components/Radio/src/styles.module.css index 4da345ac742..0b1d214eb27 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/styles.module.css @@ -1,21 +1,3 @@ -.radioGroup { - display: flex; - flex-direction: column; - width: 100%; - - &[data-orientation="vertical"] { - align-items: start; - - .radio { - margin-inline-end: auto; - } - } - - &[data-disabled] { - cursor: not-allowed; - } -} - .radio { display: flex; align-items: center; diff --git a/app/client/packages/design-system/widgets/src/components/Radio/src/types.ts b/app/client/packages/design-system/widgets/src/components/Radio/src/types.ts new file mode 100644 index 00000000000..243ee62cf73 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/types.ts @@ -0,0 +1,6 @@ +import type { POSITION } from "@appsmith/wds"; +import type { RadioProps as AriaRadioProps } from "react-aria-components"; + +export interface RadioProps extends AriaRadioProps { + labelPosition?: keyof typeof POSITION; +} diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx index 685d5750ad3..2225cc3f1e0 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import type { Checkbox } from "@appsmith/wds"; -import { RadioGroup } from "@appsmith/wds"; +import { Radio, RadioGroup } from "@appsmith/wds"; import type { Meta, StoryObj } from "@storybook/react"; import { StoryGrid, DataAttrWrapper } from "@design-system/storybook"; @@ -22,12 +22,24 @@ export const LightMode: Story = { {states.map((state) => ( - + + {items.map(({ label, value }) => ( + + {label} + + ))} + ))} {states.map((state) => ( - + + {items.map(({ label, value }) => ( + + {label} + + ))} + ))} diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx index a06bceee6c3..de3da32c4e9 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx @@ -1,14 +1,14 @@ +import type { ForwardedRef } from "react"; import React, { forwardRef, useRef } from "react"; -import { RadioGroup as HeadlessRadioGroup, Radio } from "react-aria-components"; import { - FieldLabel, - Flex, - Text, useGroupOrientation, + inputFieldStyles, + FieldLabel, FieldError, + toggleGroupStyles, } from "@appsmith/wds"; -import styles from "./styles.module.css"; -import type { ForwardedRef } from "react"; +import { RadioGroup as HeadlessRadioGroup, Group } from "react-aria-components"; + import type { RadioGroupProps } from "./types"; const _RadioGroup = ( @@ -16,11 +16,12 @@ const _RadioGroup = ( ref: ForwardedRef, ) => { const { + children, contextualHelp, errorMessage, isDisabled, + isReadOnly, isRequired, - items, label, ...rest } = props; @@ -32,31 +33,30 @@ const _RadioGroup = ( return ( - - + {label} + + )} + - {items.map(({ label, value, ...rest }, index) => ( - - {label} - - ))} - - + {children} + + {errorMessage} ); }; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts index dd2260c32ec..7929aca5d62 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts @@ -1,34 +1,13 @@ +import type { FieldProps, ORIENTATION } from "@appsmith/wds"; import type { RadioGroupProps as HeadlessRadioGroupProps } from "react-aria-components"; -import type { ORIENTATION } from "../../../shared"; -interface RadioGroupItemProps { - value: string; - label?: string; - isSelected?: boolean; - isDisabled?: boolean; - index?: number; -} - -export interface RadioGroupProps extends HeadlessRadioGroupProps { - /** - * A ContextualHelp element to place next to the label. - */ - contextualHelp?: string; - /** - * The content to display as the label. - */ - label?: string; - /** - * Radio that belong to this group. - */ - items: RadioGroupItemProps[]; +export interface RadioGroupProps extends HeadlessRadioGroupProps, FieldProps { /** - * The axis the checkboxes should align with. - * @default 'horizontal' + * The orientation of the radio group. */ orientation?: keyof typeof ORIENTATION; /** - * An error message for the field. + * children for the radio group */ - errorMessage?: string; + children?: React.ReactNode; } diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx index 03611026940..e16fdfbc187 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx @@ -1,70 +1,89 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { RadioGroup, Flex } from "@appsmith/wds"; +import { RadioGroup, Flex, Radio } from "@appsmith/wds"; + +const items = [ + { label: "Value 1", value: "value-1" }, + { label: "Value 2", value: "value-2" }, +]; -/** - * Radio group is a component that allows users to select one option from a set of options. - */ const meta: Meta = { - component: RadioGroup, title: "WDS/Widgets/RadioGroup", + component: RadioGroup, + tags: ["autodocs"], + args: { + defaultValue: "value-1", + children: items.map((item) => ( + + {item.label} + + )), + }, }; export default meta; type Story = StoryObj; -const items = [ - { label: "Value 1", value: "value-1" }, - { label: "Value 2", value: "value-2" }, -]; - export const Main: Story = { args: { - label: "Radio Group", - defaultValue: "value-1", - items: items, + label: "Label", + }, +}; + +export const WithLabel: Story = { + args: { + label: "Description", + }, +}; + +export const WithContextualHelp: Story = { + args: { + label: "Label", + contextualHelp: "Contextual help", }, - render: (args) => , }; -/** - * The component supports two label orientations `vertical` and `horizontal`. Default size is `horizontal`. - */ export const Orientation: Story = { - render: () => ( - - - - - ), + render: () => { + return ( + + + {items.map((item) => ( + + {item.label} + + ))} + + + {items.map((item) => ( + + {item.label} + + ))} + + + ); + }, }; export const Disabled: Story = { args: { - label: "Radio Group", - defaultValue: "value-1", isDisabled: true, - items: items, + label: "Disabled", }, - render: (args) => , }; export const Required: Story = { args: { - label: "Radio Group", - defaultValue: "value-1", isRequired: true, - items: items, + label: "Required", }, - render: (args) => , }; export const Invalid: Story = { args: { - label: "Radio Group", + errorMessage: "There is an error", + label: "Invalid", isInvalid: true, - errorMessage: "This is a error message", - items: items, }, - render: (args) => , }; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx index 6be7f257879..cc5512573a9 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; import { render, screen } from "@testing-library/react"; -import { RadioGroup } from "@appsmith/wds"; +import { Radio, RadioGroup } from "@appsmith/wds"; describe("@appsmith/wds/RadioGroup", () => { const items = [ @@ -12,7 +12,13 @@ describe("@appsmith/wds/RadioGroup", () => { it("should render the Radio group", async () => { const { container } = render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); expect(screen.getByText("Value 1")).toBeInTheDocument(); @@ -46,11 +52,13 @@ describe("@appsmith/wds/RadioGroup", () => { it("should support custom props", () => { render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); const radioGroup = screen.getByTestId("t--radio-group"); @@ -60,7 +68,13 @@ describe("@appsmith/wds/RadioGroup", () => { it("should render checked checkboxes when value is passed", () => { render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); const options = screen.getAllByRole("radio"); @@ -73,11 +87,13 @@ describe("@appsmith/wds/RadioGroup", () => { const onChangeSpy = jest.fn(); render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); const options = screen.getAllByRole("radio"); @@ -87,7 +103,15 @@ describe("@appsmith/wds/RadioGroup", () => { }); it("should be able to render disabled checkboxes", () => { - render(); + render( + + {items.map(({ label, value }) => ( + + {label} + + ))} + , + ); const options = screen.getAllByRole("radio"); diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx index 092c3124345..0337fd709ad 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx +++ b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx @@ -1,40 +1,36 @@ -import React, { useRef } from "react"; +import React from "react"; import { - FieldDescription, FieldError, - Icon, FieldLabel, - Spinner, - FieldListPopover, + ListBox, + inputFieldStyles, + Popover, } from "@appsmith/wds"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import clsx from "clsx"; -import { - Button, - Select as HeadlessSelect, - SelectValue, -} from "react-aria-components"; -import styles from "./styles.module.css"; +import { Select as HeadlessSelect } from "react-aria-components"; + import type { SelectProps } from "./types"; +import { SelectTrigger } from "./SelectTrigger"; export const Select = (props: SelectProps) => { const { + children, contextualHelp, - description, errorMessage, + isDisabled, isLoading, isRequired, - items, label, + placeholder, size = "medium", ...rest } = props; - const triggerRef = useRef(null); + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; return ( { <> + {label} + + - - - - + {errorMessage} + + {children} + )} diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx new file mode 100644 index 00000000000..5fcd5ca50f4 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Icon, Spinner, textInputStyles } from "@appsmith/wds"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { Button, Group, SelectValue } from "react-aria-components"; + +import type { SelectProps } from "./types"; + +interface SelectTriggerProps { + size?: SelectProps["size"]; + isLoading?: boolean; + isInvalid?: boolean; + placeholder?: string; + isDisabled?: boolean; +} + +export const SelectTrigger: React.FC = (props) => { + const { isDisabled, isInvalid, isLoading, placeholder, size } = props; + + return ( + + + + {Boolean(isLoading) ? : } + + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css deleted file mode 100644 index f3d0aee824b..00000000000 --- a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.formField { - display: flex; - flex-direction: column; - width: 100%; -} - -.textField { - display: flex; - position: relative; - padding: 0; - height: var(--sizing-9); - border: none; - align-items: center; - border-radius: var(--border-radius-elevation-3); - background-color: var(--color-bg-neutral-subtle); - max-inline-size: 100%; - padding-inline-start: var(--inner-spacing-2); - padding-inline-end: calc(var(--inner-spacing-3) + var(--icon-size-4)); - padding-block: var(--inner-spacing-3); - box-shadow: inset 0 0 0 var(--border-width-1) - var(--color-bd-on-neutral-subtle); - cursor: pointer; -} - -.formField[data-invalid] .textField { - box-shadow: 0 0 0 var(--border-width-1) var(--color-bd-negative); -} - -.formField[data-size="small"] .textField { - padding-block: var(--inner-spacing-2); -} - -.textField[data-focus-visible] { - box-shadow: - 0 0 0 2px var(--color-bg), - 0 0 0 4px var(--color-bd-focus); -} - -.textField[data-hovered] { - background-color: var(--color-bg-neutral-subtle-hover); - box-shadow: inset 0 0 0 var(--border-width-1) - var(--color-bd-on-neutral-subtle-hover); -} - -.textField [data-icon] { - position: absolute; - right: var(--inner-spacing-1); -} - -.fieldValue { - text-align: left; - flex: 1; -} - -.fieldValue[data-placeholder] { - color: var(--color-fg-neutral-subtle); -} - -.fieldValue [data-icon] { - display: none; -} diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/types.ts b/app/client/packages/design-system/widgets/src/components/Select/src/types.ts index 1d500e53665..d2d57a90571 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Select/src/types.ts @@ -1,26 +1,12 @@ -import type { - SelectProps as SpectrumSelectProps, - ValidationResult, -} from "react-aria-components"; -import type { SIZES, FieldListPopoverItem } from "@appsmith/wds"; +import type { SIZES, FieldProps } from "@appsmith/wds"; +import type { SelectProps as SpectrumSelectProps } from "react-aria-components"; export interface SelectProps - extends Omit, "slot"> { - /** Item objects in the collection. */ - items: FieldListPopoverItem[]; - /** The content to display as the label. */ - label?: string; - /** The content to display as the description. */ - description?: string; - /** The content to display as the error message. */ - errorMessage?: string | ((validation: ValidationResult) => string); + extends Omit, "slot">, + FieldProps { /** size of the select * * @default medium */ size?: Omit; - /** loading state for the input */ - isLoading?: boolean; - /** A ContextualHelp element to place next to the label. */ - contextualHelp?: string; } diff --git a/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx b/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx index a7b963f9604..84b1ae3e874 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx @@ -1,6 +1,7 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { Select, Button, Flex, SIZES } from "@appsmith/wds"; +import { Select, Button, Flex, SIZES, ListBoxItem } from "@appsmith/wds"; + import { selectItems, selectItemsWithIcons } from "./selectData"; /** @@ -9,6 +10,13 @@ import { selectItems, selectItemsWithIcons } from "./selectData"; const meta: Meta = { component: Select, title: "WDS/Widgets/Select", + args: { + children: selectItems.map((item) => ( + + {item.label} + + )), + }, }; export default meta; @@ -16,13 +24,13 @@ type Story = StoryObj; export const Main: Story = { args: { - items: selectItems, + label: "Label", + children: selectItems.map((item) => ( + + {item.label} + + )), }, - render: (args) => ( - - + ))} ), @@ -49,7 +58,13 @@ export const Loading: Story = { args: { placeholder: "Loading", isLoading: true, - items: selectItems, + }, +}; + +export const Disabled: Story = { + args: { + placeholder: "Disabled", + isDisabled: true, }, }; @@ -63,9 +78,8 @@ export const Validation: Story = { >