From 2bfd176a32cc5311f197d8d4f74ee1649b123e8d Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Mon, 10 Aug 2020 13:58:56 +0530 Subject: [PATCH 1/2] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 13b6780e71..e04acc8f95 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Do all this **without HTML/CSS**, and writing any custom integrations. * [Customer Support Dashboard](https://app.appsmith.com/applications/5f2aeb2580ca1f6faaed4e4a/pages/5f2d61b580ca1f6faaed4e79#utm_source=github&utm_medium=homepage) +## Build & Deploy + +* [Docker](https://docs.appsmith.com/quick-start#docker) ## Why Appsmith? @@ -55,10 +58,6 @@ Appsmith provides a better way of building internal tools by visualising them as * **Fine-grained access control**: Control who can edit / view your applications from a single control panel * **App management**: Build and organise multiple applications on a single platform -## Build & Deploy - -* [Docker](https://docs.appsmith.com/quick-start#docker) - ## Documentation & Support If you have encountered a bug or need to get in touch with us, you can contact us using one of the following channels: From 05f190c102947782c72764abef58445fee8ca8b2 Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Mon, 10 Aug 2020 14:22:45 +0530 Subject: [PATCH 2/2] Feature/entity browse (#220) # New Feature: Entity Explorer - Entities are actions (apis and queries), datasources, pages, and widgets - With this new feature, all entities in the application will be available to view in the new entity explorer sidebar - All existing application features from the api sidebar, query sidebar, datasource sidebar and pages sidebar now are avialable on the entity explorer sidebar - Users are now able to quickly switch to any entity in the application from the entity explorer sidebar. - Users can also search all entities in the application from the new sidebar. Use cmd + f or ctrl + f to focus on the search input - Users can rename entities from the new sidebar - Users can also perform contextual actions on these entities like set a page as home page, copy/move actions, delete entity, etc from the context menu available alongside the entities in the sidebar - Users can view the properties of the entities in the sidebar, as well as copy bindings to use in the application. --- .../ApiPaneTests/API_All_Verb_spec.js | 4 +- .../ApiPaneTests/API_Edit_spec.js | 7 +- .../ApiPaneTests/API_Mustache_spec.js | 4 + .../ApiPaneTests/API_Search_spec.js | 3 +- .../API_all_sidebar_actions_spec.js | 19 +- .../Binding/Bind_DatePicker_Text_spec.js | 4 + .../Binding/Bind_TableTextPagination_spec.js | 7 +- .../Binding/Bind_tableApi_spec.js | 3 +- .../Smoke_TestSuite/Binding/ChartText.js | 3 + .../Smoke_TestSuite/Binding/TextTable.js | 11 + .../DisplayWidgets/Chart_spec.js | 13 +- .../DisplayWidgets/Image_spec.js | 5 + .../DisplayWidgets/Table_spec.js | 2 + .../DisplayWidgets/Text_spec.js | 2 + .../DynamicInput/autocomplete_spec.js | 1 + .../FormWidgets/CheckBox_spec.js | 6 + .../FormWidgets/DatePicker_spec.js | 2 + .../FormWidgets/Dropdown_spec.js | 4 + .../FormWidgets/FilePicker_spec.js | 2 + .../FormWidgets/FormWidget_spec.js | 3 + .../Smoke_TestSuite/FormWidgets/Input_spec.js | 6 + .../Smoke_TestSuite/FormWidgets/Radio_spec.js | 5 + .../FormWidgets/RichTextEditor_spec.js | 2 + .../Smoke_TestSuite/LayoutWidgets/Tab_spec.js | 4 + .../QueryPane/MongoDatasource_spec.js | 5 +- .../QueryPane/PostgreDatasource_spec.js | 6 +- .../UnitTest/LoginFromUIApp_spec.js | 4 +- .../cypress/locators/DatasourcesEditor.json | 5 +- app/client/cypress/locators/Pages.json | 14 +- app/client/cypress/locators/QueryEditor.json | 3 +- .../cypress/locators/apiWidgetslocator.json | 7 +- .../cypress/locators/commonlocators.json | 2 + app/client/cypress/support/commands.js | 160 +++++--- app/client/package.json | 1 + app/client/src/AppRouter.tsx | 12 +- app/client/src/actions/actionActions.ts | 9 +- app/client/src/actions/apiPaneActions.ts | 4 +- app/client/src/actions/applicationActions.ts | 9 + app/client/src/actions/datasourceActions.ts | 7 + app/client/src/actions/explorerActions.ts | 10 + app/client/src/actions/pageActions.tsx | 26 +- app/client/src/actions/propertyPaneActions.ts | 2 +- app/client/src/actions/widgetActions.tsx | 26 ++ app/client/src/api/ApplicationApi.tsx | 4 +- app/client/src/api/PageApi.tsx | 8 +- .../src/assets/icons/control/collapse.svg | 3 + .../assets/icons/control/more-vertical.svg | 5 - .../src/assets/icons/menu/api-colored.svg | 3 + .../assets/icons/menu/datasource-colored.svg | 3 + app/client/src/assets/icons/menu/explorer.svg | 3 + app/client/src/assets/icons/menu/homepage.svg | 1 - app/client/src/assets/icons/menu/page.svg | 3 + .../src/assets/icons/menu/widgets-colored.svg | 3 + app/client/src/assets/icons/widget/modal.svg | 11 +- .../appsmith/PositionedContainer.tsx | 1 + .../appsmith/RichTextEditorComponent.tsx | 5 +- .../appsmith/help/DocumentationSearch.tsx | 2 - .../editorComponents/ActionNameEditor.tsx | 65 ++-- .../components/editorComponents/Button.tsx | 43 +-- .../CodeEditor/EvaluatedValuePopup.tsx | 3 +- .../editorComponents/ContextDropdown.tsx | 7 +- .../components/editorComponents/Divider.tsx | 2 +- .../editorComponents/DraggableComponent.tsx | 4 +- .../editorComponents/DropTargetComponent.tsx | 23 +- .../editorComponents/EditableText.tsx | 41 +- .../EditorContextProvider.tsx | 22 +- .../HighlightedCode/index.tsx | 96 +++-- .../HighlightedCode/themes.ts | 2 +- .../editorComponents/NavBarItem.tsx | 80 +--- .../editorComponents/ResizableComponent.tsx | 15 +- .../components/editorComponents/Sidebar.tsx | 100 +---- .../components/editorComponents/Tooltip.tsx | 2 + .../WidgetNameComponent/index.tsx | 4 +- .../actioncreator/TreeDropdown.tsx | 37 +- .../form/fields/KeyValueFieldArray.tsx | 11 +- .../src/constants/ApiEditorConstants.ts | 13 + app/client/src/constants/Colors.tsx | 7 + app/client/src/constants/DefaultTheme.tsx | 36 +- app/client/src/constants/Explorer.ts | 2 + app/client/src/constants/Fonts.tsx | 1 - app/client/src/constants/IconConstants.tsx | 13 +- .../src/constants/ReduxActionConstants.tsx | 16 +- app/client/src/constants/routes.ts | 54 +-- .../src/entities/DataTree/dataTreeFactory.ts | 6 +- app/client/src/icons/ControlIcons.tsx | 7 +- app/client/src/icons/MenuIcons.tsx | 30 ++ app/client/src/icons/WidgetIcons.tsx | 11 +- app/client/src/index.tsx | 1 + .../mockResponses/WidgetConfigResponse.tsx | 1 - .../src/pages/AppViewer/viewer/SideNav.tsx | 2 +- .../pages/Editor/APIEditor/ApiHomeScreen.tsx | 55 +-- .../pages/Editor/APIEditor/CurlImportForm.tsx | 3 +- .../src/pages/Editor/APIEditor/Form.tsx | 19 +- .../Editor/APIEditor/RapidApiEditorForm.tsx | 25 +- .../src/pages/Editor/APIEditor/index.tsx | 29 +- app/client/src/pages/Editor/ApiSidebar.tsx | 239 ------------ app/client/src/pages/Editor/Canvas.tsx | 8 +- .../pages/Editor/DataSourceEditor/index.tsx | 17 + .../src/pages/Editor/DataSourceSidebar.tsx | 364 ------------------ app/client/src/pages/Editor/EditorHeader.tsx | 4 +- .../Editor/Explorer/Actions/ActionEntity.tsx | 88 +++++ .../Actions/ActionEntityContextMenu.tsx | 132 +++++++ .../Editor/Explorer/Actions/ActionsGroup.tsx | 90 +++++ .../pages/Editor/Explorer/Actions/helpers.tsx | 115 ++++++ .../Editor/Explorer/ContextMenuTrigger.tsx | 29 ++ .../Datasources/DataSourceContextMenu.tsx | 37 ++ .../Explorer/Datasources/DatasourceEntity.tsx | 54 +++ .../Explorer/Datasources/DatasourcesGroup.tsx | 76 ++++ .../Explorer/Datasources/PluginGroup.tsx | 48 +++ .../Editor/Explorer/Entity/AddButton.tsx | 29 ++ .../pages/Editor/Explorer/Entity/Collapse.tsx | 36 ++ .../Editor/Explorer/Entity/CollapseToggle.tsx | 32 ++ .../Editor/Explorer/Entity/EntityProperty.tsx | 216 +++++++++++ .../pages/Editor/Explorer/Entity/Loader.tsx | 55 +++ .../src/pages/Editor/Explorer/Entity/Name.tsx | 172 +++++++++ .../Editor/Explorer/Entity/Placeholder.tsx | 27 ++ .../pages/Editor/Explorer/Entity/index.tsx | 162 ++++++++ .../pages/Editor/Explorer/ExplorerIcons.tsx | 100 +++++ .../pages/Editor/Explorer/ExplorerSearch.tsx | 83 ++++ .../Explorer/ExplorerStyledComponents.tsx | 27 ++ .../pages/Editor/Explorer/ExplorerTitle.tsx | 41 ++ .../Editor/Explorer/Pages/PageContextMenu.tsx | 88 +++++ .../Editor/Explorer/Pages/PageEntity.tsx | 76 ++++ .../pages/Editor/Explorer/Pages/PageGroup.tsx | 88 +++++ .../Explorer/Widgets/WidgetContextMenu.tsx | 57 +++ .../Editor/Explorer/Widgets/WidgetEntity.tsx | 179 +++++++++ .../Editor/Explorer/Widgets/WidgetGroup.tsx | 188 +++++++++ .../src/pages/Editor/Explorer/helpers.tsx | 50 +++ app/client/src/pages/Editor/Explorer/hooks.ts | 220 +++++++++++ .../src/pages/Editor/Explorer/index.tsx | 104 +++++ app/client/src/pages/Editor/MainContainer.tsx | 6 +- app/client/src/pages/Editor/Navbar.tsx | 85 ++++ .../PageListSidebar/CreatePageButton.tsx | 60 --- .../pages/Editor/PageListSidebar/index.tsx | 165 -------- .../src/pages/Editor/PropertyPaneTitle.tsx | 31 +- .../Editor/QueryEditor/QueryHomeScreen.tsx | 7 +- .../src/pages/Editor/QueryEditor/index.tsx | 26 +- app/client/src/pages/Editor/QuerySidebar.tsx | 201 ---------- app/client/src/pages/Editor/Sidebar.tsx | 88 ----- app/client/src/pages/Editor/WidgetCard.tsx | 26 +- app/client/src/pages/Editor/WidgetSidebar.tsx | 71 ++-- app/client/src/pages/Editor/WidgetsEditor.tsx | 78 ++-- app/client/src/pages/Editor/index.tsx | 87 ++++- app/client/src/pages/Editor/routes.tsx | 42 +- app/client/src/pages/common/AppRoute.tsx | 29 +- .../entityReducers/pageListReducer.tsx | 24 +- app/client/src/reducers/index.tsx | 4 + .../src/reducers/uiReducers/apiPaneReducer.ts | 67 +--- .../reducers/uiReducers/dragResizeReducer.ts | 16 + .../src/reducers/uiReducers/editorReducer.tsx | 14 - .../reducers/uiReducers/explorerReducer.ts | 85 ++++ app/client/src/reducers/uiReducers/index.tsx | 4 + .../src/reducers/uiReducers/pageDSLReducer.ts | 34 ++ .../reducers/uiReducers/queryPaneReducer.ts | 10 +- app/client/src/sagas/ActionSagas.ts | 10 +- app/client/src/sagas/ApiPaneSagas.ts | 126 ++---- app/client/src/sagas/ApplicationSagas.tsx | 7 +- app/client/src/sagas/CurlImportSagas.ts | 6 +- app/client/src/sagas/DatasourcesSagas.ts | 44 ++- app/client/src/sagas/InitSagas.ts | 65 +++- app/client/src/sagas/ModalSagas.ts | 27 +- app/client/src/sagas/PageSagas.tsx | 46 ++- app/client/src/sagas/QueryPaneSagas.ts | 56 +-- app/client/src/sagas/WidgetOperationSagas.tsx | 10 +- app/client/src/sagas/selectors.tsx | 2 + app/client/src/selectors/editorSelectors.tsx | 2 - app/client/src/utils/WidgetFactory.tsx | 2 +- app/client/src/utils/WidgetPropsUtils.tsx | 2 +- app/client/src/utils/helpers.tsx | 18 + .../src/utils/hooks/dragResizeHooks.tsx | 102 +++-- app/client/src/utils/hooks/useClipboard.tsx | 54 +++ app/client/src/wdyr.ts | 10 + app/client/src/widgets/BaseWidget.tsx | 4 +- app/client/tsconfig.json | 1 + app/client/yarn.lock | 9 +- 175 files changed, 4348 insertions(+), 2223 deletions(-) create mode 100644 app/client/src/actions/explorerActions.ts create mode 100644 app/client/src/assets/icons/control/collapse.svg create mode 100644 app/client/src/assets/icons/menu/api-colored.svg create mode 100644 app/client/src/assets/icons/menu/datasource-colored.svg create mode 100644 app/client/src/assets/icons/menu/explorer.svg create mode 100644 app/client/src/assets/icons/menu/page.svg create mode 100644 app/client/src/assets/icons/menu/widgets-colored.svg create mode 100644 app/client/src/components/editorComponents/Tooltip.tsx create mode 100644 app/client/src/constants/Explorer.ts delete mode 100644 app/client/src/pages/Editor/ApiSidebar.tsx delete mode 100644 app/client/src/pages/Editor/DataSourceSidebar.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Actions/ActionEntityContextMenu.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Actions/helpers.tsx create mode 100644 app/client/src/pages/Editor/Explorer/ContextMenuTrigger.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/AddButton.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/Collapse.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/CollapseToggle.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/EntityProperty.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/Loader.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/Name.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/Placeholder.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/index.tsx create mode 100644 app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx create mode 100644 app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx create mode 100644 app/client/src/pages/Editor/Explorer/ExplorerStyledComponents.tsx create mode 100644 app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx create mode 100644 app/client/src/pages/Editor/Explorer/helpers.tsx create mode 100644 app/client/src/pages/Editor/Explorer/hooks.ts create mode 100644 app/client/src/pages/Editor/Explorer/index.tsx create mode 100644 app/client/src/pages/Editor/Navbar.tsx delete mode 100644 app/client/src/pages/Editor/PageListSidebar/CreatePageButton.tsx delete mode 100644 app/client/src/pages/Editor/PageListSidebar/index.tsx delete mode 100644 app/client/src/pages/Editor/QuerySidebar.tsx delete mode 100644 app/client/src/pages/Editor/Sidebar.tsx create mode 100644 app/client/src/reducers/uiReducers/explorerReducer.ts create mode 100644 app/client/src/reducers/uiReducers/pageDSLReducer.ts create mode 100644 app/client/src/utils/hooks/useClipboard.tsx create mode 100644 app/client/src/wdyr.ts diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js index 166a3d6701..0686269133 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js @@ -171,13 +171,11 @@ describe("API Panel Test Functionality", function() { it("API check with Invalid Header", function() { cy.CreateAPI("FourthAPI"); cy.log("Creation of API Action successful"); - cy.EnterSourceDetailsWithQueryParam( + cy.EnterSourceDetailsWithHeader( testdata.baseUrl, testdata.methods, testdata.headerKey, testdata.invalidValue, - testdata.queryKey, - testdata.queryValue, ); cy.WaitAutoSave(); cy.RunAPI(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js index 32ed8cc158..daa6f15429 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js @@ -12,13 +12,10 @@ describe("API Panel Test Functionality", function() { cy.SaveAndRunAPI(); cy.validateRequest(testdata.baseUrl, testdata.methods, testdata.Get); cy.ResponseStatusCheck(testdata.successStatusCode); - cy.get(apiwidget.createApiOnSideBar) - .first() - .click({ force: true }); - cy.SearchAPIandClick("FirstAPI"); + cy.SearchEntityandOpen("FirstAPI"); cy.EditApiName("SecondAPI"); cy.ClearSearch(); - cy.SearchAPIandClick("SecondAPI"); + cy.SearchEntityandOpen("SecondAPI"); cy.DeleteAPI(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js index bda5c0c1a5..be79b8feac 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js @@ -2,15 +2,19 @@ const commonlocators = require("../../../locators/commonlocators.json"); const dsl = require("../../../fixtures/commondsl.json"); const widgetsPage = require("../../../locators/Widgets.json"); const testdata = require("../../../fixtures/testdata.json"); +const pages = require("../../../locators/Pages.json"); describe("Moustache test Functionality", function() { beforeEach(() => { cy.addDsl(dsl); }); it("Moustache test Functionality", function() { + //cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.widgetText("Api", widgetsPage.textWidget, widgetsPage.textInputval); cy.testCodeMirror("/api/users/2"); + cy.NavigateToEntityExplorer(); + cy.wait(10000); cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("TestAPINew"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js index 1df61ba5f9..7414915d7d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js @@ -8,9 +8,10 @@ describe("API Panel Test Functionality ", function() { cy.CreateAPI("FirstAPI"); cy.RunAPI(); cy.log("Creation of FirstAPI Action successful"); + cy.NavigateToAPI_Panel(); cy.CreateAPI("SecondAPI"); cy.RunAPI(); cy.log("Creation of SecondAPI Action successful"); - cy.SearchAPI("SecondAPI", "FirstAPI"); + cy.SearchEntity("SecondAPI", "FirstAPI"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js index 93e6d04f8e..a255cabee8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js @@ -1,17 +1,20 @@ +const commonlocators = require("../../../locators/commonlocators.json"); + describe("API Panel Test Functionality ", function() { it("Test API copy/Move/delete feature", function() { cy.log("Login Successful"); cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); - cy.CreateAPI("FirstAPI"); cy.log("Creation of FirstAPI Action successful"); - - cy.CopyAPIToHome("FirstAPI"); - cy.DeleteAPI("FirstAPI"); - //cy.MoveAPIToPage(); - cy.CreateAPI("SecondApi"); - cy.log("Creation of FirstAPI Action successful"); - cy.CreationOfUniqueAPIcheck("SecondApi"); + cy.GlobalSearchEntity("FirstAPI"); + cy.xpath('//*[local-name()="g" and @id="Icon/Outline/more-vertical"]') + .last() + .should("be.hidden") + .invoke("show") + .click({ force: true }); + cy.CopyAPIToHome(); + cy.GlobalSearchEntity("FirstAPICopy"); + cy.DeleteAPIFromSideBar(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js index 46a1752cc3..8a895531ab 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js @@ -2,6 +2,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); const dsl = require("../../../fixtures/uiBindDsl.json"); const publishPage = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); describe("Binding the Datepicker and Text Widget", function() { let nextDay; @@ -15,6 +16,7 @@ describe("Binding the Datepicker and Text Widget", function() { /** * Bind DatePicker1 to Text for "selectedDate" */ + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{DatePicker1.selectedDate}}"); cy.get(commonlocators.editPropCrossButton).click(); @@ -48,6 +50,7 @@ describe("Binding the Datepicker and Text Widget", function() { }); it("DatePicker1-text: Change the date in DatePicker1 and Validate the same in text widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); /** @@ -105,6 +108,7 @@ describe("Binding the Datepicker and Text Widget", function() { /** * Bind the DatePicker1 and DatePicker2 along with hard coded text to Text widget */ + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext( "text", diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js index 448d90b405..018985dfc9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js @@ -16,12 +16,13 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Table-Text, Validate Server Side Pagination of Paginate with Table Page No", function() { - cy.get(pages.pagesIcon).click({ force: true }); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); /**Bind Api1 with Table widget */ cy.testJsontext("tabledata", "{{Api1.data.results}}"); cy.CheckWidgetProperties(commonlocators.serverSidePaginationCheckbox); /**Bind Table with Textwidget with selected row */ + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{Table1.selectedRow.url}}"); cy.readTabledata("0", "1").then(tabData => { @@ -65,12 +66,12 @@ describe("Test Create Api and Bind to Table widget", function() { parseSpecialCharSequences: false, }); cy.WaitAutoSave(); - - cy.get(pages.pagesIcon).click({ force: true }); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); /** Bind the Table widget with Text widget*/ cy.testJsontext("text", "{{Table1.selectedRow.url}}"); cy.get(commonlocators.editPropCrossButton).click(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); cy.testJsontext("tabledata", "{{Api2.data.results}}"); cy.callApi("Api2"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js index bbdb16c80c..f499396bb4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js @@ -27,7 +27,8 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Test_Validate the Api data is updated on Table widget", function() { - cy.get(pages.pagesIcon).click({ force: true }); + //cy.get(pages.pagesIcon).click({ force: true }); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); cy.testJsontext("tabledata", "{{Api1.data}}"); cy.get(commonlocators.editPropCrossButton).click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js index f4dd55e2ff..aa519de87f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js @@ -2,12 +2,14 @@ const commonlocators = require("../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/ChartTextDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Text-Chart Binding Functionality", function() { before(() => { cy.addDsl(dsl); }); it("Text-Chart Binding Functionality View", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", JSON.stringify(this.data.chartInputValidate)); cy.get(commonlocators.TextInside).should( @@ -15,6 +17,7 @@ describe("Text-Chart Binding Functionality", function() { JSON.stringify(this.data.chartInputValidate), ); cy.closePropertyPane(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("chartwidget"); cy.get(viewWidgetsPage.chartType) .find(commonlocators.dropdownbuttonclick) diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js index 51140fb9cf..5eff6c04da 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js @@ -1,12 +1,20 @@ const commonlocators = require("../../../locators/commonlocators.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/TextTabledsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Text-Table Binding Functionality", function() { + Cypress.on("uncaught:exception", (err, runnable) => { + // returning false here prevents Cypress from + // failing the test + return false; + }); + before(() => { cy.addDsl(dsl); }); it("Text-Table Binding Functionality For Id", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); /** * @param(Index) Provide index value to select the row. @@ -35,6 +43,7 @@ describe("Text-Table Binding Functionality", function() { cy.get(publish.backToEditor) .first() .click(); + cy.get(pages.widgetsEditor).click(); cy.isSelectRow(2); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{Table1.selectedRow.email}}"); @@ -59,6 +68,7 @@ describe("Text-Table Binding Functionality", function() { cy.get(publish.backToEditor) .first() .click(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{Table1.pageSize}}"); cy.get(commonlocators.TableRow) @@ -85,6 +95,7 @@ describe("Text-Table Binding Functionality", function() { * @param(Index) Provide index value to select the row. */ cy.isSelectRow(1); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", JSON.stringify(this.data.textfun)); /** diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js index d84ea1689e..0328351635 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js @@ -2,14 +2,19 @@ const commonlocators = require("../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/displayWidgetDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Chart Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); - it("Chart Widget Functionality", function() { - cy.openPropertyPane("chartwidget"); + beforeEach(() => { + cy.get(pages.widgetsEditor).click(); + cy.openPropertyPane("chartwidget"); + }); + + it("Chart Widget Functionality", function() { /** * @param{Text} Random Text * @param{ChartWidget}Mouseover @@ -66,28 +71,24 @@ describe("Chart Widget Functionality", function() { cy.get(commonlocators.editPropCrossButton).click(); }); it("Chart Widget Functionality To Unchecked Visible Widget", function() { - cy.openPropertyPane("chartwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); cy.get(publish.chartWidget).should("not.be.visible"); cy.get(publish.backToEditor).click(); }); it("Chart Widget Functionality To Check Visible Widget", function() { - cy.openPropertyPane("chartwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); cy.get(publish.chartWidget).should("be.visible"); cy.get(publish.backToEditor).click(); }); it("Chart Widget Functionality To Uncheck Horizontal Scroll Visible", function() { - cy.openPropertyPane("chartwidget"); cy.togglebarDisable(commonlocators.horizontalScroll); cy.PublishtheApp(); cy.get(publish.horizontalTab).should("not.visible"); cy.get(publish.backToEditor).click(); }); it("Chart Widget Functionality To Check Horizontal Scroll Visible", function() { - cy.openPropertyPane("chartwidget"); cy.togglebar(commonlocators.horizontalScroll); cy.PublishtheApp(); cy.get(publish.horizontalTab) diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js index 8a441b1de5..c35bc2f779 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js @@ -2,12 +2,15 @@ const commonlocators = require("../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/displayWidgetDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Image Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); + it("Image Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("imagewidget"); /** * @param{Text} Random Text @@ -40,6 +43,7 @@ describe("Image Widget Functionality", function() { }); it("Image Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("imagewidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -47,6 +51,7 @@ describe("Image Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Image Widget Functionality To Check Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("imagewidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js index e357df923d..216eedd243 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js @@ -2,6 +2,7 @@ const widgetsPage = require("../../../locators/Widgets.json"); const commonlocators = require("../../../locators/commonlocators.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/tableWidgetDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Table Widget Functionality", function() { before(() => { @@ -9,6 +10,7 @@ describe("Table Widget Functionality", function() { }); it("Table Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); /** diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js index c2c55bbd0b..f933a44a04 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js @@ -2,6 +2,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const widgetsPage = require("../../../locators/Widgets.json"); const publishPage = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/displayWidgetDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Text Widget Functionality", function() { before(() => { @@ -9,6 +10,7 @@ describe("Text Widget Functionality", function() { }); beforeEach(() => { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js index 7090e3690c..ee098b0862 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js @@ -66,6 +66,7 @@ describe("Dynamic input autocomplete", () => { .focus(); cy.assertEvaluatedValuePopup("string"); + cy.NavigateToEntityExplorer(); // Test on api pane cy.NavigateToAPI_Panel(); cy.get(apiwidget.createapi).click({ force: true }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js index cc79a1da7d..1b90e5a8ca 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js @@ -3,12 +3,14 @@ const formWidgetsPage = require("../../../locators/FormWidgets.json"); const widgetsPage = require("../../../locators/Widgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/newFormDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Checkbox Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); it("Checkbox Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); /** * @param{Text} Random Text @@ -43,6 +45,7 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Disabled Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebar(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -50,6 +53,7 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Enabled Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebarDisable(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -57,6 +61,7 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Unchecked Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -64,6 +69,7 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js index 15075b9ace..8226d22581 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js @@ -2,6 +2,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); const dsl = require("../../../fixtures/newFormDsl.json"); const publishPage = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); describe("DatePicker Widget Functionality", function() { before(() => { @@ -9,6 +10,7 @@ describe("DatePicker Widget Functionality", function() { }); beforeEach(() => { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("datepickerwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js index b10f187443..8ce22e97e3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js @@ -2,12 +2,14 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/newFormDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Dropdown Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); it("Dropdown Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("dropdownwidget"); /** * @param{Text} Random Text @@ -46,6 +48,7 @@ describe("Dropdown Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Dropdown Functionality To Unchecked Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("dropdownwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -53,6 +56,7 @@ describe("Dropdown Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Dropdown Functionality To Check Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("dropdownwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js index 74cdca8ebd..a4c836c789 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js @@ -1,11 +1,13 @@ const commonlocators = require("../../../locators/commonlocators.json"); const dsl = require("../../../fixtures/newFormDsl.json"); +const pages = require("../../../locators/Pages.json"); describe("FilePicker Widget Functionality", function() { beforeEach(() => { cy.addDsl(dsl); }); it("FilePicker Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("filepickerwidget"); //Checking the edit props for FilePicker and also the properties of FilePicker widget diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js index 21fb841e87..404e09e62b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js @@ -2,12 +2,14 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/formdsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Form Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); it("Form Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("formwidget"); /** * @param{Text} Random Text @@ -43,6 +45,7 @@ describe("Form Widget Functionality", function() { }); it("Form Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("formwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js index 5695b37431..c5bb1ca99f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js @@ -2,12 +2,14 @@ const commonlocators = require("../../../locators/commonlocators.json"); const dsl = require("../../../fixtures/newFormDsl.json"); const widgetsPage = require("../../../locators/Widgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); describe("Input Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); it("Input Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); /** * @param{Text} Random Text @@ -63,6 +65,7 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Widget Functionality To Check Disabled Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebar(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -70,6 +73,7 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Widget Functionality To Check Enabled Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebarDisable(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -77,6 +81,7 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Functionality To Unchecked Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -84,6 +89,7 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Functionality To Check Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js index 22aa93fa10..f6b0cde75a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js @@ -2,11 +2,14 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/newFormDsl.json"); +const pages = require("../../../locators/Pages.json"); + describe("Radio Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); it("Radio Widget Functionality", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("radiogroupwidget"); /** * @param{Text} Random Text @@ -56,6 +59,7 @@ describe("Radio Widget Functionality", function() { }); it("Radio Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("radiogroupwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -63,6 +67,7 @@ describe("Radio Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Radio Functionality To Check Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("radiogroupwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js index 06e3863d20..8dc4c98a98 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js @@ -2,6 +2,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); const dsl = require("../../../fixtures/formdsl1.json"); const publishPage = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); describe("RichTextEditor Widget Functionality", function() { before(() => { @@ -9,6 +10,7 @@ describe("RichTextEditor Widget Functionality", function() { }); beforeEach(() => { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("richtexteditorwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js index 81d6975fbb..7ee578507e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js @@ -3,12 +3,14 @@ const Layoutpage = require("../../../locators/Layout.json"); const widgetsPage = require("../../../locators/Widgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/layoutdsl.json"); +const pages = require("../../../locators/Pages.json"); describe("Tab widget test", function() { before(() => { cy.addDsl(dsl); }); it("Tab Widget Functionality Test", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tabswidget"); /** * @param{Text} Random Text @@ -58,6 +60,7 @@ describe("Tab widget test", function() { }); it("Tab Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tabswidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -65,6 +68,7 @@ describe("Tab widget test", function() { cy.get(publish.backToEditor).click(); }); it("Tab Widget Functionality To Check Visible Widget", function() { + cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tabswidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js index 51139f347f..405f3f02fd 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js @@ -33,10 +33,11 @@ describe("Create a query with a mongo datasource, run, save and then delete the cy.runAndDeleteQuery(); cy.NavigateToDatasourceEditor(); + cy.get(".t--entity-name:contains(MongoDB)").click(); cy.get("@createDatasource").then(httpResponse => { - const datasourceId = httpResponse.response.body.data.id; + const datasourceName = httpResponse.response.body.data.name; - cy.get(`[data-cy=${datasourceId}]`).click(); + cy.get(`.t--entity-name:contains(${datasourceName})`).click(); }); cy.get(".t--delete-datasource").click(); cy.wait("@deleteDatasource").should( diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js index d118e59bcb..08564db509 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js @@ -32,11 +32,13 @@ describe("Create a query with a postgres datasource, run, save and then delete t cy.runAndDeleteQuery(); cy.NavigateToDatasourceEditor(); + cy.get(".t--entity-name:contains(PostgreSQL)").click(); cy.get("@createDatasource").then(httpResponse => { - const datasourceId = httpResponse.response.body.data.id; + const datasourceName = httpResponse.response.body.data.name; - cy.get(`[data-cy=${datasourceId}]`).click(); + cy.get(`.t--entity-name:contains(${datasourceName})`).click(); }); + cy.get(".t--delete-datasource").click(); cy.wait("@deleteDatasource").should( "have.nested.property", diff --git a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js index 104ad5b4e6..47cca0c5e8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js @@ -12,9 +12,7 @@ describe("Login from UI and check the functionality", function() { cy.generateUUID().then(uid => { pageid = uid; cy.Createpage(pageid); - cy.NavigateToWidgets(pageid); - localStorage.setItem("PageName", pageid); - cy.Deletepage(pageid); + cy.DeletepageFromSideBar(); }); cy.wait("@deletePage"); cy.get("@deletePage").should("have.property", "status", 200); diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index cbfad3500e..ddcb77dff4 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -1,5 +1,5 @@ { - "datasourceEditorIcon": ".t--nav-link-datasource-editor", + "datasourceEditorIcon": ".t--entity-name:contains('DataSources)", "host": "input[name='datasourceConfiguration.endpoints[0].host']", "port": "input[name='datasourceConfiguration.endpoints[0].port']", "databaseName": "input[name='datasourceConfiguration.authentication.databaseName']", @@ -12,5 +12,6 @@ "RESTAPI": ".t--plugin-name:contains('REST API')", "PostgreSQL": ".t--plugin-name:contains('PostgreSQL')", "sectionAuthentication": "[data-cy=section-Authentication]", - "sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]" + "sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]", + "addDatasourceEntity": "//div[contains(@class,'t--entity group plugins')]//div[contains(@class,'t--entity-add-btn')]" } diff --git a/app/client/cypress/locators/Pages.json b/app/client/cypress/locators/Pages.json index d856549b4a..0e7e482090 100644 --- a/app/client/cypress/locators/Pages.json +++ b/app/client/cypress/locators/Pages.json @@ -1,12 +1,20 @@ { - "pagesIcon": ".t--nav-link-manage-pages", + "pagesIcon": ".t--entity-name:contains('Pages')", "commonWidgets": ".t--page-sidebar-CommonWidgets", "formWidgets": ".t--page-sidebar-FormWidgets", "viewWidgets": ".t--page-sidebar-ViewWidgets", "widgetsEditor": ".t--nav-link-widgets-editor", - "AddPage": " .t--add-page-btn", + "AddPage": "//div[contains(@class,'t--entity group pages')]//div[contains(@class,'t--entity-add-btn')]", "editInput": "input.bp3-editable-text-input", "Menuaction": ".bp3-overlay-open>.bp3-transition-container", "Delete": ":nth-child(2) > .bp3-menu-item", - "apiEditorIcon": ".t--nav-link-api-editor" + "apiEditorIcon": ".t--nav-link-api-editor", + "addEntityAPI": "//div[contains(@class,'t--entity group apis')]//div[contains(@class,'t--entity-add-btn')]", + "entityWidget": ".t--entity-name:contains('Widgets')", + "entityTable": ".t--entity-name:contains('Table1')", + "entityText": ".t--entity-name:contains('Text1')", + "entityExplorer": ".t--nav-link-entity-explorer", + "popover": "//div[contains(@class,'t--entity page')]//*[local-name()='g' and @id='Icon/Outline/more-vertical']", + "editName": ".single-select >div:contains('Edit Name')", + "deletePage": ".single-select >div:contains('Delete')" } \ No newline at end of file diff --git a/app/client/cypress/locators/QueryEditor.json b/app/client/cypress/locators/QueryEditor.json index 1a7dbc1f0c..e0ddc9214c 100644 --- a/app/client/cypress/locators/QueryEditor.json +++ b/app/client/cypress/locators/QueryEditor.json @@ -3,5 +3,6 @@ "templateMenu": ".t--template-menu", "runQuery": ".t--run-query", "saveQuery": ".t--save-query", - "deleteQuery": ".t--delete-query" + "deleteQuery": ".t--delete-query", + "addQueryEntity": ".//div[contains(@class,'t--entity group queries')]//div[contains(@class,'t--entity-add-btn')]" } diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 263257a37c..1ea1f9b8cb 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -3,9 +3,9 @@ "searchApi": ".t--sidebar input[type=text]", "createapi": ".t--createBlankApiCard", "apiTxt": ".t--action-name-edit-field input", - "popover": ".bp3-popover-target >div>svg", + "popover": "//*[local-name()='g' and @id='Icon/Outline/more-vertical']", "moveTo": ".single-select >div:contains('Move to')", - "copyTo": ".single-select >div:contains('Copy to')", + "copyTo": ".single-select >div:contains('Copy to page')", "home": ".single-select >div:contains('Page1')", "delete": ".single-select >div:contains('Delete')", "path": ".t--path >div textarea", @@ -41,5 +41,6 @@ "content-Type": "(//span[@class='bp3-tree-node-label']/span)[3]", "requestBody": "(//div[contains(@class,'bp3-collapse-body')]//textarea)[1]", "showrequest": "span:contains('Show Request')", - "Responsetab": "//li[text()='Response Body']" + "Responsetab": "//li[text()='Response Body']", + "deleteAPI": ".t--apiFormDeleteBtn" } diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 96d18d397e..6a3b18ed30 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -58,6 +58,8 @@ "selectMenuItem": ".bp3-menu li>a>div", "evaluatedType": ".t--CodeEditor-evaluatedValue>pre", "evaluatedCurrentValue": ".t--CodeEditor-evaluatedValue div pre", + "entityExplorersearch": "#entity-explorer-search", + "entitySearchResult": ".t--entity-name:contains('", "saveStatusContainer": ".t--save-status-container", "saveStatusIsSaving": "t--save-status-is-saving", "saveStatusSuccess": ".t--save-status-success", diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 75b170a643..04b0eeedf8 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -234,16 +234,13 @@ Cypress.Commands.add("DeleteApp", appName => { cy.get(homePage.deleteButton).click({ force: true }); }); -Cypress.Commands.add("Deletepage", Pagename => { - cy.get(pages.pagesIcon).click({ force: true }); - cy.get(".t--page-sidebar-" + Pagename + ""); - cy.get( - ".t--page-sidebar-" + - Pagename + - ">.t--page-sidebar-menu-actions>.bp3-popover-target", - ).click({ force: true }); - cy.get(pages.Menuaction).click({ force: true }); - cy.get(pages.Delete).click({ force: true }); +Cypress.Commands.add("DeletepageFromSideBar", () => { + cy.xpath(pages.popover) + .last() + .click({ force: true }); + cy.get(pages.deletePage) + .first() + .click({ force: true }); cy.wait(2000); }); @@ -281,15 +278,27 @@ Cypress.Commands.add("SearchApp", appname => { // Wait added because after opening the application editor, sometimes it takes a little time. }); -Cypress.Commands.add("SearchAPI", (apiname1, apiname2) => { - cy.get("span:contains(".concat(apiname2).concat(")")).should("be.visible"); - cy.get(apiwidget.searchApi) - .click({ force: true }) - .type(apiname1, { force: true }); - cy.get("span:contains(".concat(apiname1).concat(")")).should("be.visible"); - cy.get("span:contains(".concat(apiname2).concat(")")).should( - "not.be.visible", - ); +Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => { + cy.get(commonlocators.entityExplorersearch).should("be.visible"); + cy.get("#entity-explorer-search") + .clear() + .type(apiname1); + cy.get( + commonlocators.entitySearchResult.concat(apiname1).concat("')"), + ).should("be.visible"); + cy.get( + commonlocators.entitySearchResult.concat(apiname2).concat("')"), + ).should("not.be.visible"); +}); + +Cypress.Commands.add("GlobalSearchEntity", apiname1 => { + cy.get(commonlocators.entityExplorersearch).should("be.visible"); + cy.get("#entity-explorer-search") + .clear() + .type(apiname1); + cy.get( + commonlocators.entitySearchResult.concat(apiname1).concat("')"), + ).should("be.visible"); }); Cypress.Commands.add("ResponseStatusCheck", statusCode => { @@ -303,23 +312,27 @@ Cypress.Commands.add("ResponseCheck", textTocheck => { }); Cypress.Commands.add("NavigateToAPI_Panel", () => { - cy.get(pages.apiEditorIcon) + cy.xpath(pages.addEntityAPI) + .should("be.visible") + .click({ force: true }); + cy.get("#loading").should("not.exist"); +}); + +Cypress.Commands.add("NavigateToEntityExplorer", () => { + cy.get(pages.entityExplorer) .should("be.visible") .click({ force: true }); cy.get("#loading").should("not.exist"); }); Cypress.Commands.add("CreateAPI", apiname => { - cy.get(apiwidget.createApiOnSideBar) - .first() - .click({ force: true }); cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); cy.get(apiwidget.resourceUrl).should("be.visible"); - cy.get(apiwidget.apiTxt).click(); + cy.get(apiwidget.ApiName).click({ force: true }); cy.get(apiwidget.apiTxt) .clear() - .type(apiname) + .type(apiname, { force: true }) .should("have.value", apiname) .blur(); cy.WaitAutoSave(); @@ -343,10 +356,10 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => { Cypress.Commands.add("EditApiName", apiname => { //cy.wait("@getUser"); - cy.get(apiwidget.EditApiName).click(); + cy.get(apiwidget.ApiName).click({ force: true }); cy.get(apiwidget.apiTxt) .clear() - .type(apiname) + .type(apiname, { force: true }) .should("have.value", apiname); cy.WaitAutoSave(); }); @@ -389,16 +402,21 @@ Cypress.Commands.add("SelectAction", action => { }); Cypress.Commands.add("ClearSearch", () => { - cy.get(apiwidget.searchApi).clear(); + cy.get(commonlocators.entityExplorersearch).should("be.visible"); + cy.get(commonlocators.entityExplorersearch).clear(); }); -Cypress.Commands.add("SearchAPIandClick", apiname1 => { - cy.get(apiwidget.searchApi) - .click({ force: true }) - .type(apiname1, { force: true }); - cy.get(".t--sidebar span:contains(".concat(apiname1).concat(")")) - .should("be.visible") - .click({ force: true }); +Cypress.Commands.add("SearchEntityandOpen", apiname1 => { + cy.get(commonlocators.entityExplorersearch).should("be.visible"); + cy.get(commonlocators.entityExplorersearch) + .clear() + .type(apiname1); + cy.get( + commonlocators.entitySearchResult.concat(apiname1).concat("')"), + ).should("be.visible"); + cy.get( + commonlocators.entitySearchResult.concat(apiname1).concat("')"), + ).click({ force: true }); }); Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { @@ -529,16 +547,15 @@ Cypress.Commands.add( ); Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => { - cy.get(apiwidget.createApiOnSideBar) - .first() - .click({ force: true }); + cy.xpath(pages.addEntityAPI).click(); cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); // cy.wait("@getUser"); cy.get(apiwidget.resourceUrl).should("be.visible"); + cy.get(apiwidget.ApiName).click({ force: true }); cy.get(apiwidget.apiTxt) .clear() - .type(apiname) + .type(apiname, { force: true }) .should("have.value", apiname) .focus(); cy.get(".bp3-popover-content").should($x => { @@ -561,8 +578,8 @@ Cypress.Commands.add("MoveAPIToHome", apiname => { }); Cypress.Commands.add("MoveAPIToPage", () => { - cy.get(apiwidget.popover) - .first() + cy.xpath(apiwidget.popover) + .last() .click({ force: true }); cy.get(apiwidget.moveTo).click({ force: true }); cy.get(apiwidget.home).click({ force: true }); @@ -573,9 +590,9 @@ Cypress.Commands.add("MoveAPIToPage", () => { ); }); -Cypress.Commands.add("CopyAPIToHome", apiname => { - cy.get(apiwidget.popover) - .first() +Cypress.Commands.add("CopyAPIToHome", () => { + cy.xpath(apiwidget.popover) + .last() .click({ force: true }); cy.get(apiwidget.copyTo).click({ force: true }); cy.get(apiwidget.home).click({ force: true }); @@ -586,11 +603,27 @@ Cypress.Commands.add("CopyAPIToHome", apiname => { ); }); -Cypress.Commands.add("DeleteAPI", apiname => { - cy.get(apiwidget.popover) - .first() +Cypress.Commands.add("DeleteAPIFromSideBar", () => { + cy.xpath(apiwidget.popover) + .last() .click({ force: true }); cy.get(apiwidget.delete).click({ force: true }); + cy.wait("@deleteAction").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); +}); + +Cypress.Commands.add("DeleteAPI", apiname => { + cy.get(apiwidget.deleteAPI) + .first() + .click({ force: true }); + cy.wait("@deleteAction").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); }); Cypress.Commands.add("CreateModal", () => { @@ -800,7 +833,19 @@ Cypress.Commands.add("DeleteModal", () => { Cypress.Commands.add("Createpage", Pagename => { cy.get(pages.pagesIcon).click({ force: true }); - cy.get(pages.AddPage).click(); + cy.xpath(pages.AddPage) + .first() + .click(); + cy.wait("@createPage").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); + cy.wait(2000); + cy.xpath(pages.popover) + .last() + .click({ force: true }); + cy.get(pages.editName).click({ force: true }); cy.get(pages.editInput) .type(Pagename) .type("{Enter}"); @@ -1022,11 +1067,8 @@ Cypress.Commands.add("tabVerify", (index, text) => { .should("be.visible"); }); -Cypress.Commands.add("NavigateToDatasourceEditor", () => { - cy.get(datasourceEditor.datasourceEditorIcon).click({ force: true }); -}); - Cypress.Commands.add("getPluginFormsAndCreateDatasource", () => { + /* cy.wait("@getPluginForm").should( "have.nested.property", "response.body.responseMeta.status", @@ -1037,10 +1079,11 @@ Cypress.Commands.add("getPluginFormsAndCreateDatasource", () => { "response.body.responseMeta.status", 201, ); + */ }); Cypress.Commands.add("NavigateToApiEditor", () => { - cy.get(pages.apiEditorIcon).click({ force: true }); + cy.xpath(pages.addEntityAPI).click({ force: true }); }); Cypress.Commands.add("testCreateApiButton", () => { @@ -1085,11 +1128,11 @@ Cypress.Commands.add("importCurl", () => { }); Cypress.Commands.add("NavigateToDatasourceEditor", () => { - cy.get(datasourceEditor.datasourceEditorIcon).click({ force: true }); + cy.xpath(datasourceEditor.addDatasourceEntity).click({ force: true }); }); Cypress.Commands.add("NavigateToQueryEditor", () => { - cy.get(queryEditor.queryEditorIcon).click({ force: true }); + cy.xpath(queryEditor.addQueryEntity).click({ force: true }); }); Cypress.Commands.add("testSaveDatasource", () => { @@ -1335,6 +1378,7 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("POST", "/api/v1/users/invite").as("postInvite"); cy.route("GET", "/api/v1/organizations/roles").as("getRoles"); cy.route("GET", "/api/v1/users/me").as("getUser"); + cy.route("POST", "/api/v1/pages").as("createPage"); }); Cypress.Commands.add("alertValidate", text => { @@ -1400,11 +1444,9 @@ Cypress.Commands.add("ValidatePublishTableData", () => { }); Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { + cy.NavigateToEntityExplorer(); cy.NavigateToApiEditor(); - cy.get("div[tabindex='0'] >div>span") - .contains("Api2") - .first() - .click(); + cy.SearchEntityandOpen("Api2"); cy.NavigateToPaginationTab(); cy.RunAPI(); cy.get(ApiEditor.apiPaginationNextTest).click(); diff --git a/app/client/package.json b/app/client/package.json index 02c939c0a1..642ec189c7 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -42,6 +42,7 @@ "@uppy/react": "^1.4.5", "@uppy/url": "^1.3.2", "@uppy/webcam": "^1.3.1", + "@welldone-software/why-did-you-render": "^4.2.5", "algoliasearch": "^4.2.0", "axios": "^0.18.0", "chance": "^1.1.3", diff --git a/app/client/src/AppRouter.tsx b/app/client/src/AppRouter.tsx index dc7cc39c3f..236bf816a2 100644 --- a/app/client/src/AppRouter.tsx +++ b/app/client/src/AppRouter.tsx @@ -48,15 +48,8 @@ class AppRouter extends React.Component { path={ORG_URL} component={OrganizationLoader} name={"Organisation"} - routeProtected - /> - + { path={APPLICATIONS_URL} component={ApplicationListLoader} name={"Home"} - routeProtected /> ({ - type: ReduxActionTypes.SAVE_API_NAME, +export const saveActionName = (payload: { id: string; name: string }) => ({ + type: ReduxActionTypes.SAVE_ACTION_NAME_INIT, payload: payload, }); @@ -189,11 +189,12 @@ export type UpdateActionPropertyActionPayload = { export const updateActionProperty = ( payload: UpdateActionPropertyActionPayload, -) => - batchAction({ +) => { + return batchAction({ type: ReduxActionTypes.UPDATE_ACTION_PROPERTY, payload, }); +}; export default { createAction: createActionRequest, diff --git a/app/client/src/actions/apiPaneActions.ts b/app/client/src/actions/apiPaneActions.ts index a198c3465f..e96dc9c63f 100644 --- a/app/client/src/actions/apiPaneActions.ts +++ b/app/client/src/actions/apiPaneActions.ts @@ -51,7 +51,9 @@ export const createNewApiAction = ( payload: { pageId }, }); -export const createNewQueryAction = (pageId: string): ReduxAction<{}> => ({ +export const createNewQueryAction = ( + pageId: string, +): ReduxAction<{ pageId: string }> => ({ type: ReduxActionTypes.CREATE_NEW_QUERY_ACTION, payload: { pageId }, }); diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts index 0f4fecc33a..3ab9eb39ec 100644 --- a/app/client/src/actions/applicationActions.ts +++ b/app/client/src/actions/applicationActions.ts @@ -27,3 +27,12 @@ export const fetchApplication = (applicationId: string) => { }, }; }; + +export const publishApplication = (applicationId: string) => { + return { + type: ReduxActionTypes.PUBLISH_APPLICATION_INIT, + payload: { + applicationId, + }, + }; +}; diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index 0df3f50d36..8dd8c81701 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -29,6 +29,13 @@ export const changeDatasource = (payload: Datasource) => { }; }; +export const switchDatasource = (id: string) => { + return { + type: ReduxActionTypes.SWITCH_DATASOURCE, + payload: { datasourceId: id }, + }; +}; + export const testDatasource = (payload: Partial) => { return { type: ReduxActionTypes.TEST_DATASOURCE_INIT, diff --git a/app/client/src/actions/explorerActions.ts b/app/client/src/actions/explorerActions.ts new file mode 100644 index 0000000000..4ccaaa134d --- /dev/null +++ b/app/client/src/actions/explorerActions.ts @@ -0,0 +1,10 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; + +export const initExplorerEntityNameEdit = (actionId: string) => { + return { + type: ReduxActionTypes.INIT_EXPLORER_ENTITY_NAME_EDIT, + payload: { + id: actionId, + }, + }; +}; diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index b028769a46..a35faa071b 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -10,6 +10,7 @@ import { } from "constants/ReduxActionConstants"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; +import AnalyticsUtil from "utils/AnalyticsUtil"; import { UrlDataState } from "@appsmith/reducers/entityReducers/urlReducer"; export const fetchPageList = ( @@ -27,7 +28,7 @@ export const fetchPage = (pageId: string): ReduxAction => { return { type: ReduxActionTypes.FETCH_PAGE_INIT, payload: { - pageId, + id: pageId, }, }; }; @@ -91,6 +92,29 @@ export const updateAndSaveLayout = (widgets: FlattenedWidgetProps) => { }; }; +export const createPage = (applicationId: string, pageName: string) => { + AnalyticsUtil.logEvent("CREATE_PAGE", { + pageName, + }); + return { + type: ReduxActionTypes.CREATE_PAGE_INIT, + payload: { + applicationId, + name: pageName, + }, + }; +}; + +export const updatePage = (id: string, name: string) => { + return { + type: ReduxActionTypes.UPDATE_PAGE_INIT, + payload: { + id, + name, + }, + }; +}; + export type WidgetAddChild = { widgetId: string; widgetName?: string; diff --git a/app/client/src/actions/propertyPaneActions.ts b/app/client/src/actions/propertyPaneActions.ts index 29ee4bd1ca..615daa5170 100644 --- a/app/client/src/actions/propertyPaneActions.ts +++ b/app/client/src/actions/propertyPaneActions.ts @@ -3,7 +3,7 @@ export const updateWidgetName = (widgetId: string, newName: string) => { return { type: ReduxActionTypes.UPDATE_WIDGET_NAME_INIT, payload: { - widgetId, + id: widgetId, newName, }, }; diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index fe8cab6a70..aa95a122f2 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -61,3 +61,29 @@ export const focusWidget = ( type: ReduxActionTypes.FOCUS_WIDGET, payload: { widgetId }, }); + +export const showModal = (id: string) => { + return { + type: ReduxActionTypes.SHOW_MODAL, + payload: { + modalId: id, + }, + }; +}; + +export const closeAllModals = () => { + return { + type: ReduxActionTypes.CLOSE_MODAL, + payload: {}, + }; +}; + +export const forceOpenPropertyPane = (id: string) => { + return { + type: ReduxActionTypes.SHOW_PROPERTY_PANE, + payload: { + widgetId: id, + force: true, + }, + }; +}; diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx index ec2477a677..3f8b1ac6ad 100644 --- a/app/client/src/api/ApplicationApi.tsx +++ b/app/client/src/api/ApplicationApi.tsx @@ -46,7 +46,7 @@ export interface CreateApplicationRequest { } export interface SetDefaultPageRequest { - pageId: string; + id: string; applicationId: string; } @@ -87,7 +87,7 @@ class ApplicationApi extends Api { static changeAppViewAccessPath = (applicationId: string) => `${applicationId}/changeAccess`; static setDefaultPagePath = (request: SetDefaultPageRequest) => - `${ApplicationApi.baseURL}${request.applicationId}/page/${request.pageId}/makeDefault`; + `${ApplicationApi.baseURL}${request.applicationId}/page/${request.id}/makeDefault`; static publishApplication( publishApplicationRequest: PublishApplicationRequest, ): AxiosPromise { diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index 4420c4867b..dfd0d150fc 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -6,7 +6,7 @@ import { AxiosPromise } from "axios"; import { PageAction } from "constants/ActionConstants"; export interface FetchPageRequest { - pageId: string; + id: string; } export interface FetchPublishedPageRequest { @@ -75,7 +75,7 @@ export interface FetchPageListResponse extends ApiResponse { } export interface DeletePageRequest { - pageId: string; + id: string; } export interface UpdateWidgetNameRequest { @@ -106,7 +106,7 @@ class PageApi extends Api { static fetchPage( pageRequest: FetchPageRequest, ): AxiosPromise { - return Api.get(PageApi.url + "/" + pageRequest.pageId); + return Api.get(PageApi.url + "/" + pageRequest.id); } static savePage( @@ -147,7 +147,7 @@ class PageApi extends Api { } static deletePage(request: DeletePageRequest): AxiosPromise { - return Api.delete(PageApi.url + "/" + request.pageId); + return Api.delete(PageApi.url + "/" + request.id); } static updateWidgetName( diff --git a/app/client/src/assets/icons/control/collapse.svg b/app/client/src/assets/icons/control/collapse.svg new file mode 100644 index 0000000000..fc7b106736 --- /dev/null +++ b/app/client/src/assets/icons/control/collapse.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/control/more-vertical.svg b/app/client/src/assets/icons/control/more-vertical.svg index 72b6deddf7..e9a7600622 100755 --- a/app/client/src/assets/icons/control/more-vertical.svg +++ b/app/client/src/assets/icons/control/more-vertical.svg @@ -4,10 +4,5 @@ - - - - - diff --git a/app/client/src/assets/icons/menu/api-colored.svg b/app/client/src/assets/icons/menu/api-colored.svg new file mode 100644 index 0000000000..6d202ff595 --- /dev/null +++ b/app/client/src/assets/icons/menu/api-colored.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/menu/datasource-colored.svg b/app/client/src/assets/icons/menu/datasource-colored.svg new file mode 100644 index 0000000000..f779edcee5 --- /dev/null +++ b/app/client/src/assets/icons/menu/datasource-colored.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/menu/explorer.svg b/app/client/src/assets/icons/menu/explorer.svg new file mode 100644 index 0000000000..af233a1ee9 --- /dev/null +++ b/app/client/src/assets/icons/menu/explorer.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/menu/homepage.svg b/app/client/src/assets/icons/menu/homepage.svg index 93d9ef10d1..2c34a49d48 100644 --- a/app/client/src/assets/icons/menu/homepage.svg +++ b/app/client/src/assets/icons/menu/homepage.svg @@ -1,5 +1,4 @@ - diff --git a/app/client/src/assets/icons/menu/page.svg b/app/client/src/assets/icons/menu/page.svg new file mode 100644 index 0000000000..dc184d19ce --- /dev/null +++ b/app/client/src/assets/icons/menu/page.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/menu/widgets-colored.svg b/app/client/src/assets/icons/menu/widgets-colored.svg new file mode 100644 index 0000000000..f28f50fc3e --- /dev/null +++ b/app/client/src/assets/icons/menu/widgets-colored.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/widget/modal.svg b/app/client/src/assets/icons/widget/modal.svg index 1edde136bb..1f1b1ad84b 100644 --- a/app/client/src/assets/icons/widget/modal.svg +++ b/app/client/src/assets/icons/widget/modal.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx index e755ae4ee4..d803c8f988 100644 --- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx +++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx @@ -30,6 +30,7 @@ export const PositionedContainer = (props: PositionedContainerProps) => { width: props.style.componentWidth + (props.style.widthUnit || "px"), padding: padding + "px", }} + id={props.widgetId} //Before you remove: This is used by property pane to reference the element className={ generateClassName(props.widgetId) + diff --git a/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx b/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx index 5f7ad041cc..adfc943841 100644 --- a/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx @@ -23,6 +23,7 @@ export const RichtextEditorComponent = ( props: RichtextEditorComponentProps, ) => { const [editorInstance, setEditorInstance] = useState(null as any); + /* eslint-disable react-hooks/exhaustive-deps */ useEffect(() => { if (editorInstance !== null) { editorInstance.mode.set( @@ -40,7 +41,7 @@ export const RichtextEditorComponent = ( const onChange = debounce(props.onValueChange, 200); (window as any).tinyMCE.init({ height: "100%", - selector: `textarea#${props.widgetId}`, + selector: `textarea#rte-${props.widgetId}`, menubar: false, branding: false, resize: false, @@ -81,7 +82,7 @@ export const RichtextEditorComponent = ( }, []); return ( - + ); }; diff --git a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx index 9c47a6de36..f0d08555aa 100644 --- a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx +++ b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx @@ -25,7 +25,6 @@ import { getDefaultRefinement } from "selectors/helpSelectors"; import { getAppsmithConfigs } from "configs"; const { algolia } = getAppsmithConfigs(); const searchClient = algoliasearch(algolia.apiId, algolia.apiKey); -console.log({ algolia }); const OenLinkIcon = HelpIcons.OPEN_LINK; const DocumentIcon = HelpIcons.DOCUMENT; @@ -220,7 +219,6 @@ const StyledPoweredBy = styled(PoweredBy)` export default function DocumentationSearch(props: { hitsPerPage: number }) { const dispatch = useDispatch(); const defaultRefinement = useSelector(getDefaultRefinement); - console.log({ algolia }); if (!algolia.enabled) return null; return ( diff --git a/app/client/src/components/editorComponents/ActionNameEditor.tsx b/app/client/src/components/editorComponents/ActionNameEditor.tsx index 1583fbda5d..2ce9e57e01 100644 --- a/app/client/src/components/editorComponents/ActionNameEditor.tsx +++ b/app/client/src/components/editorComponents/ActionNameEditor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; import { useParams } from "react-router-dom"; @@ -11,7 +11,7 @@ import { AppState } from "reducers"; import { RestAction } from "entities/Action"; import { Page } from "constants/ReduxActionConstants"; -import { saveApiName } from "actions/actionActions"; +import { saveActionName } from "actions/actionActions"; import { Spinner } from "@blueprintjs/core"; const ApiNameWrapper = styled.div` @@ -67,34 +67,43 @@ export const ActionNameEditor = () => { }; }); - const hasActionNameConflict = (name: string) => - !( - existingPageNames.indexOf(name) === -1 && - actions.findIndex(action => action.name === name) === -1 && - existingWidgetNames.indexOf(name) === -1 - ); + const hasActionNameConflict = useCallback( + (name: string) => + !( + existingPageNames.indexOf(name) === -1 && + actions.findIndex(action => action.name === name) === -1 && + existingWidgetNames.indexOf(name) === -1 + ), + [existingPageNames, actions, existingWidgetNames], + ); - const isInvalidActionName = (name: string): string | boolean => { - if (!name || name.trim().length === 0) { - return "Please enter a valid name"; - } else if ( - name !== currentActionConfig?.name && - hasActionNameConflict(name) - ) { - return `${name} is already being used.`; - } - return false; - }; + const isInvalidActionName = useCallback( + (name: string): string | boolean => { + if (!name || name.trim().length === 0) { + return "Please enter a valid name"; + } else if ( + name !== currentActionConfig?.name && + hasActionNameConflict(name) + ) { + return `${name} is already being used.`; + } + return false; + }, + [currentActionConfig, hasActionNameConflict], + ); - const handleAPINameChange = (name: string) => { - if ( - currentActionConfig && - name !== currentActionConfig?.name && - !isInvalidActionName(name) - ) { - dispatch(saveApiName({ id: currentActionConfig.id, name })); - } - }; + const handleAPINameChange = useCallback( + (name: string) => { + if ( + currentActionConfig && + name !== currentActionConfig?.name && + !isInvalidActionName(name) + ) { + dispatch(saveActionName({ id: currentActionConfig.id, name })); + } + }, + [dispatch, isInvalidActionName, currentActionConfig], + ); useEffect(() => { if (saveStatus.isSaving === false && saveStatus.error === true) { diff --git a/app/client/src/components/editorComponents/Button.tsx b/app/client/src/components/editorComponents/Button.tsx index 243abfe7e3..c44745f0be 100644 --- a/app/client/src/components/editorComponents/Button.tsx +++ b/app/client/src/components/editorComponents/Button.tsx @@ -11,8 +11,10 @@ import { Intent as BlueprintIntent, IconName, MaybeElement, + IButtonProps, } from "@blueprintjs/core"; import { Direction, Directions } from "utils/helpers"; +import { omit } from "lodash"; const outline = css` &&&&&& { @@ -21,14 +23,7 @@ const outline = css` } `; -const buttonStyles = css<{ - outline?: string; - intent?: Intent; - filled?: string; - fluid?: boolean; - skin?: Skin; - iconAlignment?: Direction; -}>` +const buttonStyles = css>` ${BlueprintButtonIntentsCSS} &&&& { padding: ${props => @@ -61,22 +56,20 @@ const buttonStyles = css<{ } ${props => (props.outline ? outline : "")} `; -const StyledButton = styled(BlueprintButton)<{ - outline?: string; - intent?: Intent; - filled?: string; - skin?: Skin; - iconAlignment?: Direction; -}>` +const StyledButton = styled((props: IButtonProps & Partial) => ( + +))` ${buttonStyles} `; -const StyledAnchorButton = styled(BlueprintAnchorButton)<{ - outline?: string; - intent?: Intent; - filled?: string; - skin?: Skin; - iconAlignment?: Direction; -}>` +const StyledAnchorButton = styled( + (props: IButtonProps & Partial) => ( + + ), +)` ${buttonStyles} `; @@ -114,8 +107,8 @@ export const Button = (props: ButtonProps) => { const baseProps = { text: props.text, minimal: !props.filled, - outline: props.outline ? props.outline.toString() : undefined, - filled: props.filled ? props.filled.toString() : undefined, + outline: !!props.outline, + filled: !!props.filled, intent: props.intent as BlueprintIntent, large: props.size === "large", small: props.size === "small", @@ -123,7 +116,7 @@ export const Button = (props: ButtonProps) => { disabled: props.disabled, type: props.type, className: props.className, - fluid: props.fluid ? props.fluid.toString() : undefined, + fluid: !!props.fluid, skin: props.skin, iconAlignment: props.iconAlignment ? props.iconAlignment : undefined, }; diff --git a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx index 553febbce2..c6de15794f 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx @@ -119,9 +119,10 @@ interface PopoverContentProps { onMouseLeave: () => void; } -const CurrentValueViewer = (props: { +export const CurrentValueViewer = (props: { theme: EditorTheme; evaluatedValue: any; + hideLabel?: boolean; }) => { let content = ( {"undefined"} diff --git a/app/client/src/components/editorComponents/ContextDropdown.tsx b/app/client/src/components/editorComponents/ContextDropdown.tsx index 18cc588db5..2a9978ee69 100644 --- a/app/client/src/components/editorComponents/ContextDropdown.tsx +++ b/app/client/src/components/editorComponents/ContextDropdown.tsx @@ -12,6 +12,8 @@ import { DropdownOption } from "widgets/DropdownWidget"; import { ControlIconName, ControlIcons } from "icons/ControlIcons"; import { noop } from "utils/AppsmithUtils"; import { Intent } from "constants/DefaultTheme"; +import { IconProps } from "constants/IconConstants"; +import { Colors } from "constants/Colors"; export type ContextDropdownOption = DropdownOption & { onSelect: (event: React.MouseEvent) => void; @@ -39,6 +41,7 @@ type ContextDropdownProps = { iconSize?: number; text?: string; placeholder?: string; + color?: string; }; }; @@ -74,10 +77,10 @@ export const ContextDropdown = (props: ContextDropdownProps) => { let trigger: ReactNode; if (props.toggle.type === "icon" && props.toggle.icon) { const TriggerElement = ControlIcons[props.toggle.icon]; - const TriggerElementProps = { - style: { display: "flex" }, + const TriggerElementProps: IconProps = { width: props.toggle.iconSize, height: props.toggle.iconSize, + color: props.toggle.color || Colors.SLATE_GRAY, }; trigger = ; } diff --git a/app/client/src/components/editorComponents/Divider.tsx b/app/client/src/components/editorComponents/Divider.tsx index acc7d601e8..9dfaddcba4 100644 --- a/app/client/src/components/editorComponents/Divider.tsx +++ b/app/client/src/components/editorComponents/Divider.tsx @@ -1,7 +1,7 @@ import { Divider } from "@blueprintjs/core"; import styled from "styled-components"; -export const StyledDivider = styled(Divider)` +export const StyledDivider = styled(Divider)<{ color?: string }>` && { margin: 10px 0; } diff --git a/app/client/src/components/editorComponents/DraggableComponent.tsx b/app/client/src/components/editorComponents/DraggableComponent.tsx index c867fac986..e19f621007 100644 --- a/app/client/src/components/editorComponents/DraggableComponent.tsx +++ b/app/client/src/components/editorComponents/DraggableComponent.tsx @@ -53,13 +53,13 @@ const DraggableComponent = (props: DraggableComponentProps) => { // This state tells us which widget is selected // The value is the widgetId of the selected widget const selectedWidget = useSelector( - (state: AppState) => state.ui.editor.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.selectedWidget, ); // This state tels us which widget is focused // The value is the widgetId of the focused widget. const focusedWidget = useSelector( - (state: AppState) => state.ui.editor.focusedWidget, + (state: AppState) => state.ui.widgetDragResize.focusedWidget, ); // This state tells us whether a `ResizableComponent` is resizing diff --git a/app/client/src/components/editorComponents/DropTargetComponent.tsx b/app/client/src/components/editorComponents/DropTargetComponent.tsx index 403d0b63ec..2c642ccbef 100644 --- a/app/client/src/components/editorComponents/DropTargetComponent.tsx +++ b/app/client/src/components/editorComponents/DropTargetComponent.tsx @@ -5,6 +5,7 @@ import React, { Context, createContext, useEffect, + memo, } from "react"; import styled from "styled-components"; import { useDrop, XYCoord, DropTargetMonitor } from "react-dnd"; @@ -30,6 +31,7 @@ import { useWidgetSelection, useCanvasSnapRowsUpdateHook, } from "utils/hooks/dragResizeHooks"; +import { getOccupiedSpaces } from "selectors/editorSelectors"; type DropTargetComponentProps = WidgetProps & { children?: ReactNode; @@ -70,15 +72,15 @@ export const DropTargetContext: Context<{ persistDropTargetRows?: (widgetId: string, row: number) => void; }> = createContext({}); -export const DropTargetComponent = (props: DropTargetComponentProps) => { +export const DropTargetComponent = memo((props: DropTargetComponentProps) => { const canDropTargetExtend = props.canExtend; const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend); - const { updateWidget, occupiedSpaces } = useContext(EditorContext); - + const { updateWidget } = useContext(EditorContext); + const occupiedSpaces = useSelector(getOccupiedSpaces); const selectedWidget = useSelector( - (state: AppState) => state.ui.editor.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.selectedWidget, ); const isResizing = useSelector( (state: AppState) => state.ui.widgetDragResize.isResizing, @@ -180,9 +182,14 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => { // Only show propertypane if this is a new widget. // If it is not a new widget, then let the DraggableComponent handle it. - showPropertyPane && - updateWidgetParams.payload.newWidgetId && - showPropertyPane(updateWidgetParams.payload.newWidgetId); + // Give evaluations a second to complete. + setTimeout( + () => + showPropertyPane && + updateWidgetParams.payload.newWidgetId && + showPropertyPane(updateWidgetParams.payload.newWidgetId), + 100, + ); // Select the widget if it is a new widget selectWidget && selectWidget(widget.widgetId); @@ -285,6 +292,6 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => { ); -}; +}); export default DropTargetComponent; diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx index f2e1f7c238..f7a8f35cf8 100644 --- a/app/client/src/components/editorComponents/EditableText.tsx +++ b/app/client/src/components/editorComponents/EditableText.tsx @@ -7,6 +7,7 @@ import styled from "styled-components"; import _ from "lodash"; import Edit from "assets/images/EditPen.svg"; import ErrorTooltip from "./ErrorTooltip"; +import { Colors } from "constants/Colors"; export enum EditInteractionKind { SINGLE, @@ -26,6 +27,8 @@ type EditableTextProps = { isInvalid?: (value: string) => string | boolean; editInteractionKind: EditInteractionKind; hideEditIcon?: boolean; + minimal?: boolean; + onBlur?: (value?: string) => void; }; const EditPen = styled.img` @@ -35,19 +38,26 @@ const EditPen = styled.img` } `; -const EditableTextWrapper = styled.div<{ isEditing: boolean }>` +const EditableTextWrapper = styled.div<{ + isEditing: boolean; + minimal: boolean; +}>` && { display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; & .${Classes.EDITABLE_TEXT} { - border: ${props => (props.isEditing ? "1px solid #ccc" : "none")}; + border: ${props => + props.isEditing && !props.minimal + ? `1px solid ${Colors.HIT_GRAY}` + : "none"}; cursor: pointer; - padding: 5px 5px; + padding: ${props => (!props.minimal ? "5px 5px" : "0px")}; text-transform: none; flex: 1 0 100%; max-width: 100%; + overflow: hidden; display: flex; &:before, &:after { @@ -61,11 +71,16 @@ const EditableTextWrapper = styled.div<{ isEditing: boolean }>` } `; -const TextContainer = styled.div<{ isValid: boolean }>` +const TextContainer = styled.div<{ isValid: boolean; minimal: boolean }>` display: flex; &&&& .bp3-editable-text { - border-radius: 3px; - border-color: ${props => (props.isValid ? "hsl(0,0%,80%)" : "red")}; + ${props => (!props.minimal ? "border-radius: 3px;" : "")} + ${props => + !props.minimal + ? `border-color: ${props.isValid ? Colors.HIT_GRAY : "red"}` + : ""}; + ${props => + props.minimal ? `border-bottom: 1px solid ${Colors.HIT_GRAY}` : ""} } `; @@ -79,7 +94,7 @@ export const EditableText = (props: EditableTextProps) => { useEffect(() => { if (props.forceDefault === true) setValue(props.defaultValue); - }, [props.forceDefault]); + }, [props.forceDefault, props.defaultValue]); const edit = (e: any) => { setIsEditing(true); @@ -87,6 +102,7 @@ export const EditableText = (props: EditableTextProps) => { e.stopPropagation(); }; const onChange = (_value: string) => { + props.onBlur && props.onBlur(); const isInvalid = props.isInvalid ? props.isInvalid(_value) : false; if (!isInvalid) { props.onTextChanged(_value); @@ -115,9 +131,10 @@ export const EditableText = (props: EditableTextProps) => { onClick={ props.editInteractionKind === EditInteractionKind.SINGLE ? edit : _.noop } + minimal={!!props.minimal} > - + { value={value} placeholder={props.placeholder} className={props.className} + onCancel={props.onBlur} /> - {!props.hideEditIcon && !props.updating && !isEditing && ( - - )} + {!props.minimal && + !props.hideEditIcon && + !props.updating && + !isEditing && } diff --git a/app/client/src/components/editorComponents/EditorContextProvider.tsx b/app/client/src/components/editorComponents/EditorContextProvider.tsx index cf4e88c946..e6ce8d84c2 100644 --- a/app/client/src/components/editorComponents/EditorContextProvider.tsx +++ b/app/client/src/components/editorComponents/EditorContextProvider.tsx @@ -1,8 +1,6 @@ import React, { Context, createContext, ReactNode } from "react"; import { connect } from "react-redux"; -import { AppState } from "reducers"; - import { WidgetOperation } from "widgets/BaseWidget"; import { updateWidget } from "actions/pageActions"; @@ -13,7 +11,6 @@ import { ExecuteActionPayload } from "constants/ActionConstants"; import { RenderModes } from "constants/WidgetConstants"; import { OccupiedSpace } from "constants/editorConstants"; -import { getOccupiedSpaces } from "selectors/editorSelectors"; import { resetChildrenMetaProperty, updateWidgetMetaProperty, @@ -52,7 +49,6 @@ const EditorContextProvider = (props: EditorContextProviderProps) => { updateWidget, updateWidgetProperty, updateWidgetMetaProperty, - occupiedSpaces, disableDrag, children, resetChildrenMetaProperty, @@ -64,7 +60,6 @@ const EditorContextProvider = (props: EditorContextProviderProps) => { updateWidget, updateWidgetProperty, updateWidgetMetaProperty, - occupiedSpaces, disableDrag, resetChildrenMetaProperty, }} @@ -74,18 +69,6 @@ const EditorContextProvider = (props: EditorContextProviderProps) => { ); }; -/** - * TODO: If a property is created here, it is only available - * in editor mode. If you need a property in published app, it - * has to be copied in src/pages/AppViewer/index.tsx file as well. - * Rework to avoid duplicating the property. - */ -const mapStateToProps = (state: AppState) => { - return { - occupiedSpaces: getOccupiedSpaces(state), - }; -}; - const mapDispatchToProps = (dispatch: any) => { return { updateWidgetProperty: ( @@ -122,7 +105,4 @@ const mapDispatchToProps = (dispatch: any) => { }; }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(EditorContextProvider); +export default connect(null, mapDispatchToProps)(EditorContextProvider); diff --git a/app/client/src/components/editorComponents/HighlightedCode/index.tsx b/app/client/src/components/editorComponents/HighlightedCode/index.tsx index 711289c62c..51c8b6176d 100644 --- a/app/client/src/components/editorComponents/HighlightedCode/index.tsx +++ b/app/client/src/components/editorComponents/HighlightedCode/index.tsx @@ -1,54 +1,82 @@ -import React, { useRef, useEffect, MutableRefObject } from "react"; +import React, { + useRef, + useEffect, + MutableRefObject, + forwardRef, + Ref, +} from "react"; import styled from "styled-components"; import Prism from "prismjs"; import themes from "./themes"; +import { Skin } from "constants/DefaultTheme"; -// TODO(abhinav): See if this can be re-used in other places. -export enum SKINS { - LIGHT = "LIGHT", - DARK = "DARK", -} +// TODO(abhinav): This is rudimentary. Enhance it. +Prism.languages["appsmith-binding"] = { + punctuation: { + pattern: /^{{|}}$/, + }, + property: { + pattern: /(\.\w+)/, + }, +}; -const StyledCode = styled.div<{ skin: SKINS }>` - ${props => (props.skin === SKINS.DARK ? themes.DARK : themes.LIGHT)} +const StyledCode = styled.div<{ skin: Skin }>` + position: relative; + ${props => (props.skin === Skin.DARK ? themes.DARK : themes.LIGHT)}; + padding: 0 0px; + + } `; /* When adding an entry please make sure to include it in the craco.common.config.js as well */ export enum SYNTAX_HIGHLIGHTING_SUPPORTED_LANGUAGES { - JAVASCRIPT = "language-javascript", // Please note that we're using the CSS class name required by prismjs. + JAVASCRIPT = "language-javascript", + APPSMITH = "language-appsmith-binding", // Please note that we're using the CSS class name required by prismjs. } type HighlightedCodeProps = { codeText: string; language?: SYNTAX_HIGHLIGHTING_SUPPORTED_LANGUAGES; - skin?: SKINS; + skin?: Skin; + multiline?: boolean; + onClick?: () => void; + className?: string; }; +/* eslint-disable react/display-name */ +export const HighlightedCode = forwardRef( + (props: HighlightedCodeProps, ref: Ref) => { + const codeRef: MutableRefObject = useRef(null); -export const HighlightedCode = (props: HighlightedCodeProps) => { - const codeBlockRef: MutableRefObject = useRef(null); + // Highlight when component renders with new props. + // Skin is irrelevant here, as it only uses css. + // Skinning is handled in StyledCode component. + useEffect(() => { + if (codeRef.current) { + // When this is run, the code text is tokenized + // into HTML on which the theme CSS is applied + Prism.highlightElement(codeRef.current); + } + }, [props.codeText, props.language, codeRef]); - // Highlight when component renders with new props. - // Skin is irrelevant here, as it only uses css. - // Skinning is handled in StyledCode component. - useEffect(() => { - if (codeBlockRef.current) { - // When this is run, the code text is tokenized - // into HTML on which the theme CSS is applied - Prism.highlightElement(codeBlockRef.current); - } - }, [props.codeText, props.language]); + // Set the default language to javascript if not provided. + const language = + props.language || SYNTAX_HIGHLIGHTING_SUPPORTED_LANGUAGES.JAVASCRIPT; - // Set the default language to javascript if not provided. - const language = - props.language || SYNTAX_HIGHLIGHTING_SUPPORTED_LANGUAGES.JAVASCRIPT; - - return ( - - - {props.codeText} - - - ); -}; + return ( + + {!props.multiline && ( + + {props.codeText} + + )} + + ); + }, +); export default HighlightedCode; diff --git a/app/client/src/components/editorComponents/HighlightedCode/themes.ts b/app/client/src/components/editorComponents/HighlightedCode/themes.ts index 4b545fc169..f38d156524 100644 --- a/app/client/src/components/editorComponents/HighlightedCode/themes.ts +++ b/app/client/src/components/editorComponents/HighlightedCode/themes.ts @@ -243,7 +243,7 @@ export const DARK = css` .token.constant, .token.symbol, .token.builtin { - color: hsl(53, 89%, 79%); /* #F9EE98 */ + color: #29cca3; } .token.attr-name, diff --git a/app/client/src/components/editorComponents/NavBarItem.tsx b/app/client/src/components/editorComponents/NavBarItem.tsx index b70712eda2..58a05506d8 100644 --- a/app/client/src/components/editorComponents/NavBarItem.tsx +++ b/app/client/src/components/editorComponents/NavBarItem.tsx @@ -2,30 +2,22 @@ import React from "react"; import styled from "styled-components"; import { NavLink } from "react-router-dom"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import NotificationIcon from "components/designSystems/appsmith/NotificationIcon"; -import { theme } from "constants/DefaultTheme"; +import { Colors } from "constants/Colors"; type MenuBarItemProps = { icon: Function; path: string; title: string; - exact: boolean; + exact?: boolean; width: number; height: number; external?: boolean; className?: string; highlight?: boolean; onClick?: Function; + isActive: (currentPath: string, expectedPath: string) => boolean; }; -// const AnmiatedNotificationIcon = - -const StyledNotificationIcon = styled(NotificationIcon)` - position: absolute; - top: -4px; - right: -3px; -`; - type Props = MenuBarItemProps; const IconContainer = styled.div<{ @@ -39,8 +31,8 @@ const IconContainer = styled.div<{ margin-bottom: 5px; background-color: ${props => props.theme.colors.menuButtonBGInactive}; border-radius: ${props => props.theme.radii[1]}px; - height: ${props => props.height}px; - width: ${props => props.width}px; + width: ${props => props.width + 8}px; + height: ${props => props.width + 8}px; svg path { fill: ${props => props.theme.colors.menuIconColorInactive}; } @@ -57,16 +49,14 @@ const ItemContainer = styled.div` color: ${props => props.theme.colors.textOnDarkBG}; font-size: ${props => props.theme.fontSizes[1]}px; cursor: pointer; - background-color: ${props => props.theme.colors.navBG}; &:hover { - background-color: ${props => props.theme.colors.paneBG}; + background: ${Colors.TUNDORA}; text-decoration: none; } - color: ${props => props.theme.colors.menuButtonBGInactive}; &.active { - background-color: ${props => props.theme.colors.paneBG}; + background: ${Colors.TUNDORA}; color: ${props => props.theme.colors.textOnDarkBG}; - ${IconContainer} { + & > div { background-color: ${props => props.theme.colors.primaryOld}; svg path { fill: ${props => props.theme.colors.textOnDarkBG}; @@ -82,30 +72,6 @@ const ItemContainer = styled.div` } `; -const Anchor = styled.a` - width: 64px; - display: inline-block; -`; - -const ExternalLink = function(props: any) { - return ( - { - props.onClick && props.onClick(); - }} - href={props.to} - className={props.className} - target="_blank" - > - {props.children} - - ); -}; - -const DetailsContainer = styled.div` - position: relative; -`; - class NavBarItem extends React.Component { render(): React.ReactNode { const { @@ -115,16 +81,18 @@ class NavBarItem extends React.Component { exact, width, height, - external, - highlight, onClick, + isActive, } = this.props; - const Link = external ? ExternalLink : NavLink; + return ( - { + return isActive(path, location.pathname); + }} className={this.props.className} onClick={() => { onClick && onClick(); @@ -133,21 +101,11 @@ class NavBarItem extends React.Component { }); }} > - - - {icon({ width: width - 8, height: height - 8 })} - - {title} - {highlight && ( - - )} - - + + {icon({ width, height })} + + {title} + ); } diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index 9664088399..c3e87adbdc 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useRef } from "react"; +import React, { useContext, useRef, memo } from "react"; import { XYCoord } from "react-dnd"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; @@ -38,16 +38,19 @@ import { import AnalyticsUtil from "utils/AnalyticsUtil"; import { scrollElementIntoParentCanvasView } from "utils/helpers"; import { getNearestParentCanvas } from "utils/generators"; +import { getOccupiedSpaces } from "selectors/editorSelectors"; export type ResizableComponentProps = ContainerWidgetProps & { paddingOffset: number; }; /* eslint-disable react/display-name */ -export const ResizableComponent = (props: ResizableComponentProps) => { +export const ResizableComponent = memo((props: ResizableComponentProps) => { const resizableRef = useRef(null); // Fetch information from the context - const { updateWidget, occupiedSpaces } = useContext(EditorContext); + const { updateWidget } = useContext(EditorContext); + const occupiedSpaces = useSelector(getOccupiedSpaces); + const { updateDropTargetRows, persistDropTargetRows } = useContext( DropTargetContext, ); @@ -56,10 +59,10 @@ export const ResizableComponent = (props: ResizableComponentProps) => { const { selectWidget } = useWidgetSelection(); const { setIsResizing } = useWidgetDragResize(); const selectedWidget = useSelector( - (state: AppState) => state.ui.editor.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.selectedWidget, ); const focusedWidget = useSelector( - (state: AppState) => state.ui.editor.focusedWidget, + (state: AppState) => state.ui.widgetDragResize.focusedWidget, ); const isDragging = useSelector( @@ -275,6 +278,6 @@ export const ResizableComponent = (props: ResizableComponentProps) => { ); -}; +}); export default ResizableComponent; diff --git a/app/client/src/components/editorComponents/Sidebar.tsx b/app/client/src/components/editorComponents/Sidebar.tsx index cb94cdd65e..ca199e45ba 100644 --- a/app/client/src/components/editorComponents/Sidebar.tsx +++ b/app/client/src/components/editorComponents/Sidebar.tsx @@ -1,108 +1,32 @@ -import React from "react"; -import { Switch } from "react-router"; +import React, { memo } from "react"; +import { Switch, Route } from "react-router"; import styled from "styled-components"; -import { - API_EDITOR_URL, - BUILDER_URL, - API_EDITOR_ID_URL, - PAGE_LIST_EDITOR_URL, - DATA_SOURCES_EDITOR_URL, - DATA_SOURCES_EDITOR_ID_URL, - QUERIES_EDITOR_URL, - QUERIES_EDITOR_ID_URL, - getCurlImportPageURL, - API_EDITOR_URL_WITH_SELECTED_PAGE_ID, - getProviderTemplatesURL, -} from "constants/routes"; - +import { WIDGETS_URL } from "constants/routes"; import WidgetSidebar from "pages/Editor/WidgetSidebar"; -import QuerySidebar from "pages/Editor/QuerySidebar"; -import DataSourceSidebar from "pages/Editor/DataSourceSidebar"; -import ApiSidebar from "pages/Editor/ApiSidebar"; -import PageListSidebar from "pages/Editor/PageListSidebar"; -import AppRoute from "pages/common/AppRoute"; +import ExplorerSidebar from "pages/Editor/Explorer"; const SidebarWrapper = styled.div` - background-color: ${props => props.theme.colors.paneBG}; - padding: 5px 0; + padding: 0px 0 0 6px; color: ${props => props.theme.colors.textOnDarkBG}; overflow-y: auto; `; -export const Sidebar = () => { +export const Sidebar = memo(() => { return ( - - - - - - - - - - - - + ); -}; +}); + +Sidebar.displayName = "Sidebar"; export default Sidebar; diff --git a/app/client/src/components/editorComponents/Tooltip.tsx b/app/client/src/components/editorComponents/Tooltip.tsx new file mode 100644 index 0000000000..e6eef0d43f --- /dev/null +++ b/app/client/src/components/editorComponents/Tooltip.tsx @@ -0,0 +1,2 @@ +import { Tooltip } from "@blueprintjs/core"; +export default Tooltip; diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx index 5ed7e48332..b0b0034a12 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx @@ -53,10 +53,10 @@ export const WidgetNameComponent = (props: WidgetNameComponentProps) => { (state: AppState) => state.ui.propertyPane, ); const selectedWidget = useSelector( - (state: AppState) => state.ui.editor.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.selectedWidget, ); const focusedWidget = useSelector( - (state: AppState) => state.ui.editor.focusedWidget, + (state: AppState) => state.ui.widgetDragResize.focusedWidget, ); const isResizing = useSelector( diff --git a/app/client/src/components/editorComponents/actioncreator/TreeDropdown.tsx b/app/client/src/components/editorComponents/actioncreator/TreeDropdown.tsx index 0453b5be3f..1b90c4f652 100644 --- a/app/client/src/components/editorComponents/actioncreator/TreeDropdown.tsx +++ b/app/client/src/components/editorComponents/actioncreator/TreeDropdown.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import _ from "lodash"; +import React, { useState } from "react"; +import { find, noop } from "lodash"; import { DropdownOption } from "widgets/DropdownWidget"; import { StyledPopover, @@ -11,6 +11,7 @@ import { Button as BlueprintButton, PopoverInteractionKind, PopoverPosition, + IPopoverSharedProps, } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; @@ -33,6 +34,8 @@ type TreeDropdownProps = { ) => React.ReactNode; displayValue?: string; toggle?: React.ReactNode; + className?: string; + modifiers?: IPopoverSharedProps["modifiers"]; }; function getSelectedOption( @@ -50,7 +53,7 @@ function getSelectedOption( if (option.value === selectedValue) { selectedOption = option; } else { - const childOption = _.find(option.children, { + const childOption = find(option.children, { value: selectedValue, }); if (childOption) { @@ -78,6 +81,8 @@ export default function TreeDropdown(props: TreeDropdownProps) { optionTree, ); + const [isOpen, setIsOpen] = useState(false); + const handleSelect = (option: TreeDropdownOption) => { if (option.onSelect) { option.onSelect(option, props.onSelect); @@ -97,13 +102,22 @@ export default function TreeDropdown(props: TreeDropdownProps) { active={isSelected} key={option.value} icon={option.id === "create" ? "plus" : undefined} - onClick={option.children ? _.noop : () => handleSelect(option)} + onClick={ + option.children + ? noop + : (e: any) => { + handleSelect(option); + setIsOpen(false); + e.stopPropagation(); + } + } text={option.label} intent={option.intent} popoverProps={{ minimal: true, interactionKind: PopoverInteractionKind.CLICK, position: PopoverPosition.RIGHT, + targetProps: { onClick: (e: any) => e.stopPropagation() }, }} > {option.children && option.children.map(renderTreeOption)} @@ -130,10 +144,21 @@ export default function TreeDropdown(props: TreeDropdownProps) { ); return ( { + setIsOpen(false); + }} + targetProps={{ + onClick: (e: any) => { + setIsOpen(true); + e.stopPropagation(); + }, + }} > {toggle ? toggle : defaultToggle} diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index cd84bdde1e..5183022b09 100644 --- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { FieldArray, WrappedFieldArrayProps } from "redux-form"; import styled from "styled-components"; import { Icon } from "@blueprintjs/core"; @@ -20,14 +20,6 @@ const FormRowWithLabel = styled(FormRow)` `; const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { - useEffect(() => { - // Always maintain 2 rows - if (props.fields.length < 2 && props.pushFields !== false) { - for (let i = props.fields.length; i < 2; i += 1) { - props.fields.push({ key: "", value: "" }); - } - } - }, [props.fields, props.pushFields]); return ( {props.fields.length && ( @@ -152,7 +144,6 @@ type Props = { const KeyValueFieldArray = (props: Props) => { return ( ({ label: method, diff --git a/app/client/src/constants/Colors.tsx b/app/client/src/constants/Colors.tsx index b29a136d55..ef7d49f226 100644 --- a/app/client/src/constants/Colors.tsx +++ b/app/client/src/constants/Colors.tsx @@ -19,9 +19,14 @@ export const Colors: Record = { BLACK: "#000000", BLACK_PEARL: "#040627", + CODE_GRAY: "#090707", SHARK: "#21282C", + SHARK2: "#232324", + MINE_SHAFT: "#262626", DEEP_SPACE: "#272E32", OUTER_SPACE: "#363E44", + TUNDORA: "#404040", + DOVE_GRAY: "#6D6D6D", SLATE_GRAY: "#768896", PORCELAIN: "#EBEEF0", HIT_GRAY: "#A1ACB3", @@ -33,6 +38,7 @@ export const Colors: Record = { GREEN: "#29CCA3", JUNGLE_GREEN: "#24BA91", JUNGLE_GREEN_DARKER: "#30A481", + EUCALYPTUS: "#218358", RED: "#CE4257", ERROR_RED: "#E22C2C", PURPLE: "#6871EF", @@ -48,6 +54,7 @@ export const Colors: Record = { BLUE_CHARCOAL: "#23292E", TROUT: "#4C565E", JAFFA_DARK: "#EF7541", + TIA_MARIA: "#CB4810", SOLID_MERCURY: "#E5E5E5", TROUT_DARK: "#535B62", ALABASTER: "#F9F8F8", diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 091511025a..146c08a152 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -2,7 +2,6 @@ import * as styledComponents from "styled-components"; import { Colors, Color } from "./Colors"; import * as FontFamilies from "./Fonts"; import tinycolor from "tinycolor2"; -import _ from "lodash"; import { Classes } from "@blueprintjs/core"; import { AlertIcons } from "icons/AlertIcons"; import { IconProps } from "constants/IconConstants"; @@ -42,6 +41,25 @@ export enum Skin { DARK, } +export const scrollbarDark = css` + scrollbar-color: ${props => props.theme.colors.paneCard} + ${props => props.theme.colors.paneBG}; + scrollbar-width: thin; + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 6px + ${props => getColorWithOpacity(props.theme.colors.paneBG, 0.3)}; + } + + &::-webkit-scrollbar-thumb { + background-color: ${props => props.theme.colors.paneCard}; + border-radius: ${props => props.theme.radii[1]}px; + } +`; + export const BlueprintControlTransform = css` && { .${Classes.CONTROL} { @@ -366,9 +384,11 @@ export const getColorWithOpacity = (color: Color, opacity: number) => { export const getBorderCSSShorthand = (border?: ThemeBorder): string => { const values: string[] = []; - _.forIn(border, (value, key) => { - values.push(key === "thickness" ? value + "px" : value); - }); + if (border) { + for (const [key, value] of Object.entries(border)) { + values.push(key === "thickness" ? value + "px" : value.toString()); + } + } return values.join(" "); }; @@ -570,12 +590,8 @@ export const theme: Theme = { cmBacground: Colors.BLUE_CHARCOAL, lightningborder: Colors.ALABASTER, }, - lineHeights: [0, 14, 18, 22, 24, 28, 36, 48, 64, 80], - fonts: [ - FontFamilies.DMSans, - FontFamilies.AppsmithWidget, - FontFamilies.FiraCode, - ], + lineHeights: [0, 14, 16, 18, 22, 24, 28, 36, 48, 64, 80], + fonts: [FontFamilies.DMSans, FontFamilies.FiraCode], borders: [ { thickness: 1, diff --git a/app/client/src/constants/Explorer.ts b/app/client/src/constants/Explorer.ts new file mode 100644 index 0000000000..2ad4fb114b --- /dev/null +++ b/app/client/src/constants/Explorer.ts @@ -0,0 +1,2 @@ +export const ENTITY_EXPLORER_SEARCH_ID = "entity-explorer-search"; +export const ENTITY_EXPLORER_SEARCH_LOCATION_HASH = "#search"; diff --git a/app/client/src/constants/Fonts.tsx b/app/client/src/constants/Fonts.tsx index 7bfd67d343..5dac83c6b7 100644 --- a/app/client/src/constants/Fonts.tsx +++ b/app/client/src/constants/Fonts.tsx @@ -1,5 +1,4 @@ export const DMSans = "DM Sans"; -export const AppsmithWidget = "widget-icons"; export const FiraCode = '"Fira code", "Fira Mono", monospace'; export const HomePageRedesign = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; diff --git a/app/client/src/constants/IconConstants.tsx b/app/client/src/constants/IconConstants.tsx index 744212b529..d33eff4690 100644 --- a/app/client/src/constants/IconConstants.tsx +++ b/app/client/src/constants/IconConstants.tsx @@ -8,6 +8,7 @@ export type IconProps = { background?: Color; onClick?: (e?: any) => void; className?: string; + keepColors?: boolean; }; export const IconWrapper = styled.div` @@ -20,11 +21,13 @@ export const IconWrapper = styled.div` svg { width: ${props => props.width || props.theme.fontSizes[7]}px; height: ${props => props.height || props.theme.fontSizes[7]}px; - path { - fill: ${props => props.color || props.theme.colors.textOnDarkBG}; + ${props => + !props.keepColors + ? `path { + fill: ${props.color || props.theme.colors.textOnDarkBG}; } circle { - fill: ${props => props.background || props.theme.colors.paneBG}; - } - } + fill: ${props.background || props.theme.colors.paneBG}; + }` + : ""} `; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index f1c8fd9e0c..c7b20c0149 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -232,13 +232,20 @@ export const ReduxActionTypes: { [key: string]: string } = { CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR", SET_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT", SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN", - SAVE_API_NAME: "SAVE_API_NAME", - SAVE_API_NAME_SUCCESS: "SAVE_API_NAME_SUCCESS", + SAVE_ACTION_NAME_INIT: "SAVE_ACTION_NAME_INIT", + SAVE_ACTION_NAME_SUCCESS: "SAVE_ACTION_NAME_SUCCESS", UPDATE_API_NAME_DRAFT: "UPDATE_API_NAME_DRAFT", SET_ACTION_PROPERTY: "SET_ACTION_PROPERTY", UPDATE_ACTION_PROPERTY: "UPDATE_ACTION_PROPERTY", + SWITCH_DATASOURCE: "SWITCH_DATASOURCE", + INIT_EXPLORER_ENTITY_NAME_EDIT: "INIT_EXPLORER_ENTITY_NAME_EDIT", FETCH_ACTIONS_VIEW_MODE_INIT: "FETCH_ACTIONS_VIEW_MODE_INIT", FETCH_ACTIONS_VIEW_MODE_SUCCESS: "FETCH_ACTIONS_VIEW_MODE_SUCCESS", + END_EXPLORER_ENTITY_NAME_EDIT: "END_EXPLORER_ENTITY_NAME_EDIT", + POPULATE_PAGEDSLS_INIT: "POPULATE_PAGEDSLS_INIT", + POPULATE_PAGEDSLS_SUCCESS: "POPULATE_PAGEDSLS_SUCCESS", + FETCH_PAGE_DSL_INIT: "FETCH_PAGE_DSL_INIT", + FETCH_PAGE_DSL_SUCCESS: "FETCH_PAGE_DSL_SUCCESS", SET_URL_DATA: "SET_URL_DATA", }; @@ -319,11 +326,14 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { CREATE_MODAL_ERROR: "CREATE_MODAL_ERROR", FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_ERROR: "FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_ERROR", - SAVE_API_NAME_ERROR: "SAVE_API_NAME_ERROR", + SAVE_ACTION_NAME_ERROR: "SAVE_ACTION_NAME_ERROR", FETCH_USER_APPLICATIONS_ORGS_ERROR: "FETCH_USER_APPLICATIONS_ORGS_ERROR", FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR", FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR", FETCH_ACTIONS_VIEW_MODE_ERROR: "FETCH_ACTION_VIEW_MODE_ERROR", + SAVE_API_NAME_ERROR: "SAVE_API_NAME_ERROR", + POPULATE_PAGEDSLS_ERROR: "POPULATE_PAGEDSLS_ERROR", + FETCH_PAGE_DSL_ERROR: "FETCH_PAGE_DSL_ERROR", }; export const ReduxFormActionTypes: { [key: string]: string } = { diff --git a/app/client/src/constants/routes.ts b/app/client/src/constants/routes.ts index d909b4c6df..e5cffa850b 100644 --- a/app/client/src/constants/routes.ts +++ b/app/client/src/constants/routes.ts @@ -1,5 +1,3 @@ -import { MenuIcons } from "icons/MenuIcons"; - export const BASE_URL = "/"; export const ORG_URL = "/org"; export const PAGE_NOT_FOUND_URL = "/404"; @@ -51,6 +49,19 @@ export const BUILDER_PAGE_URL = ( ); }; +export const WIDGETS_URL = ( + applicationId = ":applicationId", + pageId = ":pageId", + params?: Record, +): string => { + if (!pageId) return APPLICATIONS_URL; + const queryParams = convertToQueryParams(params); + return ( + `${BUILDER_BASE_URL(applicationId)}/pages/${pageId}/edit/widgets` + + queryParams + ); +}; + export const API_EDITOR_URL = ( applicationId = ":applicationId", pageId = ":pageId", @@ -164,45 +175,6 @@ export const QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID = ( )}/queries?importTo=${selectedPageId}`; }; -export const EDITOR_ROUTES = [ - { - icon: MenuIcons.WIDGETS_ICON, - path: BUILDER_PAGE_URL, - title: "Widgets", - className: "t--nav-link-widgets-editor", - exact: true, - }, - { - icon: MenuIcons.APIS_ICON, - path: API_EDITOR_URL, - className: "t--nav-link-api-editor", - title: "APIs", - exact: false, - }, - { - icon: MenuIcons.QUERIES_ICON, - className: "t--nav-link-query-editor", - path: QUERIES_EDITOR_URL, - title: "Queries", - exact: false, - }, - { - icon: MenuIcons.DATASOURCES_ICON, - className: "t--nav-link-datasource-editor", - path: DATA_SOURCES_EDITOR_URL, - title: "Datasources", - exact: false, - allowed: true, - }, - { - icon: MenuIcons.PAGES_ICON, - path: PAGE_LIST_EDITOR_URL, - className: "t--nav-link-manage-pages", - title: "Pages", - exact: true, - }, -]; - export const FORGOT_PASSWORD_URL = `${USER_AUTH_URL}/forgotPassword`; export const RESET_PASSWORD_URL = `${USER_AUTH_URL}/resetPassword`; export const BASE_SIGNUP_URL = `/signup`; diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index d0c8211dc3..a2c5e8ddf7 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -8,7 +8,7 @@ import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsRe import { MetaState } from "reducers/entityReducers/metaReducer"; import { PageListPayload } from "constants/ReduxActionConstants"; import WidgetFactory from "utils/WidgetFactory"; -import { ActionConfig, Property } from "entities/Action"; +import { ActionConfig, Property, PluginType } from "entities/Action"; import { AuthUserState } from "@appsmith/reducers/entityReducers/authUserReducer"; import { UrlDataState } from "@appsmith/reducers/entityReducers/urlReducer"; @@ -38,6 +38,8 @@ export interface DataTreeAction extends Omit { data: ActionResponse["body"]; actionId: string; config: Partial; + pluginType: PluginType; + name: string; run: ActionDispatcher | {}; dynamicBindingPathList: Property[]; ENTITY_TYPE: ENTITY_TYPE.ACTION; @@ -111,6 +113,8 @@ export class DataTreeFactory { dataTree[config.name] = { ...a, actionId: config.id, + name: config.name, + pluginType: config.pluginType, config: config.actionConfiguration, dynamicBindingPathList, data: a.data ? a.data.body : {}, diff --git a/app/client/src/icons/ControlIcons.tsx b/app/client/src/icons/ControlIcons.tsx index fa2350858a..488ef9227d 100644 --- a/app/client/src/icons/ControlIcons.tsx +++ b/app/client/src/icons/ControlIcons.tsx @@ -12,7 +12,7 @@ import { ReactComponent as DecreaseIcon } from "assets/icons/control/decrease.sv import { ReactComponent as DraggableIcon } from "assets/icons/control/draggable.svg"; import { ReactComponent as CloseIcon } from "assets/icons/control/close.svg"; import { ReactComponent as HelpIcon } from "assets/icons/control/help.svg"; - +import { ReactComponent as CollapseIcon } from "assets/icons/control/collapse.svg"; import { ReactComponent as PickMyLocationSelectedIcon } from "assets/icons/control/pick-location-selected.svg"; import { ReactComponent as SettingsIcon } from "assets/icons/control/settings.svg"; import { ReactComponent as DragIcon } from "assets/icons/control/drag.svg"; @@ -108,6 +108,11 @@ export const ControlIcons: { ), + COLLAPSE_CONTROL: (props: IconProps) => ( + + + + ), SORT_CONTROL: (props: IconProps) => ( diff --git a/app/client/src/icons/MenuIcons.tsx b/app/client/src/icons/MenuIcons.tsx index db176d88ed..b5251141f4 100644 --- a/app/client/src/icons/MenuIcons.tsx +++ b/app/client/src/icons/MenuIcons.tsx @@ -4,9 +4,14 @@ import { ReactComponent as WidgetsIcon } from "assets/icons/menu/widgets.svg"; import { ReactComponent as ApisIcon } from "assets/icons/menu/api.svg"; import { ReactComponent as OrgIcon } from "assets/icons/menu/org.svg"; import { ReactComponent as PagesIcon } from "assets/icons/menu/pages.svg"; +import { ReactComponent as PageIcon } from "assets/icons/menu/page.svg"; import { ReactComponent as DataSourcesIcon } from "assets/icons/menu/data-sources.svg"; import { ReactComponent as QueriesIcon } from "assets/icons/menu/queries.svg"; import { ReactComponent as HomepageIcon } from "assets/icons/menu/homepage.svg"; +import { ReactComponent as ExplorerIcon } from "assets/icons/menu/explorer.svg"; +import { ReactComponent as ApisColoredIcon } from "assets/icons/menu/api-colored.svg"; +import { ReactComponent as DataSourcesColoredIcon } from "assets/icons/menu/datasource-colored.svg"; +import { ReactComponent as WidgetsColoredIcon } from "assets/icons/menu/widgets-colored.svg"; import { Icon } from "@blueprintjs/core"; /* eslint-disable react/display-name */ @@ -33,6 +38,11 @@ export const MenuIcons: { ), + PAGE_ICON: (props: IconProps) => ( + + + + ), DATASOURCES_ICON: (props: IconProps) => ( @@ -48,9 +58,29 @@ export const MenuIcons: { ), + EXPLORER_ICON: (props: IconProps) => ( + + + + ), DOCS_ICON: (props: IconProps) => ( ), + WIDGETS_COLORED_ICON: (props: IconProps) => ( + + + + ), + APIS_COLORED_ICON: (props: IconProps) => ( + + + + ), + DATASOURCES_COLORED_ICON: (props: IconProps) => ( + + + + ), }; diff --git a/app/client/src/icons/WidgetIcons.tsx b/app/client/src/icons/WidgetIcons.tsx index 21debddf0a..41e31c193c 100644 --- a/app/client/src/icons/WidgetIcons.tsx +++ b/app/client/src/icons/WidgetIcons.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { JSXElementConstructor } from "react"; import { IconProps, IconWrapper } from "constants/IconConstants"; import { ReactComponent as SpinnerIcon } from "assets/icons/widget/alert.svg"; import { ReactComponent as ButtonIcon } from "assets/icons/widget/button.svg"; @@ -19,11 +19,11 @@ import { ReactComponent as RichTextEditorIcon } from "assets/icons/widget/rich-t import { ReactComponent as ChartIcon } from "assets/icons/widget/chart.svg"; import { ReactComponent as FormIcon } from "assets/icons/widget/form.svg"; import { ReactComponent as MapIcon } from "assets/icons/widget/map.svg"; - +import { ReactComponent as ModalIcon } from "assets/icons/widget/modal.svg"; /* eslint-disable react/display-name */ export const WidgetIcons: { - [id: string]: Function; + [id: string]: JSXElementConstructor; } = { SPINNER_WIDGET: (props: IconProps) => ( @@ -120,6 +120,11 @@ export const WidgetIcons: { ), + MODAL_WIDGET: (props: IconProps) => ( + + + + ), }; export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons]; diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index 62f46a3142..4fa35431cc 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import "./wdyr"; import { Helmet } from "react-helmet"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index f78ef1c2c7..3fc3f19184 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -219,7 +219,6 @@ const WidgetConfigResponse: WidgetConfigReducerState = { detachFromLayout: true, canOutsideClickClose: true, shouldScrollContents: true, - isVisible: false, widgetName: "Modal", children: [], blueprint: { diff --git a/app/client/src/pages/AppViewer/viewer/SideNav.tsx b/app/client/src/pages/AppViewer/viewer/SideNav.tsx index d9f8e29066..2b09d3f40a 100644 --- a/app/client/src/pages/AppViewer/viewer/SideNav.tsx +++ b/app/client/src/pages/AppViewer/viewer/SideNav.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import styled from "styled-components"; import { Menu, Button } from "@blueprintjs/core"; import SideNavItem, { SideNavItemProps } from "./SideNavItem"; diff --git a/app/client/src/pages/Editor/APIEditor/ApiHomeScreen.tsx b/app/client/src/pages/Editor/APIEditor/ApiHomeScreen.tsx index 018e9a6844..70e7f85d01 100644 --- a/app/client/src/pages/Editor/APIEditor/ApiHomeScreen.tsx +++ b/app/client/src/pages/Editor/APIEditor/ApiHomeScreen.tsx @@ -41,11 +41,7 @@ import Spinner from "components/editorComponents/Spinner"; import CurlLogo from "assets/images/Curl-logo.svg"; import { FetchProviderWithCategoryRequest } from "api/ProvidersApi"; import { Plugin } from "api/PluginApi"; -import { - createNewApiAction, - setCurrentCategory, - setLastUsedEditorPage, -} from "actions/apiPaneActions"; +import { createNewApiAction, setCurrentCategory } from "actions/apiPaneActions"; import { getInitialsAndColorCode } from "utils/AppsmithUtils"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { CURL } from "constants/ApiConstants"; @@ -285,42 +281,6 @@ const CardList = styled.div` } `; -// const NoCollections = styled.div` -// padding-top: 52px; -// padding-bottom: 50px; -// text-align: center; -// width: 438px; -// margin: 0 auto; -// color: #666666; -// font-size: 14px; -// line-height: 24px; -// `; -// -// const ImportedApisCard = styled.div` -// flex: 1; -// display: inline-flex; -// flex-wrap: wrap; -// margin-left: -10px; -// justify-content: flex-start; -// text-align: center; -// border-radius: 4px; -// -// .importedApiIcon { -// display: flex; -// height: 30px; -// width: 30px; -// margin-right: 15px; -// color: ${Colors.OXFORD_BLUE}; -// } -// .eachImportedApiCard { -// margin: 10px; -// width: 225px; -// height: 60px; -// display: flex; -// flex-wrap: wrap; -// } -// `; - const DropdownSelect = styled.div` font-size: 14px; float: right; @@ -376,7 +336,6 @@ type ApiHomeScreenProps = { isSwitchingCategory: boolean; createNewApiAction: (pageId: string) => void; setCurrentCategory: (category: string) => void; - setLastUsedEditorPage: (path: string) => void; previouslySetCategory: string; fetchProvidersError: boolean; }; @@ -419,7 +378,6 @@ class ApiHomeScreen extends React.Component { page: 1, }); } - this.props.setLastUsedEditorPage(this.props.match.url); } componentDidUpdate(prevProps: Props) { @@ -437,8 +395,10 @@ class ApiHomeScreen extends React.Component { } } - handleCreateNew = (params: string) => { - const pageId = new URLSearchParams(params).get("importTo"); + handleCreateNew = () => { + const pageId = new URLSearchParams(this.props.location.search).get( + "importTo", + ); if (pageId) { this.props.createNewApiAction(pageId); } @@ -480,7 +440,6 @@ class ApiHomeScreen extends React.Component { } = this.props; const { showSearchResults } = this.state; - const queryParams: string = location.search; let destinationPageId = new URLSearchParams(location.search).get( "importTo", ); @@ -600,7 +559,7 @@ class ApiHomeScreen extends React.Component { this.handleCreateNew(queryParams)} + onClick={this.handleCreateNew} >

Create new

@@ -852,8 +811,6 @@ const mapDispatchToProps = (dispatch: any) => ({ createNewApiAction: (pageId: string) => dispatch(createNewApiAction(pageId)), setCurrentCategory: (category: string) => dispatch(setCurrentCategory(category)), - setLastUsedEditorPage: (path: string) => - dispatch(setLastUsedEditorPage(path)), }); export default connect( diff --git a/app/client/src/pages/Editor/APIEditor/CurlImportForm.tsx b/app/client/src/pages/Editor/APIEditor/CurlImportForm.tsx index 18f3dd3f86..a239765b1a 100644 --- a/app/client/src/pages/Editor/APIEditor/CurlImportForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/CurlImportForm.tsx @@ -14,7 +14,7 @@ import { Colors } from "constants/Colors"; import Button from "components/editorComponents/Button"; import CurlLogo from "assets/images/Curl-logo.svg"; -const CurlImportFormContainer = styled.form` +const CurlImportFormContainer = styled.div` display: flex; flex-direction: column; width: 100%; @@ -111,7 +111,6 @@ type Props = StateAndRouteProps & class CurlImportForm extends React.Component { render() { const { handleSubmit, history, isImportingCurl } = this.props; - return (
diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx index 96f1023dc1..1759da8118 100644 --- a/app/client/src/pages/Editor/APIEditor/Form.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { connect } from "react-redux"; import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form"; import { @@ -10,7 +10,6 @@ import FormLabel from "components/editorComponents/FormLabel"; import FormRow from "components/editorComponents/FormRow"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import { PaginationField } from "api/ActionAPI"; -import { ReduxActionTypes } from "constants/ReduxActionConstants"; import DropdownField from "components/editorComponents/form/fields/DropdownField"; import { API_EDITOR_FORM_NAME } from "constants/forms"; import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView"; @@ -121,10 +120,6 @@ interface APIFormProps { actionConfigurationHeaders?: any; actionName: string; apiId: string; - location: { - pathname: string; - }; - dispatch: any; apiName: string; } @@ -137,7 +132,6 @@ export const NameWrapper = styled.div` input { margin: 0; box-sizing: border-box; - // border: 0; } `; @@ -153,17 +147,7 @@ const ApiEditorForm: React.FC = (props: Props) => { actionConfigurationBody, httpMethodFromForm, actionName, - location, - dispatch, } = props; - useEffect(() => { - dispatch({ - type: ReduxActionTypes.SET_LAST_USED_EDITOR_PAGE, - payload: { - path: location.pathname, - }, - }); - }); const allowPostBody = httpMethodFromForm && httpMethodFromForm !== HTTP_METHODS[0]; @@ -298,6 +282,5 @@ export default connect((state: AppState) => { })( reduxForm({ form: API_EDITOR_FORM_NAME, - destroyOnUnmount: false, })(ApiEditorForm), ); diff --git a/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx index 1901a5e140..fb568bc067 100644 --- a/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { connect } from "react-redux"; import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form"; import { POST_BODY_FORMAT_OPTIONS } from "constants/ApiEditorConstants"; @@ -7,7 +7,6 @@ import FormLabel from "components/editorComponents/FormLabel"; import FormRow from "components/editorComponents/FormRow"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import { PaginationField, BodyFormData, Property } from "api/ActionAPI"; -import { ReduxActionTypes } from "constants/ReduxActionConstants"; import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; import ApiResponseView from "components/editorComponents/ApiResponseView"; @@ -130,8 +129,6 @@ const RapidApiEditorForm: React.FC = (props: Props) => { providerImage, providerURL, providerCredentialSteps, - location, - dispatch, } = props; const postbodyResponsePresent = @@ -140,26 +137,6 @@ const RapidApiEditorForm: React.FC = (props: Props) => { actionConfigurationBodyFormData && actionConfigurationBodyFormData.length > 0; - // let credentialStepsData; - // if (providerCredentialSteps.length !== 0) { - // credentialStepsData = providerCredentialSteps.split("\\n"); - // } - // console.log(credentialStepsData, "credentialStepsData"); - - useEffect(() => { - dispatch({ - type: ReduxActionTypes.SET_LAST_USED_EDITOR_PAGE, - payload: { - path: location.pathname, - }, - }); - }); - - // const abc = (text: string) => { - // console.log(text, "text"); - - // } - return (
; isDeleting: Record; isCreating: boolean; - isMoving: boolean; - isCopying: boolean; apiName: string; currentApplication: UserApplication; currentPageName: string | undefined; @@ -54,6 +53,7 @@ interface ReduxActionProps { submitForm: (name: string) => void; runAction: (id: string, paginationField?: PaginationField) => void; deleteAction: (id: string, name: string) => void; + changeAPIPage: (apiId: string) => void; } function getPageName(pages: any, pageId: string) { @@ -66,6 +66,9 @@ type Props = ReduxActionProps & RouteComponentProps<{ apiId: string; applicationId: string; pageId: string }>; class ApiEditor extends React.Component { + componentDidMount() { + this.props.changeAPIPage(this.props.match.params.apiId); + } handleDeleteClick = () => { const pageName = getPageName( this.props.pages, @@ -79,6 +82,12 @@ class ApiEditor extends React.Component { this.props.deleteAction(this.props.match.params.apiId, this.props.apiName); }; + componentDidUpdate(prevProps: Props) { + if (prevProps.match.params.apiId !== this.props.match.params.apiId) { + this.props.changeAPIPage(this.props.match.params.apiId); + } + } + handleRunClick = (paginationField?: PaginationField) => { const pageName = getPageName( this.props.pages, @@ -119,12 +128,10 @@ class ApiEditor extends React.Component { isRunning, isDeleting, isCreating, - isCopying, - isMoving, paginationType, isEditorInitialized, } = this.props; - if (isCreating || isCopying || isMoving || !isEditorInitialized) { + if (isCreating || !isEditorInitialized) { return ( @@ -174,7 +181,6 @@ class ApiEditor extends React.Component { : "" } apiName={this.props.apiName} - location={this.props.location} /> )} @@ -207,13 +213,7 @@ class ApiEditor extends React.Component { const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { const apiAction = getActionById(state, props); const apiName = getApiName(state, props.match.params.apiId); - const { - isDeleting, - isRunning, - isCreating, - isMoving, - isCopying, - } = state.ui.apiPane; + const { isDeleting, isRunning, isCreating } = state.ui.apiPane; return { actions: state.entities.actions, currentApplication: getCurrentApplication(state), @@ -227,8 +227,6 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { isRunning, isDeleting, isCreating, - isMoving, - isCopying, isEditorInitialized: getIsEditorInitialized(state), }; }; @@ -239,6 +237,7 @@ const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({ dispatch(runAction(id, paginationField)), deleteAction: (id: string, name: string) => dispatch(deleteAction({ id, name })), + changeAPIPage: (actionId: string) => dispatch(changeApi(actionId)), }); export default connect(mapStateToProps, mapDispatchToProps)(ApiEditor); diff --git a/app/client/src/pages/Editor/ApiSidebar.tsx b/app/client/src/pages/Editor/ApiSidebar.tsx deleted file mode 100644 index bcc3108601..0000000000 --- a/app/client/src/pages/Editor/ApiSidebar.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; -import { RouteComponentProps } from "react-router"; -import styled from "styled-components"; -import { AppState } from "reducers"; -import { ActionDataState } from "reducers/entityReducers/actionsReducer"; -import { - API_EDITOR_URL_WITH_SELECTED_PAGE_ID, - APIEditorRouteParams, -} from "constants/routes"; -import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer"; -import { - moveActionRequest, - copyActionRequest, - deleteAction, -} from "actions/actionActions"; -import { - changeApi, - createNewApiAction, - initApiPane, -} from "actions/apiPaneActions"; -import EditorSidebar from "pages/Editor/EditorSidebar"; -import { getNextEntityName } from "utils/AppsmithUtils"; -import AnalyticsUtil from "utils/AnalyticsUtil"; -import { Page } from "constants/ReduxActionConstants"; -import { RestAction } from "entities/Action"; - -const HTTPMethod = styled.span<{ method?: string }>` - flex: 1; - font-size: 12px; - color: ${props => { - switch (props.method) { - case "GET": - return "#29CCA3"; - case "POST": - return "#F7C75B"; - case "PUT": - return "#30A5E0"; - case "PATCH": - return "#8E8E8E"; - case "DELETE": - return "#CE4257"; - default: - return "#333"; - } - }}; -`; - -const ActionItem = styled.div` - flex: 1; - display: flex; - align-items: center; - max-width: 90%; -`; - -const ActionName = styled.span` - flex: 3; - padding: 0 2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -interface ReduxStateProps { - actions: ActionDataState; - apiPane: ApiPaneReduxState; - pages: Page[]; -} - -interface ReduxDispatchProps { - onApiChange: (id: string) => void; - initApiPane: (urlId?: string) => void; - moveAction: ( - id: string, - pageId: string, - name: string, - originalPageId: string, - ) => void; - copyAction: (id: string, pageId: string, name: string) => void; - deleteAction: (id: string, name: string) => void; - createNewApiAction: (pageId: string) => void; -} - -type Props = ReduxStateProps & - ReduxDispatchProps & - RouteComponentProps; - -class ApiSidebar extends React.Component { - componentDidMount(): void { - this.props.initApiPane(this.props.match.params.apiId); - } - - handleApiChange = (actionId: string) => { - this.props.onApiChange(actionId); - }; - - handleMove = (itemId: string, destinationPageId: string) => { - const { pages } = this.props; - const action = this.props.actions.filter(a => a.config.id === itemId)[0]; - const pageApiNames = this.props.actions - .filter(a => a.config.pageId === destinationPageId) - .map(a => a.config.name); - let name = action.config.name; - const page = pages.find(page => page.pageId === destinationPageId); - - AnalyticsUtil.logEvent("MOVE_API_CLICK", { - apiId: itemId, - apiName: name, - pageName: page?.pageName, - }); - if (pageApiNames.indexOf(action.config.name) > -1) { - name = getNextEntityName(name, pageApiNames); - } - this.props.moveAction( - itemId, - destinationPageId, - name, - action.config.pageId, - ); - }; - - handleCopy = (itemId: string, destinationPageId: string) => { - const { pages } = this.props; - const action = this.props.actions.filter(a => a.config.id === itemId)[0]; - const pageApiNames = this.props.actions - .filter(a => a.config.pageId === destinationPageId) - .map(a => a.config.name); - let name = `${action.config.name}Copy`; - const page = pages.find(page => page.pageId === destinationPageId); - - AnalyticsUtil.logEvent("DUPLICATE_API_CLICK", { - apiId: itemId, - apiName: name, - pageName: page?.pageName, - }); - - if (pageApiNames.indexOf(name) > -1) { - name = getNextEntityName(name, pageApiNames); - } - this.props.copyAction(itemId, destinationPageId, name); - }; - - handleDelete = (itemId: string, itemName: string, pageName: string) => { - AnalyticsUtil.logEvent("DELETE_API_CLICK", { - apiId: itemId, - apiName: itemName, - pageName: pageName, - }); - this.props.deleteAction(itemId, itemName); - }; - - renderItem = (action: RestAction) => { - return ( - { - AnalyticsUtil.logEvent("API_SELECT", { - apiId: action.id, - apiName: action.name, - }); - }} - > - {action.actionConfiguration ? ( - - {action.actionConfiguration.httpMethod} - - ) : ( - - )} - {action.name} - - ); - }; - - handleCreateNewApiClick = (selectedPageId: string) => { - const { history } = this.props; - const { pageId, applicationId } = this.props.match.params; - history.push( - API_EDITOR_URL_WITH_SELECTED_PAGE_ID( - applicationId, - pageId, - selectedPageId, - ), - ); - }; - - render() { - const { - apiPane: { isFetching }, - match: { - params: { apiId }, - }, - actions, - } = this.props; - const data = actions - .filter(a => a.config?.pluginType === "API") - .map(a => a.config); - return ( - - ); - } -} - -const mapStateToProps = (state: AppState): ReduxStateProps => ({ - actions: state.entities.actions, - apiPane: state.ui.apiPane, - pages: state.entities.pageList.pages, -}); - -const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({ - onApiChange: (actionId: string) => dispatch(changeApi(actionId)), - initApiPane: (urlId?: string) => dispatch(initApiPane(urlId)), - moveAction: ( - id: string, - destinationPageId: string, - name: string, - originalPageId: string, - ) => - dispatch( - moveActionRequest({ id, destinationPageId, originalPageId, name }), - ), - copyAction: (id: string, destinationPageId: string, name: string) => - dispatch(copyActionRequest({ id, destinationPageId, name })), - deleteAction: (id: string, name: string) => - dispatch(deleteAction({ id, name })), - createNewApiAction: (pageId: string) => dispatch(createNewApiAction(pageId)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ApiSidebar); diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index 28dbe4113f..5fba906bc2 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { memo } from "react"; import WidgetFactory from "utils/WidgetFactory"; import { RenderModes } from "constants/WidgetConstants"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; @@ -11,7 +11,7 @@ interface CanvasProps { } // TODO(abhinav): get the render mode from context -const Canvas = (props: CanvasProps) => { +const Canvas = memo((props: CanvasProps) => { try { return ( @@ -26,6 +26,8 @@ const Canvas = (props: CanvasProps) => { console.log("Error rendering DSL", error); return null; } -}; +}); + +Canvas.displayName = "Canvas"; export default Canvas; diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index 19c4f9a809..f3382f95ba 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -12,6 +12,7 @@ import { updateDatasource, testDatasource, deleteDatasource, + switchDatasource, } from "actions/datasourceActions"; import { DATASOURCE_DB_FORM } from "constants/forms"; import DatasourceHome from "./DatasourceHome"; @@ -44,6 +45,20 @@ type Props = ReduxStateProps & }>; class DataSourceEditor extends React.Component { + componentDidUpdate(prevProps: Props) { + if ( + this.props.match.params.datasourceId && + this.props.match.params.datasourceId !== + prevProps.match.params.datasourceId + ) { + this.props.switchDatasource(this.props.match.params.datasourceId); + } + } + componentDidMount() { + if (this.props.match.params.datasourceId) { + this.props.switchDatasource(this.props.match.params.datasourceId); + } + } handleSubmit = () => { this.props.submitForm(DATASOURCE_DB_FORM); }; @@ -137,6 +152,7 @@ const mapDispatchToProps = (dispatch: any): DatasourcePaneFunctions => ({ }, testDatasource: (data: Datasource) => dispatch(testDatasource(data)), deleteDatasource: (id: string) => dispatch(deleteDatasource({ id })), + switchDatasource: (id: string) => dispatch(switchDatasource(id)), }); export interface DatasourcePaneFunctions { @@ -144,6 +160,7 @@ export interface DatasourcePaneFunctions { updateDatasource: (data: Datasource) => void; testDatasource: (data: Datasource) => void; deleteDatasource: (id: string) => void; + switchDatasource: (id: string) => void; } export default connect(mapStateToProps, mapDispatchToProps)(DataSourceEditor); diff --git a/app/client/src/pages/Editor/DataSourceSidebar.tsx b/app/client/src/pages/Editor/DataSourceSidebar.tsx deleted file mode 100644 index 9f228e878d..0000000000 --- a/app/client/src/pages/Editor/DataSourceSidebar.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; -import { initialize } from "redux-form"; -import styled from "styled-components"; -import { RouteComponentProps } from "react-router"; -import Button from "components/editorComponents/Button"; -import { DATASOURCE_DB_FORM } from "constants/forms"; -import { IIconProps } from "@blueprintjs/core"; -import { Colors } from "constants/Colors"; -import TreeDropdown from "components/editorComponents/actioncreator/TreeDropdown"; -import { BaseTextInput } from "components/designSystems/appsmith/TextInputComponent"; -import { getDataSources } from "selectors/editorSelectors"; -import { getPluginImages } from "selectors/entitiesSelector"; -import { - initDatasourcePane, - storeDatastoreRefs, - deleteDatasource, - changeDatasource, -} from "actions/datasourceActions"; -import { ControlIcons } from "icons/ControlIcons"; -import { theme } from "constants/DefaultTheme"; -import { selectPlugin } from "actions/datasourceActions"; -import { fetchPluginForm } from "actions/pluginActions"; -import { DATA_SOURCES_EDITOR_URL } from "constants/routes"; -import { AppState } from "reducers"; -import { Datasource } from "api/DatasourcesApi"; -import Fuse from "fuse.js"; - -interface ReduxDispatchProps { - initDatasourcePane: (pluginType: string, urlId?: string) => void; - selectPlugin: (pluginType: string) => void; - initializeForm: (data: Record) => void; - storeDatastoreRefs: (refsList: []) => void; - fetchFormConfig: (id: string) => void; - deleteDatasource: (id: string) => void; - onDatasourceChange: (datasource: Datasource) => void; -} - -interface ReduxStateProps { - dataSources: Datasource[]; - pluginImages: Record; - datastoreRefs: Record; - formConfigs: Record; - drafts: Record; -} - -type DataSourceSidebarProps = {}; - -type State = { - search: string; -}; - -const ActionItem = styled.div` - flex: 1; - display: flex; - align-items: center; -`; - -const ActionName = styled.span` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 130px; - margin-left: 10px; -`; - -const SearchBar = styled(BaseTextInput)` - margin-bottom: 10px; - input { - background-color: #23292e; - border: none; - color: ${props => props.theme.colors.textOnDarkBG} - :focus { - background-color: #23292e; - } - } - .bp3-icon { - background-color: #23292e; - } -`; - -const ItemContainer = styled.div<{ - isSelected: boolean; -}>` -padding: 8px 12px; -border-radius: 4px; -align-items: center; -cursor: pointer; -display: flex; -font-size: 14px; -margin-bottom: 2px; - background-color: ${props => - props.isSelected ? props.theme.colors.paneCard : props.theme.colors.paneBG} - :hover { - background-color: ${props => props.theme.colors.paneCard}; - } -`; - -const StyledImage = styled.img` - height: 20px; - width: 20px; - - svg { - path { - fill: ${Colors.WHITE}; - } - } -`; - -const StyledAddButton = styled(Button)` - padding: "9px"; - &&& { - outline: none; - padding: 10px !important; - } - span { - font-weight: normal !important; - } -`; - -const Controls = styled.div` - padding: 10px; -`; - -const Wrapper = styled.div` - padding: 5px; -`; - -const Container = styled.div` - .createBtn { - border: none; - color: ${Colors.WHITE} !important; - width: 100%; - display: block !important; - font-weight: normal; - font-size: 14px; - line-height: 20px; - - &:hover { - color: ${Colors.WHITE} !important; - background-color: ${Colors.BLUE_CHARCOAL} !important; - - svg { - path { - fill: ${Colors.WHITE}; - } - } - } - } - .highlightButton { - color: ${Colors.WHITE} !important; - background-color: ${Colors.BLUE_CHARCOAL} !important; - display: block !important; - font-weight: normal; - font-size: 14px; - line-height: 20px; - - svg { - path { - fill: ${Colors.WHITE}; - } - } - } -`; - -const DraftIconIndicator = styled.span<{ isHidden: boolean }>` - width: 8px; - height: 8px; - border-radius: 8px; - background-color: #f2994a; - margin: 0 5px; - opacity: ${({ isHidden }) => (isHidden ? 0 : 1)}; -`; - -type Props = DataSourceSidebarProps & - RouteComponentProps<{ - pageId: string; - applicationId: string; - datasourceId: string; - }> & - ReduxStateProps & - ReduxDispatchProps; - -const FUSE_OPTIONS = { - shouldSort: true, - threshold: 0.5, - location: 0, - minMatchCharLength: 3, - findAllMatches: true, - keys: ["name"], -}; - -class DataSourceSidebar extends React.Component { - refsCollection: any; - - constructor(props: Props) { - super(props); - this.state = { - search: "", - }; - - this.refsCollection = {}; - } - - componentDidMount() { - const { dataSources, storeDatastoreRefs } = this.props; - this.refsCollection = dataSources.reduce((acc: any, value) => { - acc[value.id] = React.createRef(); - return acc; - }, {}); - storeDatastoreRefs(this.refsCollection); - } - - shouldComponentUpdate(nextProps: Readonly): boolean { - if (Object.keys(nextProps.drafts) !== Object.keys(this.props.drafts)) { - return true; - } - return nextProps.dataSources !== this.props.dataSources; - } - - handleCreateNewDatasource = () => { - const { history } = this.props; - const { pageId, applicationId } = this.props.match.params; - - history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId)); - }; - - handleItemSelected = (datasource: Datasource) => { - this.props.onDatasourceChange(datasource); - }; - - handleSearchChange = (e: React.ChangeEvent<{ value: string }>) => { - const value = e.target.value; - this.setState({ - search: value, - }); - }; - - getSearchFilteredList = () => { - const { search } = this.state; - const { dataSources } = this.props; - const fuse = new Fuse(dataSources, FUSE_OPTIONS); - return search ? fuse.search(search) : dataSources; - }; - - renderItem = () => { - const { - match: { - params: { datasourceId }, - }, - datastoreRefs, - deleteDatasource, - drafts, - pluginImages, - } = this.props; - - const filteredList = this.getSearchFilteredList(); - - return filteredList.map(datasource => { - return ( - this.handleItemSelected(datasource)} - > - - - {datasource.name} - - - { - return null; - }} - selectedValue="" - optionTree={[ - { - value: "delete", - onSelect: () => deleteDatasource(datasource.id), - label: "Delete", - intent: "danger", - }, - ]} - toggle={ - - } - /> - - ); - }); - }; - - render() { - const { search } = this.state; - const { - match: { - params: { datasourceId }, - }, - } = this.props; - - return ( - - - - - - - - {this.renderItem()} - - ); - } -} - -const mapStateToProps = (state: AppState): ReduxStateProps => { - const { drafts } = state.ui.datasourcePane; - return { - formConfigs: state.entities.plugins.formConfigs, - dataSources: getDataSources(state), - pluginImages: getPluginImages(state), - datastoreRefs: state.ui.datasourcePane.datasourceRefs, - drafts, - }; -}; - -const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({ - initDatasourcePane: (pluginType: string, urlId?: string) => - dispatch(initDatasourcePane(pluginType, urlId)), - onDatasourceChange: (datasource: Datasource) => - dispatch(changeDatasource(datasource)), - fetchFormConfig: (id: string) => dispatch(fetchPluginForm({ id })), - selectPlugin: (pluginId: string) => dispatch(selectPlugin(pluginId)), - initializeForm: (data: Record) => - dispatch(initialize(DATASOURCE_DB_FORM, data)), - storeDatastoreRefs: (refsList: {}) => { - dispatch(storeDatastoreRefs(refsList)); - }, - deleteDatasource: (id: string) => dispatch(deleteDatasource({ id })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DataSourceSidebar); diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index 08415ad243..0e44ccbd83 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -122,8 +122,6 @@ const ShareButton = styled(Button)` `; type EditorHeaderProps = { - currentApplication?: ApplicationPayload; - isSaving?: boolean; pageSaveError?: boolean; pageName?: string; pageId?: string; @@ -131,6 +129,8 @@ type EditorHeaderProps = { publishedTime?: string; orgId: string; applicationId?: string; + currentApplication?: ApplicationPayload; + isSaving: boolean; publishApplication: (appId: string) => void; }; diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx new file mode 100644 index 0000000000..cf43ea3b13 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx @@ -0,0 +1,88 @@ +import React, { ReactNode, useCallback, memo } from "react"; +import Entity, { EntityClassNames } from "../Entity"; +import ActionEntityContextMenu from "./ActionEntityContextMenu"; +import history from "utils/history"; +import { saveActionName } from "actions/actionActions"; +import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; +import EntityProperty from "../Entity/EntityProperty"; +import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; + +const getUpdateActionNameReduxAction = (id: string, name: string) => { + return saveActionName({ id, name }); +}; + +const getActionProperties = (action: any, step: number) => { + const config = entityDefinitions.ACTION(action); + + return ( + config && + Object.keys(config) + .filter(k => k.indexOf("!") === -1) + .map((actionProperty: string) => { + let value = action[actionProperty]; + if (actionProperty === "run") { + value = "Function"; + actionProperty = actionProperty + "()"; + } + if (actionProperty === "data") { + value = action.data; + } + return ( + + ); + }) + ); +}; + +type ExplorerActionEntityProps = { + action: DataTreeAction; + url: string; + icon: ReactNode; + active: boolean; + step: number; + searchKeyword?: string; + pageId: string; +}; + +export const ExplorerActionEntity = memo((props: ExplorerActionEntityProps) => { + const switchToAction = useCallback(() => { + props.url && history.push(props.url); + }, [props.url]); + + const contextMenu = ( + + ); + return ( + + {getActionProperties(props.action, props.step + 1)} + + ); +}); + +ExplorerActionEntity.displayName = "ExplorerActionEntity"; + +export default ExplorerActionEntity; diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionEntityContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionEntityContextMenu.tsx new file mode 100644 index 0000000000..0c3cc15141 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionEntityContextMenu.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import TreeDropdown from "components/editorComponents/actioncreator/TreeDropdown"; + +import { AppState } from "reducers"; +import { getNextEntityName } from "utils/AppsmithUtils"; +import ContextMenuTrigger from "../ContextMenuTrigger"; + +import { + moveActionRequest, + copyActionRequest, + deleteAction, +} from "actions/actionActions"; + +import { initExplorerEntityNameEdit } from "actions/explorerActions"; +import { ContextMenuPopoverModifiers } from "../helpers"; +import { noop } from "lodash"; + +const useNewAPIName = () => { + // This takes into consideration only the current page widgets + // If we're moving to a different page, there could be a widget + // with the same name as the generated API name + // TODO: Figure out how to handle this scenario + const apiNames = useSelector((state: AppState) => + state.entities.actions.map(action => action.config.name), + ); + return (name: string) => + apiNames.indexOf(name) > -1 ? getNextEntityName(name, apiNames) : name; +}; + +type EntityContextMenuProps = { + id: string; + name: string; + className?: string; + pageId: string; +}; +export const ActionEntityContextMenu = (props: EntityContextMenuProps) => { + const nextEntityName = useNewAPIName(); + + const dispatch = useDispatch(); + const copyActionToPage = useCallback( + (actionId: string, actionName: string, pageId: string) => + dispatch( + copyActionRequest({ + id: actionId, + destinationPageId: pageId, + name: nextEntityName(`${actionName}Copy`), + }), + ), + [dispatch, nextEntityName], + ); + const moveActionToPage = useCallback( + (actionId: string, actionName: string, destinationPageId: string) => + dispatch( + moveActionRequest({ + id: actionId, + destinationPageId, + originalPageId: props.pageId, + name: nextEntityName(actionName), + }), + ), + [dispatch, nextEntityName, props.pageId], + ); + const deleteActionFromPage = useCallback( + (actionId: string, actionName: string) => + dispatch(deleteAction({ id: actionId, name: actionName })), + [dispatch], + ); + + const menuPages = useSelector((state: AppState) => { + return state.entities.pageList.pages.map(page => ({ + label: page.pageName, + id: page.pageId, + value: page.pageName, + })); + }); + + const editActionName = useCallback( + () => dispatch(initExplorerEntityNameEdit(props.id)), + [dispatch, props.id], + ); + + return ( + { + return { + ...page, + onSelect: () => copyActionToPage(props.id, props.name, page.id), + }; + }), + }, + { + value: "move", + onSelect: noop, + label: "Move to page", + children: menuPages + .filter(page => page.id !== props.pageId) // Remove current page from the list + .map(page => { + return { + ...page, + onSelect: () => moveActionToPage(props.id, props.name, page.id), + }; + }), + }, + { + value: "delete", + onSelect: () => deleteActionFromPage(props.id, props.name), + label: "Delete", + intent: "danger", + }, + ]} + toggle={} + /> + ); +}; + +export default ActionEntityContextMenu; diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx new file mode 100644 index 0000000000..c23e357225 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx @@ -0,0 +1,90 @@ +import React, { ReactNode, useCallback, memo } from "react"; +import ExplorerActionEntity from "./ActionEntity"; +import { Page } from "constants/ReduxActionConstants"; +import { ExplorerURLParams, getActionIdFromURL } from "../helpers"; +import { ActionGroupConfig } from "./helpers"; +import { useParams } from "react-router"; +import { ApiActionConfig } from "entities/Action"; +import EntityPlaceholder from "../Entity/Placeholder"; +import Entity from "../Entity"; +import history from "utils/history"; +import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; + +type ExplorerActionsGroupProps = { + actions: DataTreeAction[]; + step: number; + searchKeyword?: string; + config: ActionGroupConfig; + page: Page; +}; +export const ExplorerActionsGroup = memo((props: ExplorerActionsGroupProps) => { + const params = useParams(); + let childNode: ReactNode = props.actions.map((action: DataTreeAction) => { + const url = props.config?.getURL( + params.applicationId, + props.page.pageId, + action.actionId, + ); + const actionId = getActionIdFromURL(); + const active = actionId === action.actionId; + + let method = undefined; + method = (action.config as ApiActionConfig).httpMethod; + const icon = props.config?.getIcon(method); + return ( + + ); + }); + + if (!props.searchKeyword && (!childNode || !props.actions.length)) { + childNode = ( + + No {props.config?.groupName || "Actions"} yet. Please click the{" "} + + icon on + {props.config?.groupName || "Actions"} above, to + create. + + ); + } + + const switchToCreateActionPage = useCallback(() => { + const path = props.config?.generateCreatePageURL( + params?.applicationId, + props.page.pageId, + props.page.pageId, + ); + history.push(path); + }, [props.config, props.page.pageId, params]); + + return ( + + {childNode} + + ); +}); + +ExplorerActionsGroup.displayName = "ExplorerActionsGroup"; + +export default ExplorerActionsGroup; diff --git a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx new file mode 100644 index 0000000000..86e7322b95 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx @@ -0,0 +1,115 @@ +import React, { ReactNode } from "react"; +import { apiIcon, queryIcon, MethodTag } from "../ExplorerIcons"; +import { PluginType } from "entities/Action"; +import { generateReactKey } from "utils/generators"; +import { QUERIES_EDITOR_URL, API_EDITOR_URL } from "constants/routes"; +import { + API_EDITOR_ID_URL, + QUERIES_EDITOR_ID_URL, + API_EDITOR_URL_WITH_SELECTED_PAGE_ID, + QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID, +} from "constants/routes"; + +import { Page } from "constants/ReduxActionConstants"; +import ExplorerActionsGroup from "./ActionsGroup"; +import { ExplorerURLParams } from "../helpers"; +import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; + +export type ActionGroupConfig = { + groupName: string; + type: PluginType; + icon: JSX.Element; + key: string; + getURL: (applicationId: string, pageId: string, id: string) => string; + generateCreatePageURL: ( + applicationId: string, + pageId: string, + selectedPageId: string, + ) => string; + getIcon: (method?: string) => ReactNode; + isGroupActive: (params: ExplorerURLParams, pageId: string) => boolean; + isGroupExpanded: (params: ExplorerURLParams, pageId: string) => boolean; +}; + +// When we have new action plugins, we can just add it to this map +// There should be no other place where we refer to the PluginType in entity explorer. +/*eslint-disable react/display-name */ +export const ACTION_PLUGIN_MAP: Array< + ActionGroupConfig | undefined +> = Object.keys(PluginType).map((type: string) => { + switch (type) { + case PluginType.API: + return { + groupName: "APIs", + type, + icon: apiIcon, + key: generateReactKey(), + getURL: (applicationId: string, pageId: string, id: string) => { + return `${API_EDITOR_ID_URL(applicationId, pageId, id)}`; + }, + getIcon: (method?: string) => { + if (!method) return apiIcon; + return ; + }, + generateCreatePageURL: API_EDITOR_URL_WITH_SELECTED_PAGE_ID, + isGroupActive: (params: ExplorerURLParams, pageId: string) => + window.location.pathname === + API_EDITOR_URL(params.applicationId, pageId), + isGroupExpanded: (params: ExplorerURLParams, pageId: string) => + window.location.pathname.indexOf( + API_EDITOR_URL(params.applicationId, pageId), + ) > -1, + }; + case PluginType.DB: + return { + groupName: "Queries", + type, + icon: queryIcon, + key: generateReactKey(), + getURL: (applicationId: string, pageId: string, id: string) => + `${QUERIES_EDITOR_ID_URL(applicationId, pageId, id)}`, + getIcon: () => { + return queryIcon; + }, + generateCreatePageURL: QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID, + isGroupActive: (params: ExplorerURLParams, pageId: string) => + window.location.pathname === + QUERIES_EDITOR_URL(params.applicationId, pageId), + isGroupExpanded: (params: ExplorerURLParams, pageId: string) => + window.location.pathname.indexOf( + QUERIES_EDITOR_URL(params.applicationId, pageId), + ) > -1, + }; + default: + return undefined; + } +}); + +// Gets the Actions groups in the entity explorer +// ACTION_PLUGIN_MAP specifies the number of groups +// APIs, Queries, etc. +export const getActionGroups = ( + page: Page, + actions: DataTreeAction[], + step: number, + searchKeyword?: string, +) => { + return ACTION_PLUGIN_MAP?.map((config?: ActionGroupConfig) => { + if (!config) return null; + const entries = actions.filter( + (entry: DataTreeAction) => entry.pluginType === config?.type, + ); + if (entries.length === 0 && !!searchKeyword) return null; + + return ( + + ); + }); +}; diff --git a/app/client/src/pages/Editor/Explorer/ContextMenuTrigger.tsx b/app/client/src/pages/Editor/Explorer/ContextMenuTrigger.tsx new file mode 100644 index 0000000000..9ee1c319f9 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/ContextMenuTrigger.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { ControlIcons } from "icons/ControlIcons"; +import { withTheme } from "styled-components"; +import { Theme } from "constants/DefaultTheme"; +import { EntityTogglesWrapper } from "./ExplorerStyledComponents"; +import styled from "styled-components"; + +const ToggleIcon = styled(ControlIcons.MORE_VERTICAL_CONTROL)` + &&& { + flex-grow: 0; + width: ${props => props.theme.fontSizes[3]}px; + height: ${props => props.theme.fontSizes[3]}px; + } +`; +export const ContextMenuTrigger = (props: { + className?: string; + theme: Theme; +}) => { + return ( + + + + ); +}; + +export default withTheme(ContextMenuTrigger); diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx new file mode 100644 index 0000000000..6f9a425836 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx @@ -0,0 +1,37 @@ +import React, { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { deleteDatasource } from "actions/datasourceActions"; +import TreeDropdown from "components/editorComponents/actioncreator/TreeDropdown"; +import ContextMenuTrigger from "../ContextMenuTrigger"; +import { noop } from "lodash"; +import { ContextMenuPopoverModifiers } from "../helpers"; + +export const DataSourceContextMenu = (props: { + datasourceId: string; + className?: string; +}) => { + const dispatch = useDispatch(); + const dispatchDelete = useCallback(() => { + dispatch(deleteDatasource({ id: props.datasourceId })); + }, [dispatch, props.datasourceId]); + return ( + } + /> + ); +}; + +export default DataSourceContextMenu; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx new file mode 100644 index 0000000000..b4a46a117c --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from "react"; +import { Datasource } from "api/DatasourcesApi"; +import DataSourceContextMenu from "./DataSourceContextMenu"; +import { queryIcon } from "../ExplorerIcons"; +import { useParams } from "react-router"; +import { ExplorerURLParams, getDatasourceIdFromURL } from "../helpers"; +import Entity, { EntityClassNames } from "../Entity"; +import { DATA_SOURCES_EDITOR_ID_URL } from "constants/routes"; +import history from "utils/history"; + +type ExplorerDatasourceEntityProps = { + datasource: Datasource; + step: number; + searchKeyword?: string; +}; +export const ExplorerDatasourceEntity = ( + props: ExplorerDatasourceEntityProps, +) => { + const params = useParams(); + const switchDatasource = useCallback( + () => + history.push( + DATA_SOURCES_EDITOR_ID_URL( + params.applicationId, + params.pageId, + props.datasource.id, + ), + ), + [params.applicationId, params.pageId, props.datasource.id], + ); + const datasourceIdFromURL = getDatasourceIdFromURL(); + const active = datasourceIdFromURL === props.datasource.id; + return ( + + } + /> + ); +}; + +export default ExplorerDatasourceEntity; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx new file mode 100644 index 0000000000..c559dce3a1 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx @@ -0,0 +1,76 @@ +import React, { useMemo, ReactNode } from "react"; +import { Datasource } from "api/DatasourcesApi"; +import { datasourceIcon } from "../ExplorerIcons"; +import Entity from "../Entity"; +import { groupBy } from "lodash"; +import { DATA_SOURCES_EDITOR_URL } from "constants/routes"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "../helpers"; +import history from "utils/history"; +import { Plugin } from "api/PluginApi"; +import DatasourcePluginGroup from "./PluginGroup"; + +type ExplorerDatasourcesGroupProps = { + dataSources: Datasource[]; + plugins: Plugin[]; + step: number; + searchKeyword?: string; +}; + +export const ExplorerDatasourcesGroup = ( + props: ExplorerDatasourcesGroupProps, +) => { + const params = useParams(); + const disableDatasourceGroup = + !props.dataSources || !props.dataSources.length; + + const pluginGroups = useMemo(() => groupBy(props.dataSources, "pluginId"), [ + props.dataSources, + ]); + + const pluginGroupNodes: ReactNode[] = []; + for (const [pluginId, datasources] of Object.entries(pluginGroups)) { + const plugin = props.plugins.find( + (plugin: Plugin) => plugin.id === pluginId, + ); + + pluginGroupNodes.push( + , + ); + } + return ( + -1 + } + isDefaultExpanded={ + window.location.pathname.indexOf( + DATA_SOURCES_EDITOR_URL(params.applicationId, params.pageId), + ) > -1 || !!props.searchKeyword + } + disabled={disableDatasourceGroup} + createFn={() => { + history.push( + DATA_SOURCES_EDITOR_URL(params.applicationId, params.pageId), + ); + }} + > + {pluginGroupNodes} + + ); +}; + +export default ExplorerDatasourcesGroup; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx b/app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx new file mode 100644 index 0000000000..39445574d5 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import Entity from "../Entity"; +import { Plugin } from "api/PluginApi"; +import { Datasource } from "api/DatasourcesApi"; +import { getPluginIcon } from "../ExplorerIcons"; +import { getDatasourceIdFromURL } from "../helpers"; +import ExplorerDatasourceEntity from "./DatasourceEntity"; + +type DatasourcePluginGroupProps = { + plugin?: Plugin; + datasources: Datasource[]; + step: number; + searchKeyword?: string; +}; +export const DatasourcePluginGroup = (props: DatasourcePluginGroupProps) => { + const pluginIcon = getPluginIcon(props.plugin); + const datasourceIdFromURL = getDatasourceIdFromURL(); + const currentGroup = + !!datasourceIdFromURL && + props.datasources + .map((datasource: Datasource) => datasource.id) + .indexOf(datasourceIdFromURL) > -1; + + return ( + + {props.datasources.map((datasource: Datasource) => { + return ( + + ); + })} + + ); +}; + +export default DatasourcePluginGroup; diff --git a/app/client/src/pages/Editor/Explorer/Entity/AddButton.tsx b/app/client/src/pages/Editor/Explorer/Entity/AddButton.tsx new file mode 100644 index 0000000000..4f5ffde562 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/AddButton.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { EntityTogglesWrapper } from "../ExplorerStyledComponents"; +import styled from "styled-components"; +const Wrapper = styled(EntityTogglesWrapper)` + &&& { + & > span { + line-height: 16px; + } + } +`; +export const EntityAddButton = (props: { + onClick?: () => void; + className?: string; +}) => { + const handleClick = (e: any) => { + props.onClick && props.onClick(); + e.stopPropagation(); + }; + if (!props.onClick) return null; + else { + return ( + + + + + ); + } +}; + +export default EntityAddButton; diff --git a/app/client/src/pages/Editor/Explorer/Entity/Collapse.tsx b/app/client/src/pages/Editor/Explorer/Entity/Collapse.tsx new file mode 100644 index 0000000000..f828fbe345 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/Collapse.tsx @@ -0,0 +1,36 @@ +import React, { ReactNode } from "react"; +import styled from "styled-components"; +import { Collapse } from "@blueprintjs/core"; +import { Colors } from "constants/Colors"; + +const TRACK_WIDTH = 1; + +const CollapsedContainer = styled.div<{ step: number; active?: boolean }>` + overflow: hidden; + &:before { + ${props => (props.active ? `content: ""` : "")}; + width: ${TRACK_WIDTH}px; + background: ${Colors.TUNDORA}; + bottom: ${props => props.theme.spaces[2]}px; + left: ${props => (props.step + 1) * props.theme.spaces[2] + TRACK_WIDTH}px; + top: ${props => -props.theme.spaces[2]}px; + position: absolute; + } +`; +export const EntityCollapse = (props: { + children: ReactNode; + isOpen: boolean; + step: number; + active?: boolean; +}) => { + if (!props.children) return null; + return ( + + + {props.children} + + + ); +}; + +export default EntityCollapse; diff --git a/app/client/src/pages/Editor/Explorer/Entity/CollapseToggle.tsx b/app/client/src/pages/Editor/Explorer/Entity/CollapseToggle.tsx new file mode 100644 index 0000000000..b062f140d8 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/CollapseToggle.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Icon, IconName } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; +import { Colors } from "constants/Colors"; + +export const CollapseToggle = (props: { + isOpen: boolean; + isVisible: boolean; + onClick: () => void; + disabled: boolean; + className: string; +}) => { + const handleClick = (e: any) => { + props.onClick(); + e.stopPropagation(); + }; + const icon: IconName = props.isOpen + ? IconNames.CARET_DOWN + : IconNames.CARET_RIGHT; + + if (!props.isVisible) return ; + return ( + + ); +}; + +export default CollapseToggle; diff --git a/app/client/src/pages/Editor/Explorer/Entity/EntityProperty.tsx b/app/client/src/pages/Editor/Explorer/Entity/EntityProperty.tsx new file mode 100644 index 0000000000..a7b434936b --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/EntityProperty.tsx @@ -0,0 +1,216 @@ +import React, { useRef, MutableRefObject, memo } from "react"; +import styled from "styled-components"; +import HighlightedCode, { + SYNTAX_HIGHLIGHTING_SUPPORTED_LANGUAGES, +} from "components/editorComponents/HighlightedCode"; +import { Popover, PopoverInteractionKind, Classes } from "@blueprintjs/core"; +import { CurrentValueViewer } from "components/editorComponents/CodeEditor/EvaluatedValuePopup"; +import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import useClipboard from "utils/hooks/useClipboard"; +import { Colors } from "constants/Colors"; +import { scrollbarDark } from "constants/DefaultTheme"; +import { ControlIcons } from "icons/ControlIcons"; + +import { ContextMenuPopoverModifiers } from "../helpers"; +import { EntityClassNames } from "."; + +const StyledValue = styled.pre<{ step: number }>` + & { + display: inline; + font-size: 10px; + line-height: 12px; + color: ${Colors.GRAY_CHATEAU}; + padding-left: ${props => + props.step * props.theme.spaces[2] + props.theme.spaces[3]}px; + margin: 0; + } +`; + +const Wrapper = styled.div<{ step: number }>` + &&&& { + margin: ${props => props.theme.spaces[2]}px 0; + + position: relative; + code { + border: none; + box-shadow: none; + padding: 5px 0em; + background: none; + } + & div.clipboard-message { + position: absolute; + left: 0; + height: 100%; + top: 0; + width: 100%; + font-size: 12px; + color: white; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + z-index: 1; + &.success { + background: ${Colors.TUNDORA}; + } + &.error { + background: ${Colors.RED}; + } + } + & > div:first-of-type { + padding-top: 4px; + padding-bottom: 4px; + cursor: pointer; + &:hover { + &:before { + content: "Copy"; + background: ${Colors.TUNDORA}; + opacity: 0.5; + position: absolute; + left: 0; + height: 100%; + top: 0; + width: 100%; + font-size: 12px; + color: white; + display: flex; + justify-content: flex-end; + align-items: center; + text-align: right; + z-index: 1; + } + } + } + + & { + code.${SYNTAX_HIGHLIGHTING_SUPPORTED_LANGUAGES.APPSMITH} { + white-space: pre-wrap; + background: transparent; + font-size: 11px; + overflow-wrap: break-word; + text-shadow: none; + padding-left: ${props => + props.step * props.theme.spaces[2] + props.theme.spaces[3]}px; + } + } + + & .${Classes.POPOVER_WRAPPER} { + display: inline; + vertical-align: middle; + margin-left: 4px; + cursor: pointer; + } + & .${Classes.POPOVER_TARGET} { + display: inline; + } + } +`; + +const StyledPopoverContent = styled.div` + ${scrollbarDark} + background: black; + max-height: 500px; + width: 400px; + padding: 10px; + overflow: auto; + & > div { + max-height: 100%; + & > pre { + overflow: hidden; + } + } + & > pre { + width: 100%; + overflow: hidden; + white-space: pre-wrap; + color: white; + } +`; + +const CollapseIcon = ControlIcons.COLLAPSE_CONTROL; +const collapseIcon = ; + +export type EntityPropertyProps = { + propertyName: string; + entityName: string; + value: string; + step: number; +}; + +const transformedValue = (value: any) => { + if ( + typeof value === "object" || + Array.isArray(value) || + (value && value.length && value.length > 30) + ) { + return JSON.stringify(value).slice(0, 25) + "..."; + } + return `${value}`; +}; + +/* eslint-disable react/display-name */ +export const EntityProperty = memo((props: EntityPropertyProps) => { + const propertyRef: MutableRefObject = useRef(null); + const write = useClipboard(propertyRef); + + const codeText = `{{${props.entityName}.${props.propertyName}}}`; + + const showPopup = + typeof props.value === "object" || + Array.isArray(props.value) || + (props.value && props.value.length && props.value.length > 25); + const isString = typeof props.value === "string"; + + const copyBindingToClipboard = () => { + write(codeText); + }; + + let propertyValue = ( + + {transformedValue(props.value)} + + ); + if (showPopup) { + propertyValue = ( + + + {transformedValue(props.value)} + + + {collapseIcon} + {showPopup && ( + + {!isString && ( + + )} + {isString &&
{props.value}
} +
+ )} +
+
+ ); + } + + return ( + + + {propertyValue} + + ); +}); + +export default EntityProperty; diff --git a/app/client/src/pages/Editor/Explorer/Entity/Loader.tsx b/app/client/src/pages/Editor/Explorer/Entity/Loader.tsx new file mode 100644 index 0000000000..3a41629efd --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/Loader.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import styled from "styled-components"; + +const Wrapper = styled.div` + & { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + } + & > div { + position: absolute; + top: 0; + right: 100%; + bottom: 0; + left: 0; + background: ${props => props.theme.colors.primary}; + width: 0; + animation: borealisBar 1s linear infinite; + } + + @keyframes borealisBar { + 0% { + left: 0%; + right: 100%; + width: 0%; + } + 10% { + left: 0%; + right: 75%; + width: 25%; + } + 90% { + right: 0%; + left: 75%; + width: 25%; + } + 100% { + left: 100%; + right: 0%; + width: 0%; + } + } +`; +export const EntityLoader = (props: { isVisible: boolean }) => { + if (!props.isVisible) return null; + return ( + +
+
+ ); +}; + +export default EntityLoader; diff --git a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx new file mode 100644 index 0000000000..aa2c53f22b --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx @@ -0,0 +1,172 @@ +import React, { useCallback, useMemo } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import styled from "styled-components"; +import EditableText, { + EditInteractionKind, +} from "components/editorComponents/EditableText"; +import { convertToCamelCase } from "utils/helpers"; +import { AppState } from "reducers"; +import { Page, ReduxActionTypes } from "constants/ReduxActionConstants"; +import { Colors } from "constants/Colors"; + +const searchHighlightSpanClassName = "token"; +const searchTokenizationDelimiter = "!!"; + +const Wrapper = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0 4px; + & span.token { + color: ${Colors.OCEAN_GREEN}; + } +`; + +const replace = ( + str: string, + delimiter: string, + className = "token", + keyIndex = 1, +): JSX.Element[] => { + const occurrenceIndex = str.indexOf(delimiter); + if (occurrenceIndex === -1) + return [{str}]; + const sliced = str.slice(occurrenceIndex + delimiter.length); + const nextOccurenceIndex = sliced.indexOf(delimiter); + const rest = str.slice( + occurrenceIndex + delimiter.length + nextOccurenceIndex + delimiter.length, + ); + const token = str.slice( + occurrenceIndex + delimiter.length, + occurrenceIndex + delimiter.length + nextOccurenceIndex, + ); + const final = [ + {str.slice(0, occurrenceIndex)}, + + {token} + , + ].concat(replace(rest, delimiter, className, keyIndex + 1)); + return final; +}; + +export interface EntityNameProps { + name: string; + isEditing?: boolean; + onChange?: (name: string) => void; + updateEntityName: (name: string) => void; + entityId: string; + searchKeyword?: string; + className?: string; +} + +export const EntityName = (props: EntityNameProps) => { + const { name, updateEntityName, searchKeyword } = props; + const dispatch = useDispatch(); + const existingPageNames: string[] = useSelector((state: AppState) => + state.entities.pageList.pages.map((page: Page) => page.pageName), + ); + + const existingWidgetNames: string[] = useSelector((state: AppState) => + Object.values(state.entities.canvasWidgets).map( + widget => widget.widgetName, + ), + ); + + const existingActionNames: string[] = useSelector((state: AppState) => + state.entities.actions.map( + (action: { config: { name: string } }) => action.config.name, + ), + ); + + const hasNameConflict = useCallback( + (newName: string) => + !( + existingPageNames.indexOf(newName) === -1 && + existingActionNames.indexOf(newName) === -1 && + existingWidgetNames.indexOf(newName) === -1 + ), + [existingPageNames, existingActionNames, existingWidgetNames], + ); + + const isInvalidName = useCallback( + (newName: string): string | boolean => { + if (!newName || newName.trim().length === 0) { + return "Please enter a name"; + } else if (newName !== name && hasNameConflict(newName)) { + return `${newName} is already being used.`; + } + return false; + }, + [name, hasNameConflict], + ); + + const handleAPINameChange = useCallback( + (newName: string) => { + if (name && newName !== name && !isInvalidName(newName)) { + dispatch(updateEntityName(newName)); + } + }, + [dispatch, isInvalidName, name, updateEntityName], + ); + + const searchHighlightedName = useMemo(() => { + if (searchKeyword) { + const regex = new RegExp(searchKeyword, "gi"); + const delimited = name.replace(regex, function(str) { + return searchTokenizationDelimiter + str + searchTokenizationDelimiter; + }); + + const final = replace( + delimited, + searchTokenizationDelimiter, + searchHighlightSpanClassName, + ); + return final; + } + return name; + }, [searchKeyword, name]); + + const exitEditMode = useCallback(() => { + dispatch({ + type: ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT, + }); + }, [dispatch]); + + const enterEditMode = useCallback( + () => + props.updateEntityName && + dispatch({ + type: ReduxActionTypes.INIT_EXPLORER_ENTITY_NAME_EDIT, + payload: { + id: props.entityId, + }, + }), + [dispatch, props.entityId, props.updateEntityName], + ); + + if (!props.isEditing) + return ( + + {searchHighlightedName} + + ); + return ( + + + + ); +}; + +export default EntityName; diff --git a/app/client/src/pages/Editor/Explorer/Entity/Placeholder.tsx b/app/client/src/pages/Editor/Explorer/Entity/Placeholder.tsx new file mode 100644 index 0000000000..5a40019dd1 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/Placeholder.tsx @@ -0,0 +1,27 @@ +import React, { ReactNode } from "react"; +import styled from "styled-components"; +import { Colors } from "constants/Colors"; +const Wrapper = styled.div<{ step: number }>` + margin-left: ${props => props.step * props.theme.spaces[2]}px; + width: calc(100% - ${props => props.step * props.theme.spaces[2]}px); + font-size: ${props => props.theme.fontSizes[2]}px; + color: ${Colors.WHITE}; + padding: ${props => props.theme.spaces[4]}px; + text-align: left; + display: flex; + justify-content: flex-start; + align-items: center; +`; + +export const EntityPlaceholder = (props: { + step: number; + children: ReactNode; +}) => { + return ( + +

{props.children}

+
+ ); +}; + +export default EntityPlaceholder; diff --git a/app/client/src/pages/Editor/Explorer/Entity/index.tsx b/app/client/src/pages/Editor/Explorer/Entity/index.tsx new file mode 100644 index 0000000000..e18d7a2dd2 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/index.tsx @@ -0,0 +1,162 @@ +import React, { ReactNode, useState, useEffect } from "react"; +import styled from "styled-components"; +import { Colors } from "constants/Colors"; +import CollapseToggle from "./CollapseToggle"; +import EntityName from "./Name"; +import AddButton from "./AddButton"; +import Collapse from "./Collapse"; +import { useEntityUpdateState, useEntityEditState } from "../hooks"; +import Loader from "./Loader"; +import { Classes } from "@blueprintjs/core"; + +export enum EntityClassNames { + CONTEXT_MENU = "entity-context-menu", + ADD_BUTTON = "t--entity-add-btn", + NAME = "t--entity-name", + COLLAPSE_TOGGLE = "t--entity-collapse-toggle", + WRAPPER = "t--entity", + PROPERTY = "t--entity-property", +} + +const Wrapper = styled.div<{ active: boolean }>` + line-height: ${props => props.theme.lineHeights[2]}px; +`; + +const EntityItem = styled.div<{ + active: boolean; + step: number; + spaced: boolean; +}>` + position: relative; + font-size: 12px; + padding-left: ${props => props.step * props.theme.spaces[2]}px; + background: ${props => (props.active ? Colors.TUNDORA : "none")}; + height: 30px; + width: 100%; + display: inline-grid; + grid-template-columns: ${props => + props.spaced ? "20px auto 1fr 30px" : "8px auto 1fr 30px"}; + border-radius: 0; + color: ${props => (props.active ? Colors.WHITE : Colors.ALTO)}; + cursor: pointer; + align-items: center; + &:hover { + background: ${Colors.TUNDORA}; + } + & .${Classes.POPOVER_TARGET}, & .${Classes.POPOVER_WRAPPER} { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + &&&& .${EntityClassNames.CONTEXT_MENU} { + display: block; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + visibility: hidden; + } + &&&&:hover .${EntityClassNames.CONTEXT_MENU} { + visibility: visible; + } +`; + +export type EntityProps = { + entityId: string; + className?: string; + name: string; + children?: ReactNode; + icon: ReactNode; + disabled?: boolean; + action?: () => void; + active?: boolean; + isDefaultExpanded?: boolean; + createFn?: () => void; + contextMenu?: ReactNode; + searchKeyword?: string; + step: number; + updateEntityName?: (id: string, name: string) => any; + runActionOnExpand?: boolean; +}; + +export const Entity = (props: EntityProps) => { + const [isOpen, open] = useState(!props.disabled && !!props.isDefaultExpanded); + const isUpdating = useEntityUpdateState(props.entityId); + const isEditing = useEntityEditState(props.entityId); + + useEffect(() => { + // If the default state must be expanded, expand to show children + if (props.isDefaultExpanded) { + open(true); + } + if (!props.searchKeyword && !props.isDefaultExpanded) { + open(false); + } + }, [props.isDefaultExpanded, open, props.searchKeyword]); + + const toggleChildren = () => { + // Make sure this entity is enabled before toggling the collpse of children. + !props.disabled && open(!isOpen); + if (props.runActionOnExpand && !isOpen) { + props.action && props.action(); + } + }; + + const updateNameCallback = (name: string) => { + return ( + props.updateEntityName && props.updateEntityName(props.entityId, name) + ); + }; + + const handleClick = () => { + if (props.action) props.action(); + else toggleChildren(); + }; + + return ( + + + + {props.icon} + + + {props.contextMenu} + + + + {props.children} + + + ); +}; + +Entity.displayName = "Entity"; + +export default Entity; diff --git a/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx b/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx new file mode 100644 index 0000000000..a2e88d045a --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import { MenuIcons } from "icons/MenuIcons"; +import { Colors } from "constants/Colors"; +import { WidgetType } from "constants/WidgetConstants"; +import { WidgetIcons } from "icons/WidgetIcons"; +import { Plugin } from "api/PluginApi"; +import ImageAlt from "assets/images/placeholder-image.svg"; +import styled from "styled-components"; +import { + HTTP_METHODS, + HTTP_METHOD_COLOR_MAP, +} from "constants/ApiEditorConstants"; +import { Icon } from "@blueprintjs/core"; + +const ENTITY_ICON_SIZE = 14; + +const PagesIcon = MenuIcons.PAGES_ICON; +export const pageGroupIcon = ( + +); + +const PageIcon = MenuIcons.PAGE_ICON; +export const pageIcon = ( + +); + +export const homePageIcon = ( + +); + +const WidgetIcon = MenuIcons.WIDGETS_COLORED_ICON; +export const widgetIcon = ( + +); + +const ApiIcon = MenuIcons.APIS_COLORED_ICON; +export const apiIcon = ( + +); + +const QueryIcon = MenuIcons.DATASOURCES_COLORED_ICON; +export const queryIcon = ( + +); + +const DataSourceIcon = MenuIcons.DATASOURCES_ICON; +export const datasourceIcon = ( + +); + +export const getWidgetIcon = (type: WidgetType) => { + const WidgetIcon = WidgetIcons[type]; + if (WidgetIcon) + return ; + return null; +}; + +const PluginIcon = styled.img` + height: ${ENTITY_ICON_SIZE}px; + width: ${ENTITY_ICON_SIZE}px; + margin-right: 4px; +`; + +export const getPluginIcon = (plugin?: Plugin) => { + if (plugin && plugin.iconLocation) { + return ; + } + return ; +}; + +const StyledTag = styled.div<{ color: string }>` + font-size: 8px; + width: 40px; + font-weight: 700; + color: #fff; + background: ${props => props.color}; + display: flex; + justify-content: center; + align-items: center; +`; + +export const MethodTag = (props: { type: typeof HTTP_METHODS[number] }) => { + return ( + + {props.type} + + ); +}; diff --git a/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx b/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx new file mode 100644 index 0000000000..91d12b394e --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx @@ -0,0 +1,83 @@ +import React, { forwardRef, Ref } from "react"; +import { Icon, Classes } from "@blueprintjs/core"; +import styled from "styled-components"; +import { Colors } from "constants/Colors"; +import { ENTITY_EXPLORER_SEARCH_ID } from "constants/Explorer"; + +const ExplorerSearchWrapper = styled.div` + display: grid; + grid-template-columns: 30px 1fr 30px; + margin-bottom: 5px; + height: 48px; + justify-content: flex-start; + align-items: center; + position: sticky; + font-size: 12px; + top: 0; + z-index: 1; + background: ${Colors.MINE_SHAFT}; + & { + .${Classes.ICON} { + color: ${Colors.DOVE_GRAY}; + cursor: pointer; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + &:last-of-type { + color: ${Colors.MINE_SHAFT}; + } + } + input { + display: flex; + border: none; + background: none; + padding: 0px 10px 0px 10px; + color: ${Colors.WHITE}; + &::placeholder { + color: ${Colors.DOVE_GRAY}; + } + &:focus { + background: ${Colors.COD_GRAY}; + & ~ div.underline { + width: 100%; + } + & ~ .${Classes.ICON} { + color: ${Colors.WHITE}; + } + } + } + } +`; + +const Underline = styled.div` + position: absolute; + left: 0; + right: 0; + width: 0%; + height: 1px; + background: ${Colors.TIA_MARIA}; + bottom: 0; + transition: width 0.3s ease-in; +`; +/*eslint-disable react/display-name */ +export const ExplorerSearch = forwardRef( + (props: { clear: () => void }, ref: Ref) => { + return ( + + + + + + + ); + }, +); + +export default ExplorerSearch; diff --git a/app/client/src/pages/Editor/Explorer/ExplorerStyledComponents.tsx b/app/client/src/pages/Editor/Explorer/ExplorerStyledComponents.tsx new file mode 100644 index 0000000000..dd44293033 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/ExplorerStyledComponents.tsx @@ -0,0 +1,27 @@ +import styled from "styled-components"; +import { Colors } from "constants/Colors"; + +export const EntityTogglesWrapper = styled.div` + &&& { + width: 100%; + height: 100%; + font-size: ${props => props.theme.fontSizes[4]}px; + display: flex; + justify-content: center; + align-items: center; + color: ${Colors.ALTO}; + svg, + svg path { + fill: ${Colors.ALTO}; + } + + &:hover { + background: ${Colors.SHARK2}; + svg, + svg path { + fill: ${Colors.WHITE}; + } + color: ${Colors.WHITE}; + } + } +`; diff --git a/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx b/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx new file mode 100644 index 0000000000..0bfb7a0a08 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import styled from "styled-components"; +const ICON_SIZE = 14; + +const Wrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + & h1 { + font-size: ${props => props.theme.fontSizes[3]}px; + letter-spacing: 3px; + font-weight: ${props => props.theme.fontWeights[2]}; + } +`; +const ActionIconGroup = styled.div` + width: ${ICON_SIZE * 3}px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const ExplorerTitle = (props: { + isCollapsed: boolean; + onCollapseToggle: () => void; +}) => { + return ( + +

EXPLORER

+ + {/* + */} + +
+ ); +}; + +export default ExplorerTitle; diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx new file mode 100644 index 0000000000..5ac771bfab --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx @@ -0,0 +1,88 @@ +import React, { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import TreeDropdown, { + TreeDropdownOption, +} from "components/editorComponents/actioncreator/TreeDropdown"; +import { noop } from "lodash"; +import ContextMenuTrigger from "../ContextMenuTrigger"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { ContextMenuPopoverModifiers } from "../helpers"; +import { initExplorerEntityNameEdit } from "actions/explorerActions"; + +export const PageContextMenu = (props: { + pageId: string; + name: string; + applicationId: string; + className?: string; + isDefaultPage: boolean; +}) => { + const dispatch = useDispatch(); + + const deletePage = useCallback( + (pageId: string, pageName: string): void => { + dispatch({ + type: ReduxActionTypes.DELETE_PAGE_INIT, + payload: { + id: pageId, + }, + }); + AnalyticsUtil.logEvent("DELETE_PAGE", { + pageName, + }); + }, + [dispatch], + ); + + const setPageAsDefault = useCallback( + (pageId: string, applicationId?: string): void => { + dispatch({ + type: ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_INIT, + payload: { + id: pageId, + applicationId, + }, + }); + }, + [dispatch], + ); + + const editPageName = useCallback( + () => dispatch(initExplorerEntityNameEdit(props.pageId)), + [dispatch, props.pageId], + ); + + const optionTree: TreeDropdownOption[] = [ + { + value: "rename", + onSelect: editPageName, + label: "Edit Name", + }, + ]; + if (!props.isDefaultPage) { + optionTree.push({ + value: "setdefault", + onSelect: () => setPageAsDefault(props.pageId, props.applicationId), + label: "Set as Home Page", + }); + } + optionTree.push({ + value: "delete", + onSelect: () => deletePage(props.pageId, props.name), + label: "Delete", + intent: "danger", + }); + return ( + } + /> + ); +}; + +export default PageContextMenu; diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx new file mode 100644 index 0000000000..dfe928945a --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx @@ -0,0 +1,76 @@ +import React, { useCallback, memo } from "react"; +import { Page } from "constants/ReduxActionConstants"; +import { WidgetTree } from "../Widgets/WidgetEntity"; +import Entity, { EntityClassNames } from "../Entity"; +import { pageIcon, homePageIcon } from "../ExplorerIcons"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "../helpers"; +import { getActionGroups } from "../Actions/helpers"; +import { BUILDER_PAGE_URL } from "constants/routes"; +import history from "utils/history"; +import { updatePage } from "actions/pageActions"; +import PageContextMenu from "./PageContextMenu"; +import ExplorerWidgetGroup from "../Widgets/WidgetGroup"; +import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; + +type ExplorerPageEntityProps = { + page: Page; + isCurrentPage: boolean; + widgets?: WidgetTree; + actions: DataTreeAction[]; + step: number; + searchKeyword?: string; +}; +export const ExplorerPageEntity = memo((props: ExplorerPageEntityProps) => { + const params = useParams(); + const switchPage = useCallback(() => { + if (!props.isCurrentPage && !!params.applicationId) { + history.push(BUILDER_PAGE_URL(params.applicationId, props.page.pageId)); + } + }, [props.isCurrentPage, props.page.pageId, params.applicationId]); + + const contextMenu = ( + + ); + const icon = props.page.isDefault ? homePageIcon : pageIcon; + return ( + + {!(!props.widgets && props.searchKeyword) && ( + + )} + {getActionGroups( + props.page, + props.actions, + props.step + 1, + props.searchKeyword, + )} + + ); +}); + +ExplorerPageEntity.displayName = "ExplorerPageEntity"; + +export default ExplorerPageEntity; diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx new file mode 100644 index 0000000000..641f872bb7 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx @@ -0,0 +1,88 @@ +import React, { useCallback, useMemo } from "react"; +import Entity from "../Entity"; +import { pageGroupIcon } from "../ExplorerIcons"; +import { noop, compact } from "lodash"; +import { useDispatch } from "react-redux"; +import { getNextEntityName } from "utils/AppsmithUtils"; +import { createPage } from "actions/pageActions"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "../helpers"; +import { Page } from "constants/ReduxActionConstants"; +import { WidgetTree } from "../Widgets/WidgetEntity"; +import ExplorerPageEntity from "./PageEntity"; +import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; + +type ExplorerPageGroupProps = { + pages: Page[]; + widgets?: (WidgetTree | undefined)[]; + actions: DataTreeAction[]; + currentPageId?: string; + searchKeyword?: string; + step: number; +}; + +export const ExplorerPageGroup = (props: ExplorerPageGroupProps) => { + const dispatch = useDispatch(); + const params = useParams(); + + const createPageCallback = useCallback(() => { + const name = getNextEntityName( + "Page", + props.pages.map((page: Page) => page.pageName), + ); + dispatch(createPage(params.applicationId, name)); + }, [dispatch, props.pages, params.applicationId]); + + const pageEntityList = useMemo( + () => + compact( + props.pages.map((page: Page) => { + const widgets = props.widgets?.find( + (tree?: WidgetTree) => tree && tree.pageId === page.pageId, + ); + const actions = props.actions.filter( + (action: DataTreeAction & { pageId?: string }) => + action.pageId === page.pageId, + ); + if ( + (!widgets || widgets.length === 0) && + actions.length === 0 && + props.searchKeyword + ) { + return null; + } + return { page, widgets, actions }; + }), + ), + [props.widgets, props.actions, props.pages, props.searchKeyword], + ); + + return ( + + {pageEntityList.map(({ page, widgets, actions }) => { + return ( + + ); + })} + + ); +}; + +export default ExplorerPageGroup; diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx new file mode 100644 index 0000000000..3a7cc5f314 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx @@ -0,0 +1,57 @@ +import React, { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import TreeDropdown from "components/editorComponents/actioncreator/TreeDropdown"; +import ContextMenuTrigger from "../ContextMenuTrigger"; +import { ContextMenuPopoverModifiers } from "../helpers"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { noop } from "lodash"; +import { initExplorerEntityNameEdit } from "actions/explorerActions"; + +export const WidgetContextMenu = (props: { + widgetId: string; + parentId: string; + className?: string; +}) => { + const { parentId, widgetId } = props; + const dispatch = useDispatch(); + const dispatchDelete = useCallback(() => { + dispatch({ + type: ReduxActionTypes.WIDGET_DELETE, + payload: { + widgetId, + parentId, + }, + }); + }, [dispatch, widgetId, parentId]); + + const editWidgetName = useCallback( + () => dispatch(initExplorerEntityNameEdit(widgetId)), + [dispatch, widgetId], + ); + + return ( + } + /> + ); +}; + +export default WidgetContextMenu; diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx new file mode 100644 index 0000000000..c4e6bc9c35 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx @@ -0,0 +1,179 @@ +import React, { useMemo, useCallback, ReactNode, memo } from "react"; +import Entity, { EntityClassNames } from "../Entity"; +import { WidgetProps } from "widgets/BaseWidget"; +import { WidgetTypes, WidgetType } from "constants/WidgetConstants"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "../helpers"; +import { BUILDER_PAGE_URL } from "constants/routes"; +import history from "utils/history"; +import { flashElementById } from "utils/helpers"; +import { useDispatch, useSelector } from "react-redux"; +import { + forceOpenPropertyPane, + showModal, + closeAllModals, +} from "actions/widgetActions"; +import { useWidgetSelection } from "utils/hooks/dragResizeHooks"; +import { AppState } from "reducers"; +import { getWidgetIcon } from "../ExplorerIcons"; +import EntityProperty, { EntityPropertyProps } from "../Entity/EntityProperty"; +import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; +import { isFunction, noop } from "lodash"; +import WidgetContextMenu from "./WidgetContextMenu"; +import { updateWidgetName } from "actions/propertyPaneActions"; + +export type WidgetTree = WidgetProps & { children?: WidgetTree[] }; + +const UNREGISTERED_WIDGETS: WidgetType[] = [WidgetTypes.ICON_WIDGET]; + +const navigateToCanvas = ( + params: ExplorerURLParams, + currentPath: string, + widgetPageId: string, + widgetId: string, +) => { + const canvasEditorURL = `${BUILDER_PAGE_URL( + params.applicationId, + widgetPageId, + )}`; + if (currentPath !== canvasEditorURL) { + history.push(`${canvasEditorURL}#${widgetId}`); + } +}; + +const useWidget = ( + widgetId: string, + widgetType: WidgetType, + pageId: string, + parentModalId?: string, +) => { + const params = useParams(); + const dispatch = useDispatch(); + const { selectWidget } = useWidgetSelection(); + const selectedWidget = useSelector( + (state: AppState) => state.ui.widgetDragResize.selectedWidget, + ); + const isWidgetSelected = useMemo(() => selectedWidget === widgetId, [ + selectedWidget, + widgetId, + ]); + + const navigateToWidget = useCallback(() => { + if (widgetType === WidgetTypes.MODAL_WIDGET) { + dispatch(showModal(widgetId)); + return; + } + if (parentModalId) dispatch(showModal(parentModalId)); + else dispatch(closeAllModals()); + navigateToCanvas(params, window.location.pathname, pageId, widgetId); + flashElementById(widgetId); + if (!isWidgetSelected) selectWidget(widgetId); + dispatch(forceOpenPropertyPane(widgetId)); + }, [ + dispatch, + params, + selectWidget, + widgetType, + widgetId, + parentModalId, + pageId, + isWidgetSelected, + ]); + + return { navigateToWidget, isWidgetSelected }; +}; + +export const getWidgetProperies = ( + widgetProps: any, + step: number, +): Array => { + let config: any = + entityDefinitions[ + widgetProps.type as Exclude< + Partial, + "CANVAS_WIDGET" | "ICON_WIDGET" + > + ]; + + if (isFunction(config)) config = config(widgetProps); + + return Object.keys(config) + .filter(k => k.indexOf("!") === -1) + .map(widgetProperty => { + return { + propertyName: widgetProperty, + entityName: widgetProps.widgetName, + value: widgetProps[widgetProperty], + step, + }; + }); +}; + +export type WidgetEntityProps = { + widgetProps: WidgetTree; + step: number; + pageId: string; + children: ReactNode; + parentModalId?: string; + searchKeyword?: string; + isDefaultExpanded?: boolean; +}; + +export const WidgetEntity = memo((props: WidgetEntityProps) => { + const params = useParams(); + const { navigateToWidget, isWidgetSelected } = useWidget( + props.widgetProps.widgetId, + props.widgetProps.type, + props.pageId, + props.parentModalId, + ); + + if (UNREGISTERED_WIDGETS.indexOf(props.widgetProps.type) > -1) + return ; + + let children: ReactNode = props.children; + if (!props.children) { + children = getWidgetProperies( + props.widgetProps, + props.step + 1, + ).map((widgetProperty: EntityPropertyProps) => ( + + )); + } + + const contextMenu = ( + + ); + + return ( + + {children} + + ); +}); + +WidgetEntity.displayName = "WidgetEntity"; + +export default WidgetEntity; diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx new file mode 100644 index 0000000000..c33ecdb560 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx @@ -0,0 +1,188 @@ +import React, { useMemo, memo } from "react"; +import { useSelector } from "react-redux"; +import EntityPlaceholder from "../Entity/Placeholder"; +import Entity from "../Entity"; +import { widgetIcon } from "../ExplorerIcons"; +import WidgetEntity, { WidgetTree } from "./WidgetEntity"; +import { + WidgetTypes, + MAIN_CONTAINER_WIDGET_ID, +} from "constants/WidgetConstants"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "../helpers"; +import { BUILDER_PAGE_URL } from "constants/routes"; +import { Link } from "react-router-dom"; +import styled from "styled-components"; +import { AppState } from "reducers"; + +const getWidgetEntity = ( + entity: any, + step: number, + widgetsPageId: string, + parentModalId?: string, + searchKeyword?: string, + widgetIdsToExpand?: string[], +) => { + if (!entity) { + if (searchKeyword) { + return ; + } else { + return; + } + } + if (entity.type === WidgetTypes.CANVAS_WIDGET) { + if (!entity.children || entity.children.length === 0) return; + return entity.children.map((child: any) => + getWidgetEntity( + child, + step + 1, + widgetsPageId, + parentModalId, + searchKeyword, + widgetIdsToExpand, + ), + ); + } + const childEntities = + entity.children && + entity.children.length > 0 && + entity.children.map((child: any) => + getWidgetEntity( + child, + step, + widgetsPageId, + entity.type === WidgetTypes.MODAL_WIDGET ? entity.widgetId : undefined, + searchKeyword, + widgetIdsToExpand, + ), + ); + + const shouldExpandWidgetEntity = + widgetIdsToExpand && widgetIdsToExpand.indexOf(entity.widgetId) > -1 + ? true + : undefined; + return ( + + {childEntities} + + ); +}; + +const useWidgetExpandList = ( + widgetPageId: string, + currentPageId: string, + selectedWidget?: string, +) => { + const canvasWidgets = useSelector( + (state: AppState) => state.entities.canvasWidgets, + ); + + return useMemo(() => { + const widgetIdsExpandList = []; + if (currentPageId === widgetPageId && !!selectedWidget) { + // Make sure that the selected widget exists in canvasWidgets + let widgetId = canvasWidgets[selectedWidget] + ? canvasWidgets[selectedWidget].parentId + : undefined; + // If there is a parentId for the selectedWidget + if (widgetId) { + // Keep including the parent until we reach the main container + while (widgetId !== MAIN_CONTAINER_WIDGET_ID) { + widgetIdsExpandList.push(widgetId); + if (canvasWidgets[widgetId] && canvasWidgets[widgetId].parentId) + widgetId = canvasWidgets[widgetId].parentId; + else break; + } + } + } + return widgetIdsExpandList; + }, [canvasWidgets, widgetPageId, currentPageId, selectedWidget]); +}; + +type ExplorerWidgetGroupProps = { + pageId: string; + step: number; + widgets: WidgetTree | null; + searchKeyword?: string; +}; + +const StyledLink = styled(Link)` + & { + color: ${props => props.theme.colors.primary}; + &:hover { + color: ${props => props.theme.colors.primary}; + } + } +`; + +export const ExplorerWidgetGroup = memo((props: ExplorerWidgetGroupProps) => { + const params = useParams(); + const selectedWidget = useSelector( + (state: AppState) => state.ui.widgetDragResize.selectedWidget, + ); + + const widgetIdsExpandList = useWidgetExpandList( + props.pageId, + params.pageId, + selectedWidget, + ); + + let childNode = getWidgetEntity( + props.widgets, + props.step, + props.pageId, + undefined, + props.searchKeyword, + widgetIdsExpandList, + ); + if (!childNode && !props.searchKeyword) { + childNode = ( + + No widgets yet. Please{" "} + {params.pageId !== props.pageId ? ( + + + switch to this page + + , then  + + ) : ( + " " + )} + click the Widgets navigation menu icon on the left to + drag and drop widgets + + ); + } + return ( + + {childNode} + + ); +}); + +ExplorerWidgetGroup.displayName = "ExplorerWidgetGroup"; + +export default ExplorerWidgetGroup; diff --git a/app/client/src/pages/Editor/Explorer/helpers.tsx b/app/client/src/pages/Editor/Explorer/helpers.tsx new file mode 100644 index 0000000000..1551b2070c --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/helpers.tsx @@ -0,0 +1,50 @@ +import { IPopoverSharedProps } from "@blueprintjs/core"; +import { matchPath } from "react-router"; +import { + API_EDITOR_ID_URL, + QUERIES_EDITOR_ID_URL, + DATA_SOURCES_EDITOR_ID_URL, +} from "constants/routes"; +export const ContextMenuPopoverModifiers: IPopoverSharedProps["modifiers"] = { + offset: { + enabled: true, + offset: 200, + }, + + preventOverflow: { + enabled: true, + boundariesElement: "viewport", + }, + hide: { + enabled: false, + }, +}; + +export type ExplorerURLParams = { + applicationId: string; + pageId: string; +}; + +export const getActionIdFromURL = () => { + const apiMatch = matchPath<{ apiId: string }>(window.location.pathname, { + path: API_EDITOR_ID_URL(), + }); + if (apiMatch?.params?.apiId) { + return apiMatch.params.apiId; + } + const match = matchPath<{ queryId: string }>(window.location.pathname, { + path: QUERIES_EDITOR_ID_URL(), + }); + if (match?.params?.queryId) { + return match.params.queryId; + } +}; + +export const getDatasourceIdFromURL = () => { + const match = matchPath<{ datasourceId: string }>(window.location.pathname, { + path: DATA_SOURCES_EDITOR_ID_URL(), + }); + if (match?.params?.datasourceId) { + return match.params.datasourceId; + } +}; diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts new file mode 100644 index 0000000000..b6f0f03477 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/hooks.ts @@ -0,0 +1,220 @@ +import { + useEffect, + MutableRefObject, + useState, + useMemo, + useCallback, +} from "react"; +import { useSelector } from "react-redux"; +import { AppState } from "reducers"; +import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; +import { + ENTITY_TYPE, + DataTreeEntity, + DataTree, + DataTreeAction, +} from "entities/DataTree/dataTreeFactory"; +import { compact } from "lodash"; +import { Datasource } from "api/DatasourcesApi"; +import { debounce } from "lodash"; +import { WidgetProps } from "widgets/BaseWidget"; +import { evaluateDataTreeWithFunctions } from "selectors/dataTreeSelectors"; +import { ActionData } from "@appsmith/reducers/entityReducers/actionsReducer"; +import log from "loglevel"; + +const findWidgets = (widgets: WidgetProps, keyword: string) => { + if (widgets.children) { + widgets.children = compact( + widgets.children.map((widget: WidgetProps) => + findWidgets(widget, keyword), + ), + ); + return widgets.children.length > 0 ? widgets : undefined; + } + if (widgets.widgetName.toLowerCase().indexOf(keyword) > -1) return widgets; +}; + +const findActions = (actions: Array, keyword: string) => { + return actions.filter( + (action: DataTreeAction) => action.name.toLowerCase().indexOf(keyword) > -1, + ); +}; + +const findDataSources = (dataSources: Datasource[], keyword: string) => { + return dataSources.filter( + (dataSource: Datasource) => + dataSource.name.toLowerCase().indexOf(keyword) > -1, + ); +}; + +export const useFilteredEntities = ( + ref: MutableRefObject, +) => { + const start = performance.now(); + const [searchKeyword, setSearchKeyword] = useState(null); + + const dataTree: DataTree = useSelector(evaluateDataTreeWithFunctions); + const pages = useSelector((state: AppState) => { + return state.entities.pageList.pages; + }); + + const currentPageId = useSelector((state: AppState) => { + return state.entities.pageList.currentPageId; + }); + + const dataSources = useSelector((state: AppState) => { + return state.entities.datasources.list; + }); + const plugins = useSelector((state: AppState) => { + return state.entities.plugins.list; + }); + + const currentPageWidgetEntities = useMemo(() => { + const canvasWidgets: { [id: string]: any } = {}; + Object.values(dataTree).forEach( + ( + entity: DataTreeEntity & { + ENTITY_TYPE?: ENTITY_TYPE; + widgetId?: string; + }, + ) => { + if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET && entity.widgetId) { + canvasWidgets[entity.widgetId] = entity; + } + }, + ); + + const widgetTree = CanvasWidgetsNormalizer.denormalize("0", { + canvasWidgets, + }); + widgetTree.pageId = currentPageId; + + return searchKeyword !== null + ? findWidgets(widgetTree, searchKeyword.toLowerCase()) + : widgetTree; + }, [searchKeyword, dataTree, currentPageId]); + + const allPageDSLs = useSelector((state: AppState) => state.ui.pageDSLs); + const otherPagesWidgetEntities = useMemo(() => { + return Object.keys(allPageDSLs) + .filter((pageId: string) => pageId !== currentPageId) + .map((pageId: string) => { + const tree = allPageDSLs[pageId]; + tree.pageId = pageId; + + return searchKeyword !== null + ? findWidgets(tree, searchKeyword.toLowerCase()) + : tree; + }); + }, [searchKeyword, allPageDSLs, currentPageId]); + + const actions = useMemo( + () => + Object.values(dataTree).filter( + (entity: DataTreeEntity & { ENTITY_TYPE?: ENTITY_TYPE }) => + entity.ENTITY_TYPE === ENTITY_TYPE.ACTION, + ), + [dataTree], + ); + + const allAppActions = useSelector( + (state: AppState) => state.entities.actions, + ); + + const actionEntities = useMemo(() => { + const otherPageDataTreeActions: DataTreeAction[] = allAppActions + .filter((action: ActionData) => action.config.pageId !== currentPageId) + .map((action: ActionData) => ({ + isLoading: action.isLoading, + actionId: action.config.id, + pluginType: action.config.pluginType, + name: action.config.name, + pageId: action.config.pageId, + run: {}, + dynamicBindingPathList: action.config.dynamicBindingPathList, + ENTITY_TYPE: ENTITY_TYPE.ACTION, + data: action.data || {}, + config: { + paginationType: action.config.actionConfiguration.paginationType, + timeoutInMillisecond: + action.config.actionConfiguration.timeoutInMillisecond, + httpMethod: action.config.actionConfiguration.httpMethod, + }, + })); + const currentPageActions = actions.map(action => ({ + ...action, + pageId: currentPageId, + })); + const allActions = [...currentPageActions, ...otherPageDataTreeActions]; + return searchKeyword !== null + ? findActions(allActions as DataTreeAction[], searchKeyword.toLowerCase()) + : allActions; + }, [searchKeyword, actions, allAppActions, currentPageId]); + + const datasourceEntities = useMemo( + () => + searchKeyword !== null + ? findDataSources(dataSources, searchKeyword.toLowerCase()) + : dataSources, + [searchKeyword, dataSources], + ); + + const search = debounce((e: any) => { + const keyword = e.target.value; + if (keyword.trim().length > 0) { + setSearchKeyword(keyword); + } else { + setSearchKeyword(null); + } + }, 300); + + const event = new Event("cleared"); + useEffect(() => { + const el: HTMLInputElement | null = ref.current; + + el?.addEventListener("keydown", search); + el?.addEventListener("cleared", search); + return () => { + el?.removeEventListener("keydown", search); + el?.removeEventListener("cleared", search); + }; + }, [ref, search]); + + const clearSearch = useCallback(() => { + const el: HTMLInputElement | null = ref.current; + + if (el && el.value.trim().length > 0) { + el.value = ""; + el?.dispatchEvent(event); + } + }, [ref, event]); + const allWidgetEntities = useMemo( + () => compact([currentPageWidgetEntities, ...otherPagesWidgetEntities]), + [currentPageWidgetEntities, otherPagesWidgetEntities], + ); + + const stop = performance.now(); + log.debug("Explorer hook props calculations took", stop - start, "ms"); + return { + widgets: allWidgetEntities, + actions: actionEntities as DataTreeAction[], + dataSources: datasourceEntities, + currentPageId, + plugins, + pages, + searchKeyword: searchKeyword ?? undefined, + clearSearch, + }; +}; + +export const useEntityUpdateState = (entityId: string) => { + return useSelector( + (state: AppState) => state.ui.explorer.updatingEntity === entityId, + ); +}; + +export const useEntityEditState = (entityId: string) => { + return useSelector( + (state: AppState) => state.ui.explorer.editingEntityName === entityId, + ); +}; diff --git a/app/client/src/pages/Editor/Explorer/index.tsx b/app/client/src/pages/Editor/Explorer/index.tsx new file mode 100644 index 0000000000..68f6b38325 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/index.tsx @@ -0,0 +1,104 @@ +import React, { useRef, MutableRefObject, useEffect } from "react"; +import styled from "styled-components"; +import Divider from "components/editorComponents/Divider"; +import { useFilteredEntities } from "./hooks"; +import Search from "./ExplorerSearch"; +import ExplorerPageGroup from "./Pages/PageGroup"; +import ExplorerDatasourcesGroup from "./Datasources/DatasourcesGroup"; +import { scrollbarDark } from "constants/DefaultTheme"; +import { NonIdealState, Classes } from "@blueprintjs/core"; +import { ENTITY_EXPLORER_SEARCH_LOCATION_HASH } from "constants/Explorer"; +import { useLocation } from "react-router"; + +const Wrapper = styled.div` + height: 100%; + overflow-y: scroll; + ${scrollbarDark}; +`; + +const NoResult = styled(NonIdealState)` + &.${Classes.NON_IDEAL_STATE} { + height: auto; + } +`; + +const EntityExplorer = () => { + const searchInputRef: MutableRefObject = useRef( + null, + ); + const { + widgets, + actions, + dataSources, + currentPageId, + pages, + plugins, + searchKeyword, + clearSearch, + } = useFilteredEntities(searchInputRef); + + const location = useLocation(); + useEffect(() => { + if (location.hash === ENTITY_EXPLORER_SEARCH_LOCATION_HASH) { + searchInputRef.current?.focus(); + } + }, [location, searchInputRef]); + + const explorerPageGroup = ( + + ); + + const datasourcesGroup = ( + + ); + + const noResults = + widgets.length === 0 && + actions.length === 0 && + dataSources.length === 0 && + !!searchKeyword; + + const noPageEntities = + widgets.length === 0 && actions.length === 0 && !!searchKeyword; + + const noDatsourceEntities = dataSources.length === 0 && !!searchKeyword; + + const noResultMessage = ( + + ); + + return ( + + + {!noPageEntities && explorerPageGroup} + {noResults && noResultMessage} + + {!noDatsourceEntities && datasourcesGroup} + + ); +}; + +EntityExplorer.displayName = "EntityExplorer"; + +EntityExplorer.whyDidYouRender = { + logOnDifferentValues: false, +}; + +export default EntityExplorer; diff --git a/app/client/src/pages/Editor/MainContainer.tsx b/app/client/src/pages/Editor/MainContainer.tsx index bf5ad9f9fb..fcf2889710 100644 --- a/app/client/src/pages/Editor/MainContainer.tsx +++ b/app/client/src/pages/Editor/MainContainer.tsx @@ -1,6 +1,6 @@ import React from "react"; import EditorsRouter from "./routes"; -import Sidebar from "./Sidebar"; +import Navbar from "./Navbar"; import WidgetsEditor from "./WidgetsEditor"; import styled from "styled-components"; @@ -17,7 +17,7 @@ const EditorContainer = styled.div` const MainContainer = () => { return ( - + @@ -26,4 +26,6 @@ const MainContainer = () => { ); }; +MainContainer.displayName = "MainContainer"; + export default MainContainer; diff --git a/app/client/src/pages/Editor/Navbar.tsx b/app/client/src/pages/Editor/Navbar.tsx new file mode 100644 index 0000000000..b1ed90beb8 --- /dev/null +++ b/app/client/src/pages/Editor/Navbar.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import styled from "styled-components"; +import SidebarComponent from "components/editorComponents/Sidebar"; +import NavBarItem from "components/editorComponents/NavBarItem"; +import { + BuilderRouteParams, + BUILDER_PAGE_URL, + WIDGETS_URL, +} from "constants/routes"; +import { Colors } from "constants/Colors"; +import { MenuIcons } from "icons/MenuIcons"; + +const Wrapper = styled.div` + display: grid; + grid-template-columns: 1fr 5fr; + width: ${props => props.theme.sidebarWidth}; + box-shadow: 0px 1px 3px ${Colors.MINE_SHAFT}; + background-color: ${Colors.MINE_SHAFT}; + z-index: 3; +`; + +const NavBar = styled.div` + background-color: ${Colors.MINE_SHAFT}; + color: ${props => props.theme.colors.textOnDarkBG}; + display: flex; + flex-direction: column; + box-shadow: 0px 0px 4px ${Colors.CODE_GRAY}; +`; + +const Navbar = () => { + const params = useParams(); + + return ( + + + { + // Currently, the explorer shows on all paths except for the + // WIDGETS_URL path + + // get the applicationId and pageId from the current location pathname + const found = current.match( + /^\/applications\/(?\w+)\/pages\/(?\w+)\//, + ); + // In this case: expected = BUILDER_PAGE_URL(applicationId, pageId) + // If current url begins with expected url AND + // If the current url isn't the WIDGETS_URL THEN + // this is an explorer sidebar path + return ( + current.indexOf(expected) === 0 && + current !== + WIDGETS_URL(found?.groups?.applicationId, found?.groups?.pageId) + ); + }} + /> + expected === current} + /> + + + + ); +}; +Navbar.displayName = "Navbar"; + +Navbar.whyDidYouRender = { + logOnDifferentValues: false, + customName: "MainSidebar", +}; + +export default Navbar; diff --git a/app/client/src/pages/Editor/PageListSidebar/CreatePageButton.tsx b/app/client/src/pages/Editor/PageListSidebar/CreatePageButton.tsx deleted file mode 100644 index e2b56d8758..0000000000 --- a/app/client/src/pages/Editor/PageListSidebar/CreatePageButton.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { ReactNode, useState } from "react"; -import styled from "styled-components"; -import EditableText, { - EditInteractionKind, -} from "components/editorComponents/EditableText"; -import { PageListItemCSS } from "./PageListItem"; -import Button from "components/editorComponents/Button"; - -/** Create Page Button / Input */ - -const CreatePageWrapper = styled.div` - ${PageListItemCSS}; - & { - button, - button span { - color: ${props => props.theme.colors.textOnDarkBG}; - } - } -`; - -type CreatePageProps = { - onCreatePage: (name: string) => void; - defaultValue: string; - loading: boolean; -}; -const CreatePageButton = (props: CreatePageProps) => { - const [isCreatePageFieldVisible, showCreatePageField] = useState(false); - const onChange = (value: string) => { - props.onCreatePage(value); - showCreatePageField(false); - }; - let content: ReactNode; - if (isCreatePageFieldVisible) { - content = ( - - ); - } else { - content = ( -