From 774c8ca0c51b1b8c86707a84f8fedfd34bea6998 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Wed, 18 Dec 2024 14:34:15 +0530 Subject: [PATCH 01/47] test: CGS discard tests (#38206) ## Description > [!TIP] > _Add a TL;DR when the description is longer than 500 words or extremely technical (helps the content, marketing, and DevRel team)._ > > _Please also include relevant motivation and context. List any dependencies that are required for this change. Add links to Notion, Figma or any other documents that might be relevant to the PR._ Fixes #`Issue Number` _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="" ### :mag: Cypress test results > [!CAUTION] > If you modify the content in this section, you are likely to disrupt the CI result for your PR. ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **Tests** - Introduced a new test suite for the Git discard functionality, covering various scenarios to ensure application integrity during Git operations. --- .../server/git/ops/GitDiscardTests.java | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/git/ops/GitDiscardTests.java diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ops/GitDiscardTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ops/GitDiscardTests.java new file mode 100644 index 000000000000..7ca39f4bcb55 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ops/GitDiscardTests.java @@ -0,0 +1,293 @@ +package com.appsmith.server.git.ops; + +import com.appsmith.external.dtos.GitStatusDTO; +import com.appsmith.external.dtos.MergeStatusDTO; +import com.appsmith.external.git.handler.FSGitHandler; +import com.appsmith.server.applications.base.ApplicationService; +import com.appsmith.server.constants.ArtifactType; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.GitArtifactMetadata; +import com.appsmith.server.domains.GitAuth; +import com.appsmith.server.domains.GitProfile; +import com.appsmith.server.domains.User; +import com.appsmith.server.domains.Workspace; +import com.appsmith.server.dtos.ApplicationJson; +import com.appsmith.server.dtos.ArtifactExchangeJson; +import com.appsmith.server.dtos.GitConnectDTO; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.git.central.CentralGitService; +import com.appsmith.server.git.central.GitHandlingService; +import com.appsmith.server.git.central.GitType; +import com.appsmith.server.helpers.CommonGitFileUtils; +import com.appsmith.server.helpers.MockPluginExecutor; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.migrations.JsonSchemaMigration; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.UserService; +import com.appsmith.server.services.WorkspaceService; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.test.context.support.WithUserDetails; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + +@SpringBootTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class GitDiscardTests { + + @Autowired + CentralGitService centralGitService; + + @Autowired + ApplicationService applicationService; + + @Autowired + ApplicationPageService applicationPageService; + + @Autowired + WorkspaceService workspaceService; + + @Autowired + UserService userService; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + JsonSchemaMigration jsonSchemaMigration; + + @SpyBean + FSGitHandler fsGitHandler; + + @SpyBean + CommonGitFileUtils commonGitFileUtils; + + @SpyBean + GitHandlingService gitHandlingService; + + @SpyBean + PluginExecutorHelper pluginExecutorHelper; + + private Application createApplicationConnectedToGit(String name, String branchName, String workspaceId) + throws IOException { + + Mockito.doReturn(Mono.just(new MockPluginExecutor())) + .when(pluginExecutorHelper) + .getPluginExecutor(any()); + + if (StringUtils.isEmpty(branchName)) { + branchName = "foo"; + } + Mockito.doReturn(Mono.just(branchName)) + .when(fsGitHandler) + .cloneRemoteIntoArtifactRepo(any(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(Mono.just("commit")) + .when(fsGitHandler) + .commitArtifact( + any(Path.class), + Mockito.anyString(), + Mockito.anyString(), + Mockito.anyString(), + Mockito.anyBoolean(), + Mockito.anyBoolean()); + Mockito.doReturn(Mono.just(true)).when(fsGitHandler).checkoutToBranch(any(Path.class), Mockito.anyString()); + Mockito.doReturn(Mono.just("success")) + .when(fsGitHandler) + .pushApplication(any(Path.class), any(), any(), any(), any()); + Mockito.doReturn(Mono.just(true)).when(commonGitFileUtils).checkIfDirectoryIsEmpty(any(Path.class)); + Mockito.doReturn(Mono.just(Paths.get("textPath"))) + .when(commonGitFileUtils) + .initializeReadme(any(Path.class), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(Mono.just(Paths.get("path"))) + .when(commonGitFileUtils) + .saveArtifactToLocalRepoWithAnalytics(any(Path.class), any(), Mockito.anyString()); + + Application testApplication = new Application(); + testApplication.setName(name); + testApplication.setWorkspaceId(workspaceId); + Application application1 = + applicationPageService.createApplication(testApplication).block(); + + GitArtifactMetadata gitArtifactMetadata = new GitArtifactMetadata(); + GitAuth gitAuth = new GitAuth(); + gitAuth.setPublicKey("testkey"); + gitAuth.setPrivateKey("privatekey"); + gitArtifactMetadata.setGitAuth(gitAuth); + gitArtifactMetadata.setDefaultApplicationId(application1.getId()); + gitArtifactMetadata.setRepoName("testRepo"); + application1.setGitApplicationMetadata(gitArtifactMetadata); + application1 = applicationService.save(application1).block(); + + PageDTO page = new PageDTO(); + page.setName("New Page"); + page.setApplicationId(application1.getId()); + applicationPageService.createPage(page).block(); + + GitProfile gitProfile = new GitProfile(); + gitProfile.setAuthorEmail("test@email.com"); + gitProfile.setAuthorName("testUser"); + GitConnectDTO gitConnectDTO = new GitConnectDTO(); + gitConnectDTO.setRemoteUrl("git@github.com:test/testRepo.git"); + gitConnectDTO.setGitProfile(gitProfile); + return centralGitService + .connectArtifactToGit( + application1.getId(), gitConnectDTO, "baseUrl", ArtifactType.APPLICATION, GitType.FILE_SYSTEM) + .map(artifact -> (Application) artifact) + .block(); + } + + private Mono createArtifactJson(String filePath) throws IOException { + + ClassPathResource classPathResource = new ClassPathResource(filePath); + + String artifactJson = classPathResource.getContentAsString(Charset.defaultCharset()); + + Class exchangeJsonType = ApplicationJson.class; + + ArtifactExchangeJson artifactExchangeJson = + objectMapper.copy().disable(MapperFeature.USE_ANNOTATIONS).readValue(artifactJson, exchangeJsonType); + + return jsonSchemaMigration.migrateArtifactExchangeJsonToLatestSchema(artifactExchangeJson, null, null); + } + + @Test + @WithUserDetails(value = "api_user") + public void discardChanges_whenUpstreamChangesAvailable_discardsSuccessfully() throws IOException { + User apiUser = userService.findByEmail("api_user").block(); + Workspace toCreate = new Workspace(); + toCreate.setName("Workspace_" + UUID.randomUUID()); + Workspace workspace = + workspaceService.create(toCreate, apiUser, Boolean.FALSE).block(); + assertThat(workspace).isNotNull(); + + Application application = + createApplicationConnectedToGit("discard-changes", "discard-change-branch", workspace.getId()); + MergeStatusDTO mergeStatusDTO = new MergeStatusDTO(); + mergeStatusDTO.setStatus("2 commits pulled"); + mergeStatusDTO.setMergeAble(true); + + ArtifactExchangeJson artifactExchangeJson = createArtifactJson( + "test_assets/ImportExportServiceTest/valid-application-without-action-collection.json") + .block(); + ((Application) artifactExchangeJson.getArtifact()).setName("discardChangesAvailable"); + + GitStatusDTO gitStatusDTO = new GitStatusDTO(); + gitStatusDTO.setAheadCount(2); + gitStatusDTO.setBehindCount(0); + gitStatusDTO.setIsClean(true); + + Mockito.doReturn(Mono.just(Paths.get("path"))) + .when(commonGitFileUtils) + .saveArtifactToLocalRepoWithAnalytics(any(Path.class), any(), Mockito.anyString()); + Mockito.doReturn(Mono.just(artifactExchangeJson)) + .when(gitHandlingService) + .recreateArtifactJsonFromLastCommit(Mockito.any()); + Mockito.doReturn(Mono.just(true)).when(fsGitHandler).rebaseBranch(any(Path.class), Mockito.anyString()); + + Mono applicationMono = centralGitService + .discardChanges(application.getId(), ArtifactType.APPLICATION, GitType.FILE_SYSTEM) + .map(artifact -> (Application) artifact); + + StepVerifier.create(applicationMono) + .assertNext(application1 -> { + assertThat(application1.getPages()).isNotEqualTo(application.getPages()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void discardChanges_whenCancelledMidway_discardsSuccessfully() throws IOException, GitAPIException { + User apiUser = userService.findByEmail("api_user").block(); + Workspace toCreate = new Workspace(); + toCreate.setName("Workspace_" + UUID.randomUUID()); + Workspace workspace = + workspaceService.create(toCreate, apiUser, Boolean.FALSE).block(); + assertThat(workspace).isNotNull(); + + Application application = createApplicationConnectedToGit( + "discard-changes-midway", "discard-change-midway-branch", workspace.getId()); + MergeStatusDTO mergeStatusDTO = new MergeStatusDTO(); + mergeStatusDTO.setStatus("Nothing to fetch from remote. All changes are upto date."); + mergeStatusDTO.setMergeAble(true); + + ArtifactExchangeJson artifactExchangeJson = createArtifactJson( + "test_assets/ImportExportServiceTest/valid-application-without-action-collection.json") + .block(); + ((Application) artifactExchangeJson.getArtifact()).setName("discard-changes-midway"); + + GitStatusDTO gitStatusDTO = new GitStatusDTO(); + gitStatusDTO.setAheadCount(0); + gitStatusDTO.setBehindCount(0); + gitStatusDTO.setIsClean(true); + + Mockito.doReturn(Mono.just(Paths.get("path"))) + .when(commonGitFileUtils) + .saveArtifactToLocalRepoWithAnalytics(any(Path.class), any(), Mockito.anyString()); + Mockito.doReturn(Mono.just(artifactExchangeJson)) + .when(gitHandlingService) + .recreateArtifactJsonFromLastCommit(Mockito.any()); + Mockito.doReturn(Mono.just(mergeStatusDTO)) + .when(fsGitHandler) + .pullApplication( + any(Path.class), + Mockito.anyString(), + Mockito.anyString(), + Mockito.anyString(), + Mockito.anyString()); + Mockito.doReturn(Mono.just(gitStatusDTO)).when(fsGitHandler).getStatus(any(Path.class), Mockito.anyString()); + Mockito.doReturn(Mono.just("fetched")) + .when(fsGitHandler) + .fetchRemote( + any(Path.class), + Mockito.anyString(), + Mockito.anyString(), + eq(true), + Mockito.anyString(), + Mockito.anyBoolean()); + + centralGitService + .discardChanges(application.getId(), ArtifactType.APPLICATION, GitType.FILE_SYSTEM) + .map(artifact -> (Application) artifact) + .timeout(Duration.ofNanos(100)) + .subscribe(); + + // Wait for git clone to complete + Mono applicationFromDbMono = Mono.just(application).flatMap(application1 -> { + try { + // Before fetching the git connected application, sleep for 5 seconds to ensure that the clone + // completes + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return applicationService.getById(application1.getId()); + }); + + StepVerifier.create(applicationFromDbMono) + .assertNext(application1 -> { + assertThat(application1).isNotEqualTo(application); + }) + .verifyComplete(); + } +} From 836b544ae4a1140685d76d85e70a96695eb7e7c8 Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Wed, 18 Dec 2024 14:39:00 +0530 Subject: [PATCH 02/47] chore: refactor datasource storage to use custom repo method (#38143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As part of transaction support in PG, we are moving from using the jpa methods for database operations. This PR is refactoring the code to use custom repository class for DatasourceStorageRepository from the default CrudRepository. ## Automation /ok-to-test tags="@tag.ImportExport" ### :mag: Cypress test results > [!WARNING] > Workflow run: > Commit: 977d50e213b49f5f6180dc8f72326f1fdd6a156a > Cypress dashboard. > Tags: @tag.Datasource > Spec: > It seems like **no tests ran** πŸ˜”. We are not able to recognize it, please check workflow here. >
Sat, 14 Dec 2024 11:00:53 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Added methods to retrieve `DatasourceStorage` by `datasourceId` and `environmentId`. - Enhanced functionality for fetching `DatasourceStorage` based on `datasourceId`. - **Bug Fixes** - Removed the ability to find a datasource by both its ID and environment ID in the repository interface. - **Documentation** - Updated repository annotations to reflect new functionalities and integrations. --- .../CustomDatasourceStorageRepositoryCE.java | 8 ++++++- ...stomDatasourceStorageRepositoryCEImpl.java | 21 ++++++++++++++++++- .../ce/DatasourceStorageRepositoryCE.java | 12 +++++------ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCE.java index 0a1f3951b5cc..a2ad21d3a4bf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCE.java @@ -2,5 +2,11 @@ import com.appsmith.external.models.DatasourceStorage; import com.appsmith.server.repositories.AppsmithRepository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; -public interface CustomDatasourceStorageRepositoryCE extends AppsmithRepository {} +public interface CustomDatasourceStorageRepositoryCE extends AppsmithRepository { + Mono findByDatasourceIdAndEnvironmentId(String datasourceId, String environmentId); + + Flux findByDatasourceId(String datasourceId); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCEImpl.java index 1d0b2809857b..2115da8b3bca 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageRepositoryCEImpl.java @@ -1,7 +1,26 @@ package com.appsmith.server.repositories.ce; import com.appsmith.external.models.DatasourceStorage; +import com.appsmith.server.helpers.ce.bridge.Bridge; +import com.appsmith.server.helpers.ce.bridge.BridgeQuery; import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; public class CustomDatasourceStorageRepositoryCEImpl extends BaseAppsmithRepositoryImpl - implements CustomDatasourceStorageRepositoryCE {} + implements CustomDatasourceStorageRepositoryCE { + @Override + public Mono findByDatasourceIdAndEnvironmentId(String datasourceId, String environmentId) { + final BridgeQuery q = Bridge.equal( + DatasourceStorage.Fields.datasourceId, datasourceId) + .equal(DatasourceStorage.Fields.environmentId, environmentId); + return queryBuilder().criteria(q).one(); + } + + @Override + public Flux findByDatasourceId(String datasourceId) { + final BridgeQuery q = + Bridge.equal(DatasourceStorage.Fields.datasourceId, datasourceId); + return queryBuilder().criteria(q).all(); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageRepositoryCE.java index 390f61e1c97b..71caec3ced47 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageRepositoryCE.java @@ -2,11 +2,9 @@ import com.appsmith.external.models.DatasourceStorage; import com.appsmith.server.repositories.BaseRepository; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import com.appsmith.server.repositories.CustomDatasourceStorageRepository; +import org.springframework.stereotype.Repository; -public interface DatasourceStorageRepositoryCE extends BaseRepository { - Flux findByDatasourceId(String datasourceId); - - Mono findByDatasourceIdAndEnvironmentId(String datasourceId, String environmentId); -} +@Repository +public interface DatasourceStorageRepositoryCE + extends BaseRepository, CustomDatasourceStorageRepository {} From fa96b4ac40583577b2d322c01f83180e37852687 Mon Sep 17 00:00:00 2001 From: Sagar Khalasi Date: Thu, 19 Dec 2024 08:48:11 +0530 Subject: [PATCH 03/47] test: Added a new test case for 33601 (#38234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Automation for Bug: https://github.com/appsmithorg/appsmith/issues/33601 Fixes #[`Issue Number` ](https://github.com/appsmithorg/appsmith/issues/38071) ## Automation /ok-to-test tags="@tag.JS" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: ddb795cbf3eda8103e77ef3a7a96db59f8246bd1 > Cypress dashboard. > Tags: `@tag.JS` > Spec: >
Wed, 18 Dec 2024 16:25:55 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **New Features** - Introduced a new test case to validate the functionality of the `resetWidget` feature. - Added a structured JSON file representing an application configuration with various attributes and actions. - **Bug Fixes** - Implemented a test to address Bug 33601 related to the `resetWidget` function. --- .../ActionExecution/Bug33601_spec.ts | 74 +++++++++++++++++++ .../cypress/fixtures/resetWidgetBug33601.json | 1 + 2 files changed, 75 insertions(+) create mode 100644 app/client/cypress/e2e/Regression/ClientSide/ActionExecution/Bug33601_spec.ts create mode 100644 app/client/cypress/fixtures/resetWidgetBug33601.json diff --git a/app/client/cypress/e2e/Regression/ClientSide/ActionExecution/Bug33601_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/ActionExecution/Bug33601_spec.ts new file mode 100644 index 000000000000..38932ca6e136 --- /dev/null +++ b/app/client/cypress/e2e/Regression/ClientSide/ActionExecution/Bug33601_spec.ts @@ -0,0 +1,74 @@ +import { + agHelper, + appSettings, + assertHelper, + deployMode, + homePage, + locators, +} from "../../../../support/Objects/ObjectsCore"; +import EditorNavigation, { + EntityType, + PageLeftPane, +} from "../../../../support/Pages/EditorNavigation"; + +describe( + "Bug:33601: resetWidget function causes the next async method to be undefined", + { tags: ["@tag.JS"] }, + () => { + it("1. Bug 33601", () => { + homePage.NavigateToHome(); + homePage.ImportApp("resetWidgetBug33601.json"); + EditorNavigation.SelectEntityByName("List1", EntityType.Widget); + PageLeftPane.expandCollapseItem("List1"); + PageLeftPane.expandCollapseItem("Container1"); + EditorNavigation.SelectEntityByName("Input1", EntityType.Widget); + + agHelper.EnterInputText("Label", "Widget Input2"); + EditorNavigation.SelectEntityByName("Button1", EntityType.Widget); + cy.get(locators._widgetInputSelector("inputwidgetv2")) + .first() + .invoke("attr", "value") + .should("contain", "Widget Input2"); + agHelper + .GetAttribute(locators._imgWidgetInsideList, "src") + .then((labelValue) => { + expect(labelValue).not.to.contain("data:image/png;base64"); + }); + + agHelper.ClickButton("Submit"); + cy.get(locators._widgetInputSelector("inputwidgetv2")) + .first() + .invoke("attr", "value") + .should("be.empty"); + assertHelper.WaitForNetworkCall("@postExecute"); + agHelper + .GetAttribute(locators._imgWidgetInsideList, "src") + .then((labelValue) => { + expect(labelValue).to.contain("data:image/png;base64"); + }); + + deployMode.DeployApp(); + agHelper.AssertElementVisibility(appSettings.locators._header); + agHelper.EnterInputText("Label", "Widget Input2"); + cy.get(locators._widgetInputSelector("inputwidgetv2")) + .first() + .invoke("attr", "value") + .should("contain", "Widget Input2"); + agHelper + .GetAttribute(locators._imgWidgetInsideList, "src") + .then((labelValue) => { + expect(labelValue).not.to.contain("data:image/png;base64"); + }); + agHelper.ClickButton("Submit"); + cy.get(locators._widgetInputSelector("inputwidgetv2")) + .first() + .invoke("attr", "value") + .should("be.empty"); + agHelper + .GetAttribute(locators._imgWidgetInsideList, "src") + .then((labelValue) => { + expect(labelValue).to.contain("data:image/png;base64"); + }); + }); + }, +); diff --git a/app/client/cypress/fixtures/resetWidgetBug33601.json b/app/client/cypress/fixtures/resetWidgetBug33601.json new file mode 100644 index 000000000000..44ae3ae0e7dc --- /dev/null +++ b/app/client/cypress/fixtures/resetWidgetBug33601.json @@ -0,0 +1 @@ +{"artifactJsonType":"APPLICATION","clientSchemaVersion":1.0,"serverSchemaVersion":11.0,"exportedApplication":{"name":"Untitled application 1 (1)","isPublic":false,"pages":[{"id":"Page1","isDefault":true}],"publishedPages":[{"id":"Page1","isDefault":true}],"viewMode":false,"appIsExample":false,"unreadCommentThreads":0.0,"unpublishedApplicationDetail":{"appPositioning":{"type":"FIXED"},"navigationSetting":{},"themeSetting":{"sizing":1.0,"density":1.0,"appMaxWidth":"LARGE"}},"publishedApplicationDetail":{"appPositioning":{"type":"FIXED"},"navigationSetting":{},"themeSetting":{"sizing":1.0,"density":1.0,"appMaxWidth":"LARGE"}},"color":"#C2DAF0","icon":"smartphone","slug":"untitled-application-1-1","unpublishedCustomJSLibs":[],"publishedCustomJSLibs":[],"evaluationVersion":2.0,"applicationVersion":2.0,"collapseInvisibleWidgets":true,"isManualUpdate":false,"deleted":false},"datasourceList":[],"customJSLibList":[],"pageList":[{"unpublishedPage":{"name":"Page1","slug":"page1","layouts":[{"viewMode":false,"dsl":{"widgetName":"MainContainer","backgroundColor":"none","rightColumn":4896.0,"snapColumns":64.0,"detachFromLayout":true,"widgetId":"0","topRow":0.0,"bottomRow":620.0,"containerStyle":"none","snapRows":124.0,"parentRowSpace":1.0,"type":"CANVAS_WIDGET","canExtend":true,"version":90.0,"minHeight":1292.0,"dynamicTriggerPathList":[],"parentColumnSpace":1.0,"dynamicBindingPathList":[],"leftColumn":0.0,"children":[{"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":12.0,"widgetName":"Image1","topRow":0.0,"bottomRow":12.0,"parentRowSpace":10.0,"type":"IMAGE_WIDGET","mobileRightColumn":37.0,"animateLoading":true,"parentColumnSpace":13.0625,"dynamicTriggerPathList":[],"imageShape":"RECTANGLE","leftColumn":25.0,"dynamicBindingPathList":[{"key":"borderRadius"},{"key":"image"}],"defaultImage":"https://assets.appsmith.com/widgets/default.png","flexVerticalAlignment":"start","key":"lq388zxydc","image":"{{Api1.data}}","rightColumn":37.0,"objectFit":"cover","widgetId":"471dll4hnd","isVisible":true,"version":1.0,"parentId":"0","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"maxZoomLevel":1.0,"enableDownload":false,"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":25.0,"enableRotation":false},{"needsErrorInfo":false,"boxShadow":"{{appsmith.theme.boxShadow.appBoxShadow}}","mobileBottomRow":54.0,"currentItemsView":"{{[]}}","triggeredItemView":"{{{}}}","widgetName":"List1","requiresFlatWidgetChildren":true,"listData":"[\n {\n \"id\": \"001\",\n \"name\": \"Blue\",\n \"img\": \"https://assets.appsmith.com/widgets/default.png\"\n }\n]","isCanvas":true,"templateHeight":160.0,"topRow":14.0,"bottomRow":54.0,"pageSize":3.0,"parentRowSpace":10.0,"type":"LIST_WIDGET_V2","itemSpacing":8.0,"mobileRightColumn":36.0,"mainContainerId":"pi6w1ubhgt","animateLoading":true,"primaryKeys":"{{List1.listData.map((currentItem, currentIndex) => currentItem[\"id\"] )}}","parentColumnSpace":13.0625,"dynamicTriggerPathList":[],"dynamicBindingPathList":[{"key":"currentItemsView"},{"key":"selectedItemView"},{"key":"triggeredItemView"},{"key":"primaryKeys"},{"key":"accentColor"},{"key":"borderRadius"},{"key":"boxShadow"}],"leftColumn":12.0,"gridType":"vertical","enhancements":true,"children":[{"needsErrorInfo":false,"boxShadow":"{{appsmith.theme.boxShadow.appBoxShadow}}","mobileBottomRow":400.0,"widgetName":"Canvas1","topRow":0.0,"bottomRow":400.0,"parentRowSpace":1.0,"type":"CANVAS_WIDGET","canExtend":false,"dropDisabled":true,"openParentPropertyPane":true,"minHeight":400.0,"mobileRightColumn":313.5,"noPad":true,"parentColumnSpace":1.0,"leftColumn":0.0,"dynamicBindingPathList":[{"key":"borderRadius"},{"key":"boxShadow"}],"children":[{"needsErrorInfo":false,"boxShadow":"{{appsmith.theme.boxShadow.appBoxShadow}}","mobileBottomRow":12.0,"widgetName":"Container1","borderColor":"#E0DEDE","disallowCopy":true,"isCanvas":true,"topRow":0.0,"bottomRow":12.0,"dragDisabled":true,"type":"CONTAINER_WIDGET","shouldScrollContents":false,"isDeletable":false,"mobileRightColumn":64.0,"animateLoading":true,"leftColumn":0.0,"dynamicBindingPathList":[{"key":"borderRadius"},{"key":"boxShadow"}],"children":[{"needsErrorInfo":false,"boxShadow":"{{appsmith.theme.boxShadow.appBoxShadow}}","widgetName":"Canvas2","topRow":0.0,"bottomRow":120.0,"parentRowSpace":1.0,"type":"CANVAS_WIDGET","canExtend":false,"useAutoLayout":false,"parentColumnSpace":1.0,"leftColumn":0.0,"dynamicBindingPathList":[{"key":"borderRadius"},{"key":"boxShadow"}],"children":[{"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":8.0,"widgetName":"Image2","topRow":0.0,"bottomRow":8.0,"type":"IMAGE_WIDGET","mobileRightColumn":16.0,"animateLoading":true,"dynamicTriggerPathList":[],"imageShape":"RECTANGLE","dynamicBindingPathList":[{"key":"image"},{"key":"borderRadius"}],"leftColumn":0.0,"defaultImage":"https://assets.appsmith.com/widgets/default.png","flexVerticalAlignment":"start","key":"lq388zxydc","image":"{{currentItem.img}}","rightColumn":16.0,"objectFit":"cover","widgetId":"wzykiwm9ed","isVisible":true,"version":1.0,"parentId":"m26fb9ulyh","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"maxZoomLevel":1.0,"enableDownload":false,"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":0.0,"enableRotation":false},{"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":4.0,"widgetName":"Text1","topRow":0.0,"bottomRow":4.0,"type":"TEXT_WIDGET","mobileRightColumn":28.0,"animateLoading":true,"overflow":"NONE","dynamicTriggerPathList":[],"fontFamily":"{{appsmith.theme.fontFamily.appFont}}","dynamicBindingPathList":[{"key":"text"},{"key":"truncateButtonColor"},{"key":"fontFamily"},{"key":"borderRadius"},{"key":"text"}],"leftColumn":16.0,"shouldTruncate":false,"truncateButtonColor":"{{appsmith.theme.colors.primaryColor}}","text":"{{currentItem.name}}","key":"i3zhkfk0fg","rightColumn":28.0,"textAlign":"LEFT","dynamicHeight":"FIXED","widgetId":"yetf883xrd","minWidth":450.0,"isVisible":true,"fontStyle":"BOLD","textColor":"#231F20","version":1.0,"parentId":"m26fb9ulyh","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"fill","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":16.0,"maxDynamicHeight":9000.0,"fontSize":"1rem","textStyle":"HEADING","minDynamicHeight":4.0},{"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":7.0,"widgetName":"Input1","topRow":0.0,"bottomRow":7.0,"parentRowSpace":10.0,"labelWidth":5.0,"autoFocus":false,"type":"INPUT_WIDGET_V2","mobileRightColumn":50.0,"animateLoading":true,"parentColumnSpace":8.7470703125,"dynamicTriggerPathList":[],"resetOnSubmit":true,"leftColumn":30.0,"dynamicBindingPathList":[{"key":"accentColor"},{"key":"borderRadius"}],"labelPosition":"Top","labelStyle":"","inputType":"TEXT","isDisabled":false,"key":"eqcv4yaupf","labelTextSize":"0.875rem","isRequired":false,"rightColumn":64.0,"dynamicHeight":"FIXED","widgetId":"xtwlw8syfk","accentColor":"{{appsmith.theme.colors.primaryColor}}","showStepArrows":false,"minWidth":450.0,"isVisible":true,"label":"Label","version":2.0,"parentId":"m26fb9ulyh","labelAlignment":"left","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"fill","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":30.0,"maxDynamicHeight":9000.0,"iconAlign":"left","defaultText":"","minDynamicHeight":4.0}],"key":"on2pxsbe1l","detachFromLayout":true,"dynamicHeight":"AUTO_HEIGHT","widgetId":"m26fb9ulyh","containerStyle":"none","minWidth":450.0,"isVisible":true,"version":1.0,"parentId":"pi6w1ubhgt","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"fill","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":0.0,"maxDynamicHeight":9000.0,"minDynamicHeight":4.0,"flexLayers":[]}],"borderWidth":"1","positioning":"fixed","flexVerticalAlignment":"stretch","key":"bpyab5sr79","backgroundColor":"white","rightColumn":64.0,"dynamicHeight":"FIXED","widgetId":"pi6w1ubhgt","containerStyle":"card","minWidth":450.0,"isVisible":true,"version":1.0,"isListItemContainer":true,"parentId":"0ch5o045ug","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"fill","noContainerOffset":true,"disabledWidgetFeatures":["dynamicHeight"],"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":0.0,"maxDynamicHeight":9000.0,"minDynamicHeight":10.0}],"key":"on2pxsbe1l","rightColumn":313.5,"detachFromLayout":true,"dynamicHeight":"AUTO_HEIGHT","widgetId":"0ch5o045ug","containerStyle":"none","minWidth":450.0,"isVisible":true,"version":1.0,"parentId":"q6p9tdm2il","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"fill","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":0.0,"maxDynamicHeight":9000.0,"minDynamicHeight":4.0,"flexLayers":[]}],"flexVerticalAlignment":"start","key":"qh3v2nmy9z","backgroundColor":"transparent","rightColumn":57.0,"itemBackgroundColor":"#FFFFFF","widgetId":"q6p9tdm2il","accentColor":"{{appsmith.theme.colors.primaryColor}}","minWidth":450.0,"isVisible":true,"parentId":"0","hasMetaWidgets":true,"renderMode":"CANVAS","isLoading":false,"mobileTopRow":14.0,"responsiveBehavior":"fill","mainCanvasId":"0ch5o045ug","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":12.0,"additionalStaticProps":["level","levelData","prefixMetaWidgetId","metaWidgetId"],"selectedItemView":"{{{}}}"},{"resetFormOnClick":false,"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":62.0,"widgetName":"Button1","onClick":"{{JSObject1.myFun1();}}","buttonColor":"{{appsmith.theme.colors.primaryColor}}","dynamicPropertyPathList":[],"topRow":58.0,"bottomRow":62.0,"parentRowSpace":10.0,"type":"BUTTON_WIDGET","mobileRightColumn":44.0,"animateLoading":true,"parentColumnSpace":13.0625,"dynamicTriggerPathList":[{"key":"onClick"}],"leftColumn":28.0,"dynamicBindingPathList":[{"key":"buttonColor"},{"key":"borderRadius"}],"text":"Submit","isDisabled":false,"key":"negmx3xdl3","rightColumn":44.0,"isDefaultClickDisabled":true,"widgetId":"3jmu654b63","minWidth":120.0,"isVisible":true,"recaptchaType":"V3","version":1.0,"parentId":"0","renderMode":"CANVAS","isLoading":false,"mobileTopRow":58.0,"responsiveBehavior":"hug","disabledWhenInvalid":false,"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":28.0,"buttonVariant":"PRIMARY","placement":"CENTER"},{"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":35.0,"widgetName":"Modal1","isCanvas":true,"topRow":11.0,"bottomRow":251.0,"parentRowSpace":10.0,"type":"MODAL_WIDGET","shouldScrollContents":true,"mobileRightColumn":47.0,"animateLoading":true,"parentColumnSpace":13.0625,"leftColumn":23.0,"dynamicBindingPathList":[{"key":"borderRadius"}],"children":[{"needsErrorInfo":false,"boxShadow":"{{appsmith.theme.boxShadow.appBoxShadow}}","mobileBottomRow":240.0,"widgetName":"Canvas3","topRow":0.0,"bottomRow":240.0,"parentRowSpace":1.0,"type":"CANVAS_WIDGET","canExtend":true,"shouldScrollContents":false,"minHeight":240.0,"mobileRightColumn":313.5,"parentColumnSpace":1.0,"leftColumn":0.0,"dynamicBindingPathList":[{"key":"borderRadius"},{"key":"boxShadow"}],"children":[{"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":4.0,"widgetName":"IconButton1","onClick":"{{closeModal(Modal1.name);}}","buttonColor":"{{appsmith.theme.colors.primaryColor}}","topRow":0.0,"bottomRow":4.0,"type":"ICON_BUTTON_WIDGET","mobileRightColumn":64.0,"animateLoading":true,"dynamicTriggerPathList":[{"key":"onClick"}],"leftColumn":58.0,"dynamicBindingPathList":[{"key":"buttonColor"},{"key":"borderRadius"}],"iconSize":24.0,"isDisabled":false,"key":"u0zauctxb6","rightColumn":64.0,"iconName":"cross","widgetId":"bcvgad922z","minWidth":50.0,"isVisible":true,"version":1.0,"parentId":"v57uk8d79e","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"hug","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":58.0,"buttonVariant":"TERTIARY"},{"needsErrorInfo":false,"mobileBottomRow":5.0,"widgetName":"Text3","topRow":1.0,"bottomRow":5.0,"type":"TEXT_WIDGET","mobileRightColumn":41.0,"animateLoading":true,"overflow":"NONE","fontFamily":"{{appsmith.theme.fontFamily.appFont}}","leftColumn":1.0,"dynamicBindingPathList":[{"key":"truncateButtonColor"},{"key":"fontFamily"},{"key":"borderRadius"}],"shouldTruncate":false,"truncateButtonColor":"{{appsmith.theme.colors.primaryColor}}","text":"Modal Title","key":"i3zhkfk0fg","rightColumn":41.0,"textAlign":"LEFT","dynamicHeight":"AUTO_HEIGHT","widgetId":"fm3m0glkp9","minWidth":450.0,"isVisible":true,"fontStyle":"BOLD","textColor":"#231F20","version":1.0,"parentId":"v57uk8d79e","renderMode":"CANVAS","isLoading":false,"mobileTopRow":1.0,"responsiveBehavior":"fill","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":1.0,"maxDynamicHeight":9000.0,"fontSize":"1.25rem","minDynamicHeight":4.0},{"resetFormOnClick":false,"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":22.0,"widgetName":"Button2","onClick":"{{closeModal(Modal1.name);}}","buttonColor":"{{appsmith.theme.colors.primaryColor}}","topRow":18.0,"bottomRow":22.0,"type":"BUTTON_WIDGET","mobileRightColumn":47.0,"animateLoading":true,"dynamicTriggerPathList":[{"key":"onClick"}],"leftColumn":31.0,"dynamicBindingPathList":[{"key":"buttonColor"},{"key":"borderRadius"}],"text":"Close","isDisabled":false,"key":"negmx3xdl3","rightColumn":47.0,"isDefaultClickDisabled":true,"widgetId":"4t0w7edckc","buttonStyle":"PRIMARY","minWidth":120.0,"isVisible":true,"recaptchaType":"V3","version":1.0,"parentId":"v57uk8d79e","renderMode":"CANVAS","isLoading":false,"mobileTopRow":18.0,"responsiveBehavior":"hug","disabledWhenInvalid":false,"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":31.0,"buttonVariant":"SECONDARY","placement":"CENTER"},{"resetFormOnClick":false,"needsErrorInfo":false,"boxShadow":"none","mobileBottomRow":22.0,"widgetName":"Button3","buttonColor":"{{appsmith.theme.colors.primaryColor}}","topRow":18.0,"bottomRow":22.0,"type":"BUTTON_WIDGET","mobileRightColumn":63.0,"animateLoading":true,"leftColumn":47.0,"dynamicBindingPathList":[{"key":"buttonColor"},{"key":"borderRadius"}],"text":"Confirm","isDisabled":false,"key":"negmx3xdl3","rightColumn":63.0,"isDefaultClickDisabled":true,"widgetId":"6boclz1k1j","buttonStyle":"PRIMARY_BUTTON","minWidth":120.0,"isVisible":true,"recaptchaType":"V3","version":1.0,"parentId":"v57uk8d79e","renderMode":"CANVAS","isLoading":false,"mobileTopRow":18.0,"responsiveBehavior":"hug","disabledWhenInvalid":false,"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":47.0,"buttonVariant":"PRIMARY","placement":"CENTER"}],"isDisabled":false,"key":"on2pxsbe1l","rightColumn":313.5,"detachFromLayout":true,"dynamicHeight":"AUTO_HEIGHT","widgetId":"v57uk8d79e","minWidth":450.0,"isVisible":true,"version":1.0,"parentId":"sva4y7d01n","renderMode":"CANVAS","isLoading":false,"mobileTopRow":0.0,"responsiveBehavior":"fill","borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":0.0,"maxDynamicHeight":9000.0,"minDynamicHeight":4.0,"flexLayers":[]}],"key":"5oj4iyqf4p","height":240.0,"rightColumn":47.0,"detachFromLayout":true,"dynamicHeight":"AUTO_HEIGHT","widgetId":"sva4y7d01n","canOutsideClickClose":true,"canEscapeKeyClose":true,"version":2.0,"parentId":"0","renderMode":"CANVAS","isLoading":false,"mobileTopRow":11.0,"borderRadius":"{{appsmith.theme.borderRadius.appBorderRadius}}","mobileLeftColumn":23.0,"maxDynamicHeight":9000.0,"width":456.0,"minDynamicHeight":24.0}]},"layoutOnLoadActions":[],"layoutOnLoadActionErrors":[],"validOnPageLoadActions":true,"id":"Page1","deleted":false,"policies":[],"userPermissions":[]}],"userPermissions":[],"policyMap":{}},"publishedPage":{"name":"Page1","slug":"page1","layouts":[{"viewMode":false,"dsl":{"widgetName":"MainContainer","backgroundColor":"none","rightColumn":1224.0,"snapColumns":16.0,"detachFromLayout":true,"widgetId":"0","topRow":0.0,"bottomRow":1250.0,"containerStyle":"none","snapRows":33.0,"parentRowSpace":1.0,"type":"CANVAS_WIDGET","canExtend":true,"version":4.0,"minHeight":1292.0,"dynamicTriggerPathList":[],"parentColumnSpace":1.0,"dynamicBindingPathList":[],"leftColumn":0.0,"children":[]},"validOnPageLoadActions":true,"id":"Page1","deleted":false,"policies":[],"userPermissions":[]}],"userPermissions":[],"policyMap":{}},"gitSyncId":"676259a5536a20188545cfb3_1920cf1a-a5b3-450d-bb47-b304ac11f065","deleted":false}],"actionList":[{"pluginType":"JS","pluginId":"js-plugin","unpublishedAction":{"name":"myFun1","fullyQualifiedName":"JSObject1.myFun1","datasource":{"name":"UNUSED_DATASOURCE","pluginId":"js-plugin","messages":[],"isAutoGenerated":false,"deleted":false,"policyMap":{},"policies":[],"userPermissions":[]},"pageId":"Page1","collectionId":"Page1_JSObject1","actionConfiguration":{"timeoutInMillisecond":10000.0,"paginationType":"NONE","encodeParamsToggle":true,"body":"function () {\n resetWidget(\"List1\", true);\n closeModal(Modal1.name);\n Api1.run();\n}","selfReferencingDataPaths":[],"jsArguments":[]},"executeOnLoad":false,"clientSideExecution":true,"dynamicBindingPathList":[{"key":"body"}],"isValid":true,"invalids":[],"messages":[],"jsonPathKeys":["function () {\n resetWidget(\"List1\", true);\n closeModal(Modal1.name);\n Api1.run();\n}"],"userSetOnLoad":false,"confirmBeforeExecute":false,"policyMap":{},"userPermissions":[],"createdAt":"2024-12-18T05:12:19Z"},"publishedAction":{"datasource":{"messages":[],"isAutoGenerated":false,"deleted":false,"policyMap":{},"policies":[],"userPermissions":[]},"messages":[],"userSetOnLoad":false,"confirmBeforeExecute":false,"policyMap":{},"userPermissions":[],"createdAt":"2024-12-18T05:12:19Z"},"gitSyncId":"676259a5536a20188545cfb3_3fdf8419-0d1d-4125-bc66-cc9d2b9eea85","id":"Page1_JSObject1.myFun1","deleted":false},{"pluginType":"API","pluginId":"restapi-plugin","unpublishedAction":{"name":"Api1","datasource":{"name":"DEFAULT_REST_DATASOURCE","pluginId":"restapi-plugin","datasourceConfiguration":{"url":"https://docs.appsmith.com"},"invalids":[],"messages":[],"isAutoGenerated":false,"deleted":false,"policyMap":{},"policies":[],"userPermissions":[]},"pageId":"Page1","actionConfiguration":{"timeoutInMillisecond":10000.0,"paginationType":"NONE","path":"/img/replyto-logo_6yaZHFIeU.jpeg","headers":[],"autoGeneratedHeaders":[],"encodeParamsToggle":true,"queryParameters":[],"body":"","bodyFormData":[],"httpMethod":"GET","httpVersion":"HTTP11","selfReferencingDataPaths":[],"pluginSpecifiedTemplates":[{"value":true}],"formData":{"apiContentType":"none"}},"executeOnLoad":false,"dynamicBindingPathList":[],"isValid":true,"invalids":[],"messages":[],"jsonPathKeys":[],"userSetOnLoad":true,"confirmBeforeExecute":false,"policyMap":{},"userPermissions":[],"createdAt":"2024-12-18T05:12:38Z"},"publishedAction":{"datasource":{"messages":[],"isAutoGenerated":false,"deleted":false,"policyMap":{},"policies":[],"userPermissions":[]},"messages":[],"userSetOnLoad":false,"confirmBeforeExecute":false,"policyMap":{},"userPermissions":[],"createdAt":"2024-12-18T05:12:38Z"},"gitSyncId":"676259a5536a20188545cfb3_3ef30bd9-ef2c-4a18-a874-96cde5f40cc5","id":"Page1_Api1","deleted":false}],"actionCollectionList":[{"unpublishedCollection":{"name":"JSObject1","pageId":"Page1","pluginId":"js-plugin","pluginType":"JS","actions":[],"archivedActions":[],"body":"export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\tmyFun1 () {\n\t\tresetWidget(\"List1\", true);\n\t\tcloseModal(Modal1.name);\n\t\tApi1.run()\n\t},\n}","variables":[{"name":"myVar1","value":"[]"},{"name":"myVar2","value":"{}"}],"userPermissions":[]},"gitSyncId":"676259a5536a20188545cfb3_200b3630-e762-4bc5-bac5-0b371a18d4b4","id":"Page1_JSObject1","deleted":false}],"editModeTheme":{"name":"Default-New","displayName":"Modern","isSystemTheme":true,"deleted":false},"publishedTheme":{"name":"Default-New","displayName":"Modern","isSystemTheme":true,"deleted":false}} \ No newline at end of file From 620a3ad2b4718c76d886265f311fa411027f51fc Mon Sep 17 00:00:00 2001 From: Diljit Date: Thu, 19 Dec 2024 09:37:06 +0530 Subject: [PATCH 04/47] chore: remove metrics in fe (#38240) --- app/client/src/UITelemetry/auto-otel-web.ts | 57 ++++----------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/app/client/src/UITelemetry/auto-otel-web.ts b/app/client/src/UITelemetry/auto-otel-web.ts index 3a2aa17b6413..313057904993 100644 --- a/app/client/src/UITelemetry/auto-otel-web.ts +++ b/app/client/src/UITelemetry/auto-otel-web.ts @@ -9,15 +9,6 @@ import { } from "@opentelemetry/semantic-conventions/incubating"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; import { getAppsmithConfigs } from "ee/configs"; -import { - MeterProvider, - PeriodicExportingMetricReader, -} from "@opentelemetry/sdk-metrics"; -import { - AggregationTemporalityPreference, - OTLPMetricExporter, -} from "@opentelemetry/exporter-metrics-otlp-http"; -import { metrics } from "@opentelemetry/api"; import { registerInstrumentations } from "@opentelemetry/instrumentation"; import { PageLoadInstrumentation } from "./PageLoadInstrumentation"; import { getWebAutoInstrumentations } from "@opentelemetry/auto-instrumentations-web"; @@ -44,7 +35,7 @@ const tracerProvider = new WebTracerProvider({ }); const nrTracesExporter = new OTLPTraceExporter({ - url: addPathToCurrentUrl("/monitoring/traces"), + url: getAbsoluteUrl("/monitoring/traces"), compression: CompressionAlgorithm.GZIP, headers: { "api-key": otlpLicenseKey, @@ -71,41 +62,13 @@ tracerProvider.register({ contextManager: new ZoneContextManager(), }); -const nrMetricsExporter = new OTLPMetricExporter({ - compression: CompressionAlgorithm.GZIP, - temporalityPreference: AggregationTemporalityPreference.DELTA, - url: addPathToCurrentUrl("/monitoring/metrics"), - headers: { - "api-key": otlpLicenseKey, - }, -}); - -const meterProvider = new MeterProvider({ - resource: new Resource({ - [ATTR_DEPLOYMENT_NAME]: deploymentName, - [ATTR_SERVICE_INSTANCE_ID]: serviceInstanceId, - [ATTR_SERVICE_NAME]: serviceName, - }), - readers: [ - new PeriodicExportingMetricReader({ - exporter: nrMetricsExporter, - exportIntervalMillis: 30000, // Adjust the export interval as needed - }), - ], -}); - -// Register the MeterProvider globally -metrics.setGlobalMeterProvider(meterProvider); - registerInstrumentations({ tracerProvider, - meterProvider, instrumentations: [ new PageLoadInstrumentation({ ignoreResourceUrls: [ browserAgentEndpoint, - addPathToCurrentUrl("/monitoring/traces"), - addPathToCurrentUrl("/monitoring/metrics"), + getAbsoluteUrl("/monitoring/traces"), smartlookBaseDomain, ], }), @@ -117,13 +80,11 @@ registerInstrumentations({ ], }); -// Replaces the pathname of the current URL with the provided path. -function addPathToCurrentUrl(path: string) { - const origin = window.location.origin; - - const currentUrl = new URL(origin); - - currentUrl.pathname = path.startsWith("/") ? path : `/${path}`; - - return currentUrl.toString(); +/** + * This function adds the given path to the current origin and returns the absolute URL. + * @param path The path to be added to the current origin. + * @returns The absolute URL. + */ +function getAbsoluteUrl(path: string) { + return new URL(path, window.location.origin).toString(); } From 80f4739d7a0a158c2ee66945cad6aec6dfe7ec7c Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:34:37 +0530 Subject: [PATCH 05/47] test: commenting dependent test which was missed in RestApiOAuth2Validation_spec (#38239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /ok-to-test tags="@tag.Sanity" > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: baf2d3b85eb7016a426b3818d620828b7856ca74 > Cypress dashboard. > Tags: `@tag.Sanity` > Spec: >
Thu, 19 Dec 2024 04:03:20 UTC ## Summary by CodeRabbit - **Tests** - Disabled the execution of the OAuth2 validation test suite. - Skipped a specific test case within the suite. --------- Co-authored-by: β€œNandanAnantharamu” <β€œnandan@thinkify.io”> --- .../e2e/Sanity/Datasources/RestApiOAuth2Validation_spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/cypress/e2e/Sanity/Datasources/RestApiOAuth2Validation_spec.ts b/app/client/cypress/e2e/Sanity/Datasources/RestApiOAuth2Validation_spec.ts index c6155bfd4188..39d2a0078b5f 100644 --- a/app/client/cypress/e2e/Sanity/Datasources/RestApiOAuth2Validation_spec.ts +++ b/app/client/cypress/e2e/Sanity/Datasources/RestApiOAuth2Validation_spec.ts @@ -8,7 +8,7 @@ import { dataManager, } from "../../../support/Objects/ObjectsCore"; -describe( +describe.skip( "Datasource form OAuth2 client credentials related tests", { tags: ["@tag.Datasource", "@tag.Sanity", "@tag.Git", "@tag.AccessControl"], @@ -42,7 +42,7 @@ describe( }); }); - it("2. Validate save and Authorise", function () { + it.skip("2. Validate save and Authorise", function () { agHelper.GetNClick(dataSources._saveDs); //Accept consent From 7d87c551a50d7e700449db8796525fff70ff7c79 Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Thu, 19 Dec 2024 10:51:56 +0530 Subject: [PATCH 06/47] chore: refactor the crud method to custom repo class (#38138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As part of transaction support in PG, we are moving from using the jpa methods for database operations. This PR is refactoring the code to use custom repository class for DatasourceRepository from the default CrudRepository. ## Automation /ok-to-test tags="@tag.Datasource" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: df7dc42471f4790121f595ec04e1c29749ef6676 > Cypress dashboard. > Tags: `@tag.Datasource` > Spec: >
Wed, 18 Dec 2024 08:59:01 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced methods to retrieve `Datasource` objects by IDs and count non-deleted `Datasource` instances. - **Bug Fixes** - Removed outdated method signatures from the `DatasourceRepositoryCE` interface to streamline functionality. - **Documentation** - Added `@Repository` annotation to the `DatasourceRepositoryCE` interface for clarity on its role within the application. --- .../ce/CustomDatasourceRepositoryCE.java | 6 ++++++ .../ce/CustomDatasourceRepositoryCEImpl.java | 15 +++++++++++++++ .../repositories/ce/DatasourceRepositoryCE.java | 11 ++--------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCE.java index 8923668aa6e8..f32838704608 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCE.java @@ -6,9 +6,15 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + public interface CustomDatasourceRepositoryCE extends AppsmithRepository { Flux findAllByWorkspaceId(String workspaceId, AclPermission permission); Mono findByNameAndWorkspaceId(String name, String workspaceId, AclPermission aclPermission); + + Flux findByIdIn(List ids); + + Mono countByDeletedAtNull(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCEImpl.java index eb982061b1e0..af2dce8aaa81 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceRepositoryCEImpl.java @@ -3,11 +3,14 @@ import com.appsmith.external.models.Datasource; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.helpers.ce.bridge.Bridge; +import com.appsmith.server.helpers.ce.bridge.BridgeQuery; import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl; import org.springframework.data.domain.Sort; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + public class CustomDatasourceRepositoryCEImpl extends BaseAppsmithRepositoryImpl implements CustomDatasourceRepositoryCE { @@ -28,4 +31,16 @@ public Mono findByNameAndWorkspaceId(String name, String workspaceId .permission(aclPermission) .one(); } + + @Override + public Flux findByIdIn(List ids) { + final BridgeQuery q = Bridge.in(Datasource.Fields.id, ids); + return queryBuilder().criteria(q).all(); + } + + @Override + public Mono countByDeletedAtNull() { + final BridgeQuery q = Bridge.isNull(Datasource.Fields.deletedAt); + return queryBuilder().criteria(q).count(); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceRepositoryCE.java index 087b92cff5b5..71967e72cb65 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceRepositoryCE.java @@ -4,19 +4,12 @@ import com.appsmith.server.projections.IdPoliciesOnly; import com.appsmith.server.repositories.BaseRepository; import com.appsmith.server.repositories.CustomDatasourceRepository; +import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import java.util.List; import java.util.Set; +@Repository public interface DatasourceRepositoryCE extends BaseRepository, CustomDatasourceRepository { - - Flux findByIdIn(List ids); - - Flux findAllByWorkspaceId(String workspaceId); - - Mono countByDeletedAtNull(); - Flux findIdsAndPolicyMapByIdIn(Set datasourceIds); } From 7a7f37cd48db47395cdccf3329973b51f851905f Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Thu, 19 Dec 2024 11:12:09 +0530 Subject: [PATCH 07/47] chore: fix menu widget bugs (#38226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #38202 /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced a new configuration for widget sizing and characteristics. - Enhanced menu functionality with clearer item rendering and interaction. - **Bug Fixes** - Improved type safety and compatibility in menu item processing. - **Refactor** - Updated configuration handling for menu items, transitioning to a more generic structure. - Rearranged export statements for better organization. - **Documentation** - Updated method signatures and interface definitions for better clarity and usability. > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: ade8c87e56c003a3d5c733f9c66a2ac4345f7158 > Cypress dashboard. > Tags: `@tag.Anvil` > Spec: >
Wed, 18 Dec 2024 12:33:31 UTC --- .../WDSMenuButtonWidget/config/anvilConfig.ts | 10 ++ .../wds/WDSMenuButtonWidget/config/index.ts | 5 +- .../propertyPaneConfig/contentConfig.ts | 3 +- .../wds/WDSMenuButtonWidget/widget/index.tsx | 96 +++++++++---------- .../wds/WDSMenuButtonWidget/widget/types.ts | 39 ++------ 5 files changed, 70 insertions(+), 83 deletions(-) create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/anvilConfig.ts diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/anvilConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/anvilConfig.ts new file mode 100644 index 000000000000..169db7b31b60 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/anvilConfig.ts @@ -0,0 +1,10 @@ +export const anvilConfig = { + isLargeWidget: false, + widgetSize: { + maxWidth: { + base: "100%", + "280px": "sizing-70", + }, + minWidth: "sizing-14", + }, +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/index.ts index ab36e7a74f4d..fa4caee3937f 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/index.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/index.ts @@ -1,6 +1,7 @@ export * from "./propertyPaneConfig"; -export { autocompleteConfig } from "./autocompleteConfig"; -export { defaultsConfig } from "./defaultsConfig"; export { metaConfig } from "./metaConfig"; +export { anvilConfig } from "./anvilConfig"; export { settersConfig } from "./settersConfig"; export { methodsConfig } from "./methodsConfig"; +export { defaultsConfig } from "./defaultsConfig"; +export { autocompleteConfig } from "./autocompleteConfig"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/propertyPaneConfig/contentConfig.ts index 52525d150834..633649ebabb7 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/config/propertyPaneConfig/contentConfig.ts @@ -3,9 +3,9 @@ import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { sourceDataArrayValidation } from "./validations"; import { configureMenuItemsConfig, menuItemsConfig } from "./childPanels"; -import { updateMenuItemsSource } from "../helper"; import type { MenuButtonWidgetProps } from "../../widget/types"; import type { PropertyPaneConfig } from "constants/PropertyControlConstants"; +import { updateMenuItemsSource } from "../helper"; export const propertyPaneContentConfig = [ { @@ -43,6 +43,7 @@ export const propertyPaneContentConfig = [ isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, updateHook: updateMenuItemsSource, + hidden: () => true, dependencies: ["sourceData", "configureMenuItems"], }, { diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/index.tsx index f0708c9ece4d..a564912ab2fa 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/index.tsx @@ -1,24 +1,17 @@ -import React from "react"; -import type { SetterConfig } from "entities/AppTheming"; -import type { WidgetState } from "widgets/BaseWidget"; -import BaseWidget from "widgets/BaseWidget"; -import { - metaConfig, - defaultsConfig, - autocompleteConfig, - propertyPaneContentConfig, - propertyPaneStyleConfig, - settersConfig, - methodsConfig, -} from "../config"; -import type { AnvilConfig } from "WidgetProvider/constants"; -import { Button, MenuTrigger, Menu } from "@appsmith/wds"; +import React, { type Key } from "react"; import { isArray, orderBy } from "lodash"; -import type { MenuButtonWidgetProps, MenuItem } from "./types"; +import BaseWidget from "widgets/BaseWidget"; +import type { WidgetState } from "widgets/BaseWidget"; +import type { SetterConfig } from "entities/AppTheming"; import { EventType, type ExecuteTriggerPayload, } from "constants/AppsmithActionConstants/ActionConstants"; +import type { AnvilConfig } from "WidgetProvider/constants"; +import { Button, MenuTrigger, Menu, MenuItem } from "@appsmith/wds"; + +import * as config from "../config"; +import type { MenuButtonWidgetProps } from "./types"; class WDSMenuButtonWidget extends BaseWidget< MenuButtonWidgetProps, @@ -26,56 +19,43 @@ class WDSMenuButtonWidget extends BaseWidget< > { constructor(props: MenuButtonWidgetProps) { super(props); - - this.state = { - isLoading: false, - }; } static type = "WDS_MENU_BUTTON_WIDGET"; static getConfig() { - return metaConfig; + return config.metaConfig; } static getDefaults() { - return defaultsConfig; + return config.defaultsConfig; } static getAnvilConfig(): AnvilConfig | null { - return { - isLargeWidget: false, - widgetSize: { - maxWidth: { - base: "100%", - "280px": "sizing-70", - }, - minWidth: "sizing-14", - }, - }; + return config.anvilConfig; } static getAutocompleteDefinitions() { - return autocompleteConfig; + return config.autocompleteConfig; } static getPropertyPaneContentConfig() { - return propertyPaneContentConfig; + return config.propertyPaneContentConfig; } static getPropertyPaneStyleConfig() { - return propertyPaneStyleConfig; + return config.propertyPaneStyleConfig; } static getSetterConfig(): SetterConfig { - return settersConfig; + return config.settersConfig; } static getMethods() { - return methodsConfig; + return config.methodsConfig; } - menuItemClickHandler = (onClick: string | undefined, index: number) => { + onMenuItemClick = (onClick: string | undefined, index: number) => { if (onClick) { const config: ExecuteTriggerPayload = { triggerPropertyName: "onClick", @@ -85,6 +65,9 @@ class WDSMenuButtonWidget extends BaseWidget< }, }; + // in case when the menu items source is dynamic, we need to pass the current item to the global context + // the reason is in onClick, we can access the current item and current index by writing `{{currentItem}}` and `{{currentIndex}}`, + // so we need to pass the current item to the global context if (this.props.menuItemsSource === "dynamic") { config.globalContext = { currentItem: this.props.sourceData @@ -108,7 +91,9 @@ class WDSMenuButtonWidget extends BaseWidget< .filter((item) => item.isVisible === true); return orderBy(visibleItems, ["index"], ["asc"]); - } else if ( + } + + if ( menuItemsSource === "dynamic" && isArray(sourceData) && sourceData?.length && @@ -116,7 +101,10 @@ class WDSMenuButtonWidget extends BaseWidget< ) { const { config } = configureMenuItems; - const getValue = (propertyName: keyof MenuItem, index: number) => { + const getDynamicMenuItemValue = ( + propertyName: keyof typeof config, + index: number, + ) => { const value = config[propertyName]; if (isArray(value)) { @@ -130,15 +118,12 @@ class WDSMenuButtonWidget extends BaseWidget< .map((item, index) => ({ ...item, id: index.toString(), - isVisible: getValue("isVisible", index), - isDisabled: getValue("isDisabled", index), + isVisible: getDynamicMenuItemValue("isVisible", index), + isDisabled: getDynamicMenuItemValue("isDisabled", index), index: index, widgetId: "", - label: getValue("label", index), + label: getDynamicMenuItemValue("label", index), onClick: config?.onClick, - iconAlign: getValue("iconAlign", index), - iconName: getValue("iconName", index), - textColor: getValue("textColor", index), })) .filter((item) => item.isVisible === true); @@ -159,7 +144,7 @@ class WDSMenuButtonWidget extends BaseWidget< triggerButtonVariant, } = this.props; - const visibleItems: MenuItem[] = this.getVisibleItems(); + const visibleItems = this.getVisibleItems(); const disabledKeys = visibleItems .filter((item) => item.isDisabled === true) .map((item) => item.id); @@ -178,21 +163,30 @@ class WDSMenuButtonWidget extends BaseWidget< } onAction={(key) => { const clickedItemIndex = visibleItems.findIndex( (item) => item.id === key, ); if (clickedItemIndex > -1) { - this.menuItemClickHandler( + this.onMenuItemClick( visibleItems[clickedItemIndex]?.onClick, clickedItemIndex, ); } }} - /> + > + {visibleItems.map((item) => ( + + {item.label} + + ))} + ); } diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/types.ts b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/types.ts index bcd7daac9820..b06e1bafc055 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/types.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSMenuButtonWidget/widget/types.ts @@ -1,45 +1,26 @@ -import type { ButtonProps, COLORS, IconProps } from "@appsmith/wds"; +import type { ButtonProps, IconProps } from "@appsmith/wds"; import type { WidgetProps } from "widgets/BaseWidget"; -import type { IconName } from "@blueprintjs/icons"; - -export interface MenuItem { - // Meta - id: string; - index?: number; - widgetId?: string; - - // General - label?: string; - isVisible?: boolean; - isDisabled?: boolean; - onClick?: string; - - // Style - iconName?: IconName; - iconAlign?: "start" | "end"; - textColor?: keyof typeof COLORS; -} export interface ConfigureMenuItems { label: string; id: string; - config: MenuItem; + config: { + id: string; + label: string; + isVisible: boolean; + isDisabled: boolean; + onClick: string; + }; } export interface MenuButtonWidgetProps extends WidgetProps { - // General label?: string; isDisabled?: boolean; isVisible?: boolean; - - // Source data and menu items - sourceData?: Array>; + sourceData?: Array; menuItemsSource: "static" | "dynamic"; configureMenuItems: ConfigureMenuItems; - menuItems: Record; - getVisibleItems: () => Array; - - // Trigger button style + menuItems: Record; triggerButtonIconName?: IconProps["name"]; triggerButtonIconAlign?: ButtonProps["iconPosition"]; triggerButtonVariant?: ButtonProps["variant"]; From 65b98c65b5c4b90fc705db4f11db469bab049d09 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Thu, 19 Dec 2024 11:12:23 +0530 Subject: [PATCH 08/47] chore: Allow currency and phone dial code change (#38141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![CleanShot 2024-12-16 at 16 42 37](https://github.com/user-attachments/assets/1b59a2f9-d8d1-4fa9-8827-493622088e56) ![CleanShot 2024-12-16 at 16 43 12](https://github.com/user-attachments/assets/b8d29bba-55c5-42ec-8352-fb8b374ef8d2) Fixes #38091 /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Added new props for enhanced styling and functionality in various components, including `className` and `maxHeight`. - Introduced dropdown menus for currency and ISD code selection in the CurrencyInput and PhoneInput components. - New story added for the `TextField` component showcasing prefix and suffix usage. - Added a custom hook `useRootContainer` for better theme provider management. - **Bug Fixes** - Improved conditional rendering for input elements based on prop values. - **Style** - Significant updates to CSS for input and select components, enhancing layout and visual feedback. - New CSS classes added for ComboBox and styling adjustments for existing classes. - Enhanced styling for the ComboBox popover and other components. - New CSS rules for currency and dial code options in respective widgets. - **Documentation** - Updated property configurations for widgets to include new toggle options for currency and dial code changes. > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: b8bb73890940ea57a708ffe31b1b61f02fbe7417 > Cypress dashboard. > Tags: `@tag.Anvil` > Spec: >
Wed, 18 Dec 2024 11:33:31 UTC --- .../src/components/ComboBox/src/ComboBox.tsx | 13 ++- .../ComboBox/src/ComboBoxTrigger.tsx | 13 +-- .../components/ComboBox/src/styles.module.css | 4 + .../components/Datepicker/src/Datepicker.tsx | 5 +- .../src/components/Input/src/Input.tsx | 26 ++++- .../components/Input/src/styles.module.css | 106 +++++------------- .../widgets/src/components/Menu/src/Menu.tsx | 21 ++-- .../widgets/src/components/Menu/src/types.ts | 6 +- .../src/components/MenuItem/src/MenuItem.tsx | 8 +- .../src/components/Popover/src/constants.ts | 1 + .../src/components/Popover/src/index.ts | 2 + .../Popover/src/useRootContainer.ts | 17 +++ .../src/components/Select/src/Select.tsx | 17 +-- .../components/Select/src/SelectTrigger.tsx | 18 ++- .../components/Select/src/styles.module.css | 25 +++++ .../TextField/stories/TextField.stories.tsx | 7 ++ .../component/index.tsx | 76 +++++++++++-- .../component/styles.module.css | 3 + .../component/utilities.ts | 10 ++ .../propertyPaneConfig/contentConfig.ts | 10 ++ .../WDSPhoneInputWidget/component/index.tsx | 72 ++++++++++-- .../component/styles.module.css | 3 + .../propertyPaneConfig/contentConfig.ts | 10 ++ 23 files changed, 334 insertions(+), 139 deletions(-) create mode 100644 app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/Popover/src/constants.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Popover/src/useRootContainer.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/styles.module.css create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/component/styles.module.css diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx index 738432dd4b3e..44587284907d 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx @@ -4,10 +4,13 @@ import { FieldLabel, FieldError, inputFieldStyles, + useRootContainer, + POPOVER_LIST_BOX_MAX_HEIGHT, } from "@appsmith/wds"; import React from "react"; import { ComboBox as HeadlessCombobox } from "react-aria-components"; +import styles from "./styles.module.css"; import type { ComboBoxProps } from "./types"; import { ComboBoxTrigger } from "./ComboBoxTrigger"; @@ -24,9 +27,7 @@ export const ComboBox = (props: ComboBoxProps) => { size = "medium", ...rest } = props; - const root = document.body.querySelector( - "[data-theme-provider]", - ) as HTMLButtonElement; + const root = useRootContainer(); return ( { size={size} /> {errorMessage} - + {children} diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx index c17bcb4cca89..bf5b8bae0b90 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx @@ -1,7 +1,5 @@ -import clsx from "clsx"; import React, { useMemo } from "react"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import { Spinner, textInputStyles, Input, IconButton } from "@appsmith/wds"; +import { Spinner, Input, IconButton } from "@appsmith/wds"; import type { ComboBoxProps } from "./types"; @@ -27,12 +25,5 @@ export const ComboBoxTrigger: React.FC = (props) => { ); }, [isLoading, size, isDisabled]); - return ( - - ); + return ; }; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css new file mode 100644 index 000000000000..5a98e2049876 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css @@ -0,0 +1,4 @@ +.comboboxPopover { + width: calc(var(--trigger-width) + var(--inner-spacing-2) * 2); + transform: translateX(calc(-1 * var(--inner-spacing-1))); +} diff --git a/app/client/packages/design-system/widgets/src/components/Datepicker/src/Datepicker.tsx b/app/client/packages/design-system/widgets/src/components/Datepicker/src/Datepicker.tsx index 3d98fb0b3175..f62f5dd69fb7 100644 --- a/app/client/packages/design-system/widgets/src/components/Datepicker/src/Datepicker.tsx +++ b/app/client/packages/design-system/widgets/src/components/Datepicker/src/Datepicker.tsx @@ -5,6 +5,7 @@ import { Calendar, inputFieldStyles, TimeField, + useRootContainer, } from "@appsmith/wds"; import clsx from "clsx"; import React from "react"; @@ -44,6 +45,7 @@ export const DatePicker = (props: DatePickerProps) => { const timeMaxValue = ( props.maxValue && "hour" in props.maxValue ? props.maxValue : null ) as TimeValue; + const root = useRootContainer(); return ( (props: DatePickerProps) => { {...rest} > {({ state }) => { - const root = document.body.querySelector( - "[data-theme-provider]", - ) as HTMLButtonElement; const timeGranularity = state.granularity === "hour" || state.granularity === "minute" || diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx b/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx index 3472e2b53ac7..64e03bc95d66 100644 --- a/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx +++ b/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx @@ -1,5 +1,6 @@ import clsx from "clsx"; -import React, { forwardRef, useState } from "react"; +import { mergeRefs } from "@react-aria/utils"; +import React, { forwardRef, useRef, useState } from "react"; import { getTypographyClassName } from "@appsmith/wds-theming"; import { IconButton, Spinner, type IconProps } from "@appsmith/wds"; import { Group, Input as HeadlessInput } from "react-aria-components"; @@ -9,6 +10,7 @@ import type { InputProps } from "./types"; function _Input(props: InputProps, ref: React.Ref) { const { + className, defaultValue, isLoading, isReadOnly, @@ -19,6 +21,8 @@ function _Input(props: InputProps, ref: React.Ref) { value, ...rest } = props; + const localRef = useRef(null); + const mergedRef = mergeRefs(ref, localRef); const [showPassword, setShowPassword] = useState(false); const togglePasswordVisibility = () => setShowPassword((prev) => !prev); const isEmpty = !Boolean(value) && !Boolean(defaultValue); @@ -46,18 +50,30 @@ function _Input(props: InputProps, ref: React.Ref) { return ( + {Boolean(prefix) && ( + localRef.current?.focus()}> + {prefix} + + )} - {Boolean(prefix) && {prefix}} - {Boolean(suffix) && {suffix}} + {Boolean(suffix) && ( + localRef.current?.focus()}> + {suffix} + + )} ); } diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css index e36ba4a53650..d0faca4f3a6c 100644 --- a/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css @@ -6,12 +6,7 @@ width: 100%; } -.input { - position: relative; - display: flex; - flex: 1; - align-items: center; - box-sizing: content-box; +.inputGroup { max-inline-size: 100%; padding-block: var(--inner-spacing-1); padding-inline: var(--inner-spacing-2); @@ -25,8 +20,12 @@ overflow: hidden; } -.input:has(> [data-select-text]) { - block-size: var(--body-line-height); +.input { + border: none; + outline: none; + background-color: transparent; + flex: 1; + padding: 0; } .input:is(textarea) { @@ -57,44 +56,11 @@ ); } -.inputGroup:has(> [data-input-prefix]) .input { - padding-left: var(--sizing-8); -} - -.inputGroup:has(> [data-input-prefix]) .input[data-size="large"] { - padding-left: var(--sizing-12); -} - -.inputGroup:has(> [data-input-prefix]) .input[data-size="small"] { - padding-left: var(--sizing-6); -} - -.inputGroup:has(> [data-input-prefix]) [data-input-prefix] { - left: var(--inner-spacing-1); - position: absolute; -} - -.inputGroup:has(> [data-input-suffix]) .input { - padding-right: var(--sizing-8); -} - -.inputGroup:has(> [data-input-suffix]) .input[data-size="large"] { - padding-right: var(--sizing-12); -} - -.inputGroup:has(> [data-input-suffix]) .input[data-size="small"] { - padding-right: var(--sizing-6); -} - -.inputGroup:has(> [data-input-suffix]) [data-input-suffix] { - right: var(--inner-spacing-1); - position: absolute; -} - .inputGroup :is([data-input-suffix], [data-input-prefix]) { display: flex; justify-content: center; align-items: center; + height: 0; } /** @@ -102,14 +68,15 @@ * HOVERED * ---------------------------------------------------------------------------- */ -.inputGroup[data-hovered] - .input:not( - :is( - [data-focused], - [data-readonly], - [data-disabled], - [data-focus-within], - :has(~ input[data-disabled="true"]) +.inputGroup[data-hovered]:has( + > .input:not( + :is( + [data-focused], + [data-readonly], + [data-disabled], + [data-focus-within], + :has(~ input[data-disabled="true"]) + ) ) ) { background-color: var(--color-bg-neutral-subtle-hover); @@ -157,8 +124,8 @@ * DISABLED * ---------------------------------------------------------------------------- */ -.input[data-disabled], -.input:has(~ input[data-disabled]) { +.inputGroup:has(> .input[data-disabled]), +.inputGroup:has(> .input:has(~ input[data-disabled])) { cursor: not-allowed; box-shadow: none; } @@ -168,13 +135,14 @@ * INVALID * ---------------------------------------------------------------------------- */ -.input[data-invalid] { +.inputGroup:has(> .input[data-invalid]) { box-shadow: 0 0 0 1px var(--color-bd-negative); } -.inputGroup[data-hovered] - .input[data-invalid]:not( - :is([data-focused], [data-readonly], [data-disabled]) +.inputGroup[data-hovered]:has( + > .input[data-invalid]:not( + :is([data-focused], [data-readonly], [data-disabled]) + ) ) { box-shadow: 0 0 0 1px var(--color-bd-negative-hover); } @@ -184,7 +152,9 @@ * FOCUSSED * ---------------------------------------------------------------------------- */ -.input:is([data-focused], [data-focus-within]):not([data-readonly]) { +.inputGroup:has( + > .input:is([data-focused], [data-focus-within]):not([data-readonly]) + ) { background-color: transparent; box-shadow: 0 0 0 2px var(--color-bd-focus); } @@ -194,31 +164,17 @@ * SIZE * ---------------------------------------------------------------------------- */ -.input[data-size="small"] { +.inputGroup:has(> .input[data-size="small"]) { block-size: calc( - var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + + var(--inner-spacing-2) * 2 ); - padding-block: var(--inner-spacing-2); } -.input[data-size="large"] { +.inputGroup:has(> .input[data-size="large"]) { block-size: calc( var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) ); padding-block: var(--inner-spacing-3); padding-inline: var(--inner-spacing-3); } - -/** - * ---------------------------------------------------------------------------- - * SELECT BUTTON's TEXT - * ---------------------------------------------------------------------------- - */ -.input [data-select-text] { - display: flex; - align-items: center; -} - -.input [data-select-text] [data-icon] { - margin-inline-end: var(--inner-spacing-1); -} diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx b/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx index 223ed03d7c17..77cc1f6db4ab 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx @@ -1,16 +1,20 @@ +import { + Popover, + listStyles, + useRootContainer, + POPOVER_LIST_BOX_MAX_HEIGHT, +} from "@appsmith/wds"; import React, { createContext, useContext } from "react"; -import { listStyles, Popover } from "@appsmith/wds"; import { Menu as HeadlessMenu } from "react-aria-components"; import type { MenuProps } from "./types"; +import clsx from "clsx"; const MenuNestingContext = createContext(0); export const Menu = (props: MenuProps) => { - const { children } = props; - const root = document.body.querySelector( - "[data-theme-provider]", - ) as HTMLButtonElement; + const { children, className, ...rest } = props; + const root = useRootContainer(); const nestingLevel = useContext(MenuNestingContext); const isRootMenu = nestingLevel === 0; @@ -18,8 +22,11 @@ export const Menu = (props: MenuProps) => { return ( {/* Only the parent Popover should be placed in the root. Placing child popoves in root would cause the menu to function incorrectly */} - - + + {children} diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts index 4c7b10e72f2d..4f514c44c3a1 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts @@ -1,7 +1,3 @@ import type { MenuProps as AriaMenuProps } from "react-aria-components"; -export interface MenuProps - extends Omit< - AriaMenuProps, - "slot" | "selectionMode" | "selectedKeys" - > {} +export interface MenuProps extends Omit, "slot"> {} diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx b/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx index edd690856695..d2ae90291990 100644 --- a/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx @@ -6,12 +6,16 @@ import { import { Icon, Text, listBoxItemStyles } from "@appsmith/wds"; import type { MenuItemProps } from "./types"; +import clsx from "clsx"; export function MenuItem(props: MenuItemProps) { - const { children, icon, ...rest } = props; + const { children, className, icon, ...rest } = props; return ( - + {composeRenderProps(children, (children, { hasSubmenu }) => ( <> {icon && } diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/constants.ts b/app/client/packages/design-system/widgets/src/components/Popover/src/constants.ts new file mode 100644 index 000000000000..83bab6a45400 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/constants.ts @@ -0,0 +1 @@ +export const POPOVER_LIST_BOX_MAX_HEIGHT = 400; diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts b/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts index 72f124f6d38e..cb430550012a 100644 --- a/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts @@ -1 +1,3 @@ export * from "./Popover"; +export * from "./useRootContainer"; +export * from "./constants"; diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/useRootContainer.ts b/app/client/packages/design-system/widgets/src/components/Popover/src/useRootContainer.ts new file mode 100644 index 000000000000..d1506d822d5b --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/useRootContainer.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; + +import { useState } from "react"; + +export const useRootContainer = () => { + const [root, setRoot] = useState( + document.body.querySelector("[data-theme-provider]") as Element, + ); + + useEffect(() => { + if (!root) { + setRoot(document.body.querySelector("[data-theme-provider]") as Element); + } + }, []); + + return root; +}; diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx index 0337fd709ad9..2f1ac356ded2 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx +++ b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx @@ -1,10 +1,12 @@ import React from "react"; import { - FieldError, - FieldLabel, ListBox, - inputFieldStyles, Popover, + FieldLabel, + FieldError, + inputFieldStyles, + useRootContainer, + POPOVER_LIST_BOX_MAX_HEIGHT, } from "@appsmith/wds"; import { Select as HeadlessSelect } from "react-aria-components"; @@ -24,9 +26,7 @@ export const Select = (props: SelectProps) => { size = "medium", ...rest } = props; - const root = document.body.querySelector( - "[data-theme-provider]", - ) as HTMLButtonElement; + const root = useRootContainer(); return ( { size={size} /> {errorMessage} - + {children} diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx index 5fcd5ca50f4d..0458df47bacd 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx +++ b/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx @@ -1,8 +1,10 @@ +import clsx from "clsx"; import React from "react"; import { Icon, Spinner, textInputStyles } from "@appsmith/wds"; import { getTypographyClassName } from "@appsmith/wds-theming"; import { Button, Group, SelectValue } from "react-aria-components"; +import styles from "./styles.module.css"; import type { SelectProps } from "./types"; interface SelectTriggerProps { @@ -17,9 +19,11 @@ export const SelectTrigger: React.FC = (props) => { const { isDisabled, isInvalid, isLoading, placeholder, size } = props; return ( - + - - {Boolean(isLoading) ? : } - ); }; diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css new file mode 100644 index 000000000000..e4a05fbffd47 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css @@ -0,0 +1,25 @@ +/** + * Reason for using div.selectInputGroup instead of just .selectInputGroup is to increase the specificity of the selector + * Also, we are removing the padding-inline from selectInputGroup and adding it to selectTriggerButton + * This is done to ensure that width of the select popover is same as the width of the select trigger button. Otherwise, + * the width of the popover will be less than the width of the trigger button because of the padding-inline by .inputGroup . + */ +div.selectInputGroup { + padding-inline: 0; +} + +button.selectTriggerButton { + display: flex; + align-items: center; + padding-inline: var(--inner-spacing-2); +} + +.selectTriggerButton:has(> [data-select-text]) { + block-size: var(--body-line-height); +} + +.selectTriggerButton [data-select-text] { + display: flex; + align-items: center; + flex: 1; +} diff --git a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx index 8e44d7ce549c..0d99286e51b7 100644 --- a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx @@ -44,6 +44,13 @@ export const WithPrefixAndSuffix: Story = { prefix={} suffix={} /> + + +011 + + } + /> ), }; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/index.tsx index 4299b050633a..98e127acbb3c 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/index.tsx @@ -1,19 +1,79 @@ -import React from "react"; -import { Text, TextField } from "@appsmith/wds"; -import type { CurrencyInputComponentProps } from "./types"; -import { CurrencyTypeOptions } from "constants/Currency"; +import React, { type Key } from "react"; +import { + Button, + Menu, + MenuItem, + MenuTrigger, + Text, + TextField, +} from "@appsmith/wds"; import { useDebouncedValue } from "@mantine/hooks"; +import { CurrencyTypeOptions } from "constants/Currency"; + +import styles from "./styles.module.css"; +import { countryToFlag } from "./utilities"; +import type { CurrencyInputComponentProps } from "./types"; const DEBOUNCE_TIME = 300; export function CurrencyInputComponent(props: CurrencyInputComponentProps) { - const currency = CurrencyTypeOptions.find( + const { allowCurrencyChange, onCurrencyChange } = props; + const selectedCurrency = CurrencyTypeOptions.find( (option) => option.currency === props.currencyCode, ); - const prefix = ( - {currency?.symbol_native} - ); + const onMenuItemSelect = (key: Key) => { + const currency = CurrencyTypeOptions.find((option) => option.code === key); + + onCurrencyChange(currency?.currency); + }; + + const prefix = (function () { + if (allowCurrencyChange) { + return ( + + + + {CurrencyTypeOptions.map((item) => { + return ( + + {countryToFlag(item.code)}{" "} + {item.currency}{" "} + {item.currency_name} + + ); + })} + + + ); + } + + return ( + + {selectedCurrency?.symbol_native} + + ); + })(); // Note: because of how derived props are handled by MetaHoc, the isValid shows wrong // values for some milliseconds. To avoid that, we are using debounced value. diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/styles.module.css b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/styles.module.css new file mode 100644 index 000000000000..257c8dc9e98b --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/styles.module.css @@ -0,0 +1,3 @@ +.currencyOption [data-component="name"] { + color: var(--color-fg-neutral-subtle); +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/utilities.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/utilities.ts index ff23b8bdcc2d..ad61d5def857 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/utilities.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/component/utilities.ts @@ -63,3 +63,13 @@ export function parseLocaleFormattedStringToNumber(currencyString = "") { .replace(new RegExp("\\" + getLocaleDecimalSeperator()), "."), ); } + +export const countryToFlag = (isoCode: string) => { + return typeof String.fromCodePoint !== "undefined" + ? isoCode + .toUpperCase() + .replace(/./g, (char) => + String.fromCodePoint(char.charCodeAt(0) + 127397), + ) + : isoCode; +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/propertyPaneConfig/contentConfig.ts index da27a0d53b8f..7bba77a93fad 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/propertyPaneConfig/contentConfig.ts @@ -62,6 +62,16 @@ export const propertyPaneContentConfig = [ type: ValidationTypes.TEXT, }, }, + { + propertyName: "allowCurrencyChange", + label: "Allow currency change", + helpText: "Search by currency or country", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, { helpText: "No. of decimals in currency input", propertyName: "decimals", diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/component/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/component/index.tsx index ab7fa35a29b4..7f661b0a5a83 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/component/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/component/index.tsx @@ -1,19 +1,77 @@ -import React from "react"; +import React, { type Key } from "react"; import { ISDCodeOptions } from "constants/ISDCodes_v2"; -import { Text, TextField } from "@appsmith/wds"; +import { + Button, + Menu, + MenuItem, + MenuTrigger, + Text, + TextField, +} from "@appsmith/wds"; +import styles from "./styles.module.css"; +import { countryToFlag } from "../widget/helpers"; import type { PhoneInputComponentProps } from "./types"; export function PhoneInputComponent(props: PhoneInputComponentProps) { + const { allowDialCodeChange, onISDCodeChange } = props; + const selectedCountry = ISDCodeOptions.find( (option) => option.dial_code === props.dialCode, ); - const prefix = ( - {`${selectedCountry?.dial_code}`} - ); + const onMenuItemSelect = (key: Key) => { + const country = ISDCodeOptions.find((option) => option.code === key); + + onISDCodeChange(country?.dial_code); + }; + + const prefix = (function () { + if (allowDialCodeChange) { + return ( + + + + {ISDCodeOptions.map((item) => { + return ( + + {countryToFlag(item?.dial_code ?? "")}{" "} + {item.dial_code}{" "} + {item.name} + + ); + })} + + + ); + } + + return ( + {selectedCountry?.dial_code} + ); + })(); return ( Date: Thu, 19 Dec 2024 12:18:48 +0530 Subject: [PATCH 09/47] fix: Wrong pageId used for segment navigation (#38247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description > [!TIP] > _Add a TL;DR when the description is longer than 500 words or extremely technical (helps the content, marketing, and DevRel team)._ > > _Please also include relevant motivation and context. List any dependencies that are required for this change. Add links to Notion, Figma or any other documents that might be relevant to the PR._ Fixes #38216 ## Automation /ok-to-test tags="@tag.Git, @tag.IDE" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: 194f2ec70ff59c89f80410ee0088f16372decaa7 > Cypress dashboard. > Tags: `@tag.Git, @tag.IDE` > Spec: >
Thu, 19 Dec 2024 06:19:57 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Enhanced navigation management in the editor for improved user experience. - Streamlined retrieval of the base entity ID for navigation purposes. - **Bug Fixes** - Improved consistency in widget selection behavior based on current focus entity and keyboard events. --- app/client/src/pages/Editor/IDE/hooks.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/client/src/pages/Editor/IDE/hooks.ts b/app/client/src/pages/Editor/IDE/hooks.ts index 25f724ef5f5c..c6cb404b919c 100644 --- a/app/client/src/pages/Editor/IDE/hooks.ts +++ b/app/client/src/pages/Editor/IDE/hooks.ts @@ -27,8 +27,6 @@ import { closeJSActionTab } from "actions/jsActionActions"; import { closeQueryActionTab } from "actions/pluginActionActions"; import { getCurrentBasePageId } from "selectors/editorSelectors"; import { getCurrentEntityInfo } from "../utils"; -import { useEditorType } from "ee/hooks"; -import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks"; export const useCurrentEditorState = () => { const [selectedSegment, setSelectedSegment] = useState( @@ -60,9 +58,7 @@ export const useCurrentEditorState = () => { export const useSegmentNavigation = (): { onSegmentChange: (value: string) => void; } => { - const editorType = useEditorType(location.pathname); - const { parentEntityId: baseParentEntityId } = - useParentEntityInfo(editorType); + const basePageId = useSelector(getCurrentBasePageId); /** * Callback to handle the segment change @@ -74,17 +70,17 @@ export const useSegmentNavigation = (): { const onSegmentChange = (value: string) => { switch (value) { case EditorEntityTab.QUERIES: - history.push(queryListURL({ baseParentEntityId }), { + history.push(queryListURL({ basePageId }), { invokedBy: NavigationMethod.SegmentControl, }); break; case EditorEntityTab.JS: - history.push(jsCollectionListURL({ baseParentEntityId }), { + history.push(jsCollectionListURL({ basePageId }), { invokedBy: NavigationMethod.SegmentControl, }); break; case EditorEntityTab.UI: - history.push(widgetListURL({ baseParentEntityId }), { + history.push(widgetListURL({ basePageId }), { invokedBy: NavigationMethod.SegmentControl, }); break; From ad810c08117f350c536d93177036a761ae232e36 Mon Sep 17 00:00:00 2001 From: Valera Melnikov Date: Thu, 19 Dec 2024 10:22:39 +0300 Subject: [PATCH 10/47] chore: add custom consistent-storybook-title rule (#38241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Automation /ok-to-test tags="@tag.Sanity" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: c556bda0ecbae89388821185ab86d340c553bc1e > Cypress dashboard. > Tags: `@tag.Sanity` > Spec: >
Wed, 18 Dec 2024 16:46:31 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **New Features** - Updated titles for various components in Storybook for improved readability (e.g., "NumberInput" to "Number Input"). - Introduced a new ESLint rule to enforce Title Case formatting for Storybook titles. - **Bug Fixes** - Enhanced error handling and validation for Storybook titles through the new ESLint rule. - **Documentation** - Added test cases for the new ESLint rule to validate title formats in Storybook configurations. --- .../src/NumberInput/NumberInput.stories.tsx | 2 +- .../src/SearchInput/SearchInput.stories.tsx | 2 +- .../Templates/IDEHeader/IDEHeader.stories.tsx | 2 +- .../ComboBox/stories/ComboBox.stories.tsx | 2 +- .../Datepicker/stories/Datepicker.stories.tsx | 2 +- .../IconButton/stories/IconButton.stories.tsx | 2 +- .../stories/InlineButtons.stories.tsx | 2 +- .../RadioGroup.chromatic.stories.tsx | 2 +- .../RadioGroup/stories/RadioGroup.stories.tsx | 2 +- .../TagGroup/stories/TagGroup.stories.tsx | 2 +- .../TextArea/stories/TextArea.stories.tsx | 2 +- .../TextField/stories/TextField.stories.tsx | 2 +- .../TimeField/stories/TimeField.stories.tsx | 2 +- .../stories/ToggleGroup.stories.tsx | 2 +- .../stories/ToolbarButtons.stories.tsx | 2 +- .../widgets/src/testing/ColorGrid.stories.tsx | 2 +- .../consistent-storybook-title/rule.test.ts | 103 +++++++++++++++ .../src/consistent-storybook-title/rule.ts | 117 ++++++++++++++++++ .../packages/eslint-plugin/src/index.ts | 3 + 19 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts create mode 100644 app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts diff --git a/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx b/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx index 543d7531c0c3..350fb658ed7c 100644 --- a/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx +++ b/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx @@ -4,7 +4,7 @@ import type { NumberInputProps } from "./NumberInput.types"; import type { StoryObj } from "@storybook/react"; export default { - title: "ADS/Components/Input/NumberInput", + title: "ADS/Components/Input/Number Input", component: NumberInput, decorators: [ (Story: () => React.ReactNode) => ( diff --git a/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx b/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx index b6399f4ceb84..306e0e64a007 100644 --- a/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx +++ b/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx @@ -4,7 +4,7 @@ import type { SearchInputProps } from "./SearchInput.types"; import type { StoryObj } from "@storybook/react"; export default { - title: "ADS/Components/Input/SearchInput", + title: "ADS/Components/Input/Search Input", component: SearchInput, decorators: [ (Story: () => React.ReactNode) => ( diff --git a/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx b/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx index 6c709080dd52..c24b5e8eafda 100644 --- a/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx +++ b/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx @@ -12,7 +12,7 @@ import { Text } from "../../Text"; import { ListHeaderContainer } from "../EntityExplorer/styles"; const meta: Meta = { - title: "ADS/Templates/IDEHeader", + title: "ADS/Templates/IDE Header", component: IDEHeader, parameters: { layout: "fullscreen", diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx index 5f01ae804ca7..36a64aa1c2d8 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx @@ -6,7 +6,7 @@ import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds"; import { items } from "./items"; const meta: Meta = { - title: "WDS/Widgets/ComboBox", + title: "WDS/Widgets/Combo Box", component: ComboBox, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx b/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx index bfb042895983..37137ed02297 100644 --- a/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx @@ -10,7 +10,7 @@ import { DatePicker } from "../src"; */ const meta: Meta = { component: DatePicker, - title: "WDS/Widgets/DatePicker", + title: "WDS/Widgets/Date Picker", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx b/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx index 96fdcf0343dc..a5241ff2ca7f 100644 --- a/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx @@ -14,7 +14,7 @@ import { objectKeys } from "@appsmith/utils"; */ const meta: Meta = { component: IconButton, - title: "WDS/Widgets/IconButton", + title: "WDS/Widgets/Icon Button", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx index 44a274739d78..c30b43e51a2a 100644 --- a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx @@ -21,7 +21,7 @@ import { */ const meta: Meta = { component: InlineButtons, - title: "WDS/Widgets/InlineButtons", + title: "WDS/Widgets/Inline Buttons", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx index 2225cc3f1e01..86e63c2f8c87 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx @@ -6,7 +6,7 @@ import { StoryGrid, DataAttrWrapper } from "@design-system/storybook"; const meta: Meta = { component: RadioGroup, - title: "Design System/Widgets/RadioGroup", + title: "Design System/Widgets/Radio Group", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx index e16fdfbc1874..9ed7e7417f02 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx @@ -8,7 +8,7 @@ const items = [ ]; const meta: Meta = { - title: "WDS/Widgets/RadioGroup", + title: "WDS/Widgets/Radio Group", component: RadioGroup, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx index 2325e2b81e27..0fa6d3d77803 100644 --- a/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx @@ -8,7 +8,7 @@ import type { TagGroupProps } from "../src/TagGroup"; */ const meta: Meta = { component: TagGroup, - title: "WDS/Widgets/TagGroup", + title: "WDS/Widgets/Tag Group", subcomponents: { Tag, }, diff --git a/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx index b2b66dc45ae1..7cbd9126b494 100644 --- a/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Flex, TextArea, Button } from "@appsmith/wds"; const meta: Meta = { - title: "WDS/Widgets/TextArea", + title: "WDS/Widgets/Text Area", component: TextArea, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx index 0d99286e51b7..f623264448d1 100644 --- a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Flex, Icon, TextField, Button } from "@appsmith/wds"; const meta: Meta = { - title: "WDS/Widgets/TextField", + title: "WDS/Widgets/Text Field", component: TextField, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx index a498cd5826fd..3c1b13b70b16 100644 --- a/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx @@ -4,7 +4,7 @@ import { TimeField } from "../src"; import { Time } from "@internationalized/date"; const meta: Meta = { - title: "WDS/Widgets/TimeField", + title: "WDS/Widgets/Time Field", component: TimeField, parameters: { docs: { diff --git a/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx index 5db40b210272..995ed35bd5a4 100644 --- a/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx @@ -8,7 +8,7 @@ const items = [ ]; const meta: Meta = { - title: "WDS/Widgets/ToggleGroup", + title: "WDS/Widgets/Toggle Group", component: ToggleGroup, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx index 917338e6b3cb..2fdeaa192ba7 100644 --- a/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx @@ -21,7 +21,7 @@ import { */ const meta: Meta = { component: ToolbarButtons, - title: "WDS/Widgets/ToolbarButtons", + title: "WDS/Widgets/Toolbar Buttons", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx b/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx index d67a774feb21..977f95427f84 100644 --- a/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx +++ b/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx @@ -4,7 +4,7 @@ import { ColorGrid } from "./ColorGrid"; const meta: Meta = { component: ColorGrid, - title: "WDS/Testing/ColorGrid", + title: "WDS/Testing/Color Grid", args: { source: "oklch", size: "small", diff --git a/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts new file mode 100644 index 000000000000..b55b7948a244 --- /dev/null +++ b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts @@ -0,0 +1,103 @@ +import { TSESLint } from "@typescript-eslint/utils"; +import { consistentStorybookTitle } from "./rule"; + +const ruleTester = new TSESLint.RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}); + +ruleTester.run("storybook-title-case", consistentStorybookTitle, { + valid: [ + { + code: `export default { title: "ADS/Templates/IDE Header" };`, + filename: "example.stories.tsx", + }, + { + code: `export default { title: "ADS/Components/Input/IDE Search Input" };`, + filename: "example.stories.tsx", + }, + { + code: `export default { title: "ADS/Components/Input/AAA Number Input" };`, + filename: "example.stories.tsx", + }, + { + code: `const meta = { title: "WDS/Widgets/Button" };`, + filename: "example.stories.tsx", + }, + ], + invalid: [ + { + code: `export default { title: "ADS/Templates/IDEHeader" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Templates/IDEHeader", + suggestedTitle: "ADS/Templates/IDE Header", + }, + }, + ], + output: `export default { title: "ADS/Templates/IDE Header" };`, + }, + { + code: `export default { title: "ADS/Components/Input/IDESearch Input" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Components/Input/IDESearch Input", + suggestedTitle: "ADS/Components/Input/IDE Search Input", + }, + }, + ], + output: `export default { title: "ADS/Components/Input/IDE Search Input" };`, + }, + { + code: `export default { title: "ADS/Components/Input/IDESearchInput" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Components/Input/IDESearchInput", + suggestedTitle: "ADS/Components/Input/IDE Search Input", + }, + }, + ], + output: `export default { title: "ADS/Components/Input/IDE Search Input" };`, + }, + { + code: `export default { title: "ADS/Components/Input/AAANumber Input" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Components/Input/AAANumber Input", + suggestedTitle: "ADS/Components/Input/AAA Number Input", + }, + }, + ], + output: `export default { title: "ADS/Components/Input/AAA Number Input" };`, + }, + { + code: `export default { title: "WDS/Widgets/button" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "WDS/Widgets/button", + suggestedTitle: "WDS/Widgets/Button", + }, + }, + ], + output: `export default { title: "WDS/Widgets/Button" };`, + }, + ], +}); diff --git a/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts new file mode 100644 index 000000000000..aed34b0eb2f2 --- /dev/null +++ b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts @@ -0,0 +1,117 @@ +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; + +const titleCase = (str: string): string => { + return str + .split("/") + .map( + (segment) => + segment + .replace(/([A-Z]+)([A-Z][a-z0-9])/g, "$1 $2") // Acronyms followed by words + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") // Lowercase followed by uppercase + .replace(/\b\w/g, (char) => char.toUpperCase()), // Capitalize first letter + ) + .join("/"); +}; + +export const consistentStorybookTitle: TSESLint.RuleModule<"invalidTitle", []> = + { + defaultOptions: [], + meta: { + type: "problem", + docs: { + description: + "Ensure Storybook titles in `export default` or meta objects are in Title Case", + recommended: "error", + }, + messages: { + invalidTitle: + 'The Storybook title "{{ title }}" is not in Title Case. Suggested: "{{ suggestedTitle }}".', + }, + schema: [], // No options + fixable: "code", // Allows auto-fixing + }, + + create(context) { + const filename = context.getFilename(); + const isStoryFile = filename.endsWith(".stories.tsx"); // Apply only to *.stories.tsx files + + if (!isStoryFile) { + return {}; // No-op for non-story files + } + + const validateTitle = (title: string, node: TSESTree.Node) => { + const expectedTitle = titleCase(title); + + if (title !== expectedTitle) { + context.report({ + node, + messageId: "invalidTitle", + data: { + title, + suggestedTitle: expectedTitle, + }, + fix: (fixer) => fixer.replaceText(node, `"${expectedTitle}"`), // Use double quotes + }); + } + }; + + return { + ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration) { + if ( + node.declaration.type === "ObjectExpression" && + node.declaration.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title" && + prop.value.type === "Literal" && + typeof prop.value.value === "string", + ) + ) { + const titleProperty = node.declaration.properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title", + ) as TSESTree.Property; + + const titleValue = titleProperty.value as TSESTree.Literal; + + if (typeof titleValue.value === "string") { + validateTitle(titleValue.value, titleValue); + } + } + }, + + VariableDeclaration(node: TSESTree.VariableDeclaration) { + node.declarations.forEach((declaration) => { + if ( + declaration.init && + declaration.init.type === "ObjectExpression" && + declaration.init.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title" && + prop.value.type === "Literal" && + typeof prop.value.value === "string", + ) + ) { + const titleProperty = declaration.init.properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title", + ) as TSESTree.Property; + + const titleValue = titleProperty.value as TSESTree.Literal; + + if (typeof titleValue.value === "string") { + validateTitle(titleValue.value, titleValue); + } + } + }); + }, + }; + }, + }; diff --git a/app/client/packages/eslint-plugin/src/index.ts b/app/client/packages/eslint-plugin/src/index.ts index eda6deb6d349..49fb2417f454 100644 --- a/app/client/packages/eslint-plugin/src/index.ts +++ b/app/client/packages/eslint-plugin/src/index.ts @@ -1,16 +1,19 @@ import { objectKeysRule } from "./object-keys/rule"; import { namedUseEffectRule } from "./named-use-effect/rule"; +import { consistentStorybookTitle } from "./consistent-storybook-title/rule"; const plugin = { rules: { "object-keys": objectKeysRule, "named-use-effect": namedUseEffectRule, + "consistent-storybook-title": consistentStorybookTitle, }, configs: { recommended: { rules: { "@appsmith/object-keys": "warn", "@appsmith/named-use-effect": "warn", + "@appsmith/consistent-storybook-title": "error", }, }, }, From 68264f1e2edbeace8c7e32ef777c432a85fc2394 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 Dec 2024 11:13:13 +0300 Subject: [PATCH 11/47] chore: extract search and add to ads (#38192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Extract search and add to ADS. Fixes [#37888](https://github.com/appsmithorg/appsmith/issues/37888) ## Automation /ok-to-test tags="@tag.IDE, @tag.Sanity" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: dce12fbeebae3ef23d9bd251a8096b09306a06d5 > Cypress dashboard. > Tags: `@tag.IDE, @tag.Sanity` > Spec: >
Thu, 19 Dec 2024 07:44:13 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced the `SearchAndAdd` component for enhanced search and add functionality. - Added a new Storybook story for the `SearchAndAdd` component, providing visual documentation and interaction examples. - **Bug Fixes** - Replaced the deprecated `AddAndSearchbar` component with `SearchAndAdd` in relevant areas of the application. - **Documentation** - Enhanced documentation for the `SearchAndAdd` component through Storybook. - **Chores** - Updated import statements to reflect the new component structure and removed obsolete components. - Modified locators to use data attributes for improved element identification in tests. - Added specific identifiers for testing frameworks to enhance testability of components. --- .../cypress/support/Objects/CommonLocators.ts | 2 +- .../cypress/support/Pages/IDE/ListView.ts | 2 +- .../SearchAndAdd/SearchAndAdd.stories.tsx | 38 ++++++++++++++++ .../SearchAndAdd/SearchAndAdd.styles.tsx | 16 +++++++ .../SearchAndAdd/SearchAndAdd.tsx | 43 +++++++++++++++++++ .../SearchAndAdd/SearchAndAdd.types.ts | 12 ++++++ .../EntityExplorer/SearchAndAdd/index.ts | 2 + .../ads/src/Templates/EntityExplorer/index.ts | 1 + .../pages/Editor/IDE/EditorPane/JS/List.tsx | 9 ++-- .../Editor/IDE/EditorPane/Query/List.tsx | 9 ++-- .../pages/Editor/IDE/EditorPane/UI/List.tsx | 2 + .../EditorPane/components/AddAndSearchbar.tsx | 29 ------------- 12 files changed, 124 insertions(+), 41 deletions(-) create mode 100644 app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.stories.tsx create mode 100644 app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx create mode 100644 app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx create mode 100644 app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.types.ts create mode 100644 app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/index.ts delete mode 100644 app/client/src/pages/Editor/IDE/EditorPane/components/AddAndSearchbar.tsx diff --git a/app/client/cypress/support/Objects/CommonLocators.ts b/app/client/cypress/support/Objects/CommonLocators.ts index 07006699a065..ebf4f547697e 100644 --- a/app/client/cypress/support/Objects/CommonLocators.ts +++ b/app/client/cypress/support/Objects/CommonLocators.ts @@ -86,7 +86,7 @@ export class CommonLocators { _anvilDnDHighlight = "[data-type=anvil-dnd-highlight]"; _editPage = "[data-testid=onboarding-tasks-datasource-text], .t--drop-target"; _crossBtn = "span.cancel-icon"; - _createNew = ".t--add-item"; + _createNew = "[data-testid='t--add-item']"; _uploadFiles = "div.uppy-Dashboard-AddFiles input"; _uploadBtn = "button.uppy-StatusBar-actionBtn--upload"; _errorTab = "[data-testid=t--tab-ERROR_TAB]"; diff --git a/app/client/cypress/support/Pages/IDE/ListView.ts b/app/client/cypress/support/Pages/IDE/ListView.ts index 0da47d2b2623..f9f4352af532 100644 --- a/app/client/cypress/support/Pages/IDE/ListView.ts +++ b/app/client/cypress/support/Pages/IDE/ListView.ts @@ -4,7 +4,7 @@ class ListView { public locators = { list: "[data-testid='t--ide-list']", listItem: "[data-testid='t--ide-list-item']", - addItem: "button.t--add-item", + addItem: "[data-testid='t--add-item']", }; public assertListVisibility() { diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.stories.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.stories.tsx new file mode 100644 index 000000000000..9d40d80145b0 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.stories.tsx @@ -0,0 +1,38 @@ +/* eslint-disable no-console */ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { SearchAndAdd, type SearchAndAddProps } from "."; + +const meta: Meta = { + title: "ADS/Templates/Entity Explorer/Search And Add", + component: SearchAndAdd, +}; + +export default meta; + +const Template = (props: SearchAndAddProps) => { + const { onAdd, onSearch, placeholder, showAddButton = true } = props; + + return ( +
+ +
+ ); +}; + +export const Basic = Template.bind({}) as StoryObj; + +Basic.args = { + onAdd: () => console.log("Add clicked"), + onSearch: (searchTerm: string) => console.log(searchTerm), + placeholder: "Search", + showAddButton: true, +}; diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx new file mode 100644 index 000000000000..cb734378ea67 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx @@ -0,0 +1,16 @@ +import styled from "styled-components"; + +import { Button } from "@appsmith/ads"; + +export const Root = styled.div` + display: flex; + gap: var(--ads-v2-spaces-3); + width: 100%; +`; + +export const SquareButton = styled(Button)` + && { + max-width: 24px; + min-width: 24px; + } +`; diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx new file mode 100644 index 000000000000..e00766fc71a5 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx @@ -0,0 +1,43 @@ +import React, { forwardRef } from "react"; + +import { SearchInput } from "@appsmith/ads"; +import * as Styles from "./SearchAndAdd.styles"; +import type { SearchAndAddProps } from "./SearchAndAdd.types"; + +export const SearchAndAdd = forwardRef( + (props, ref) => { + const { + onAdd, + onSearch, + placeholder, + searchTerm = "", + showAddButton, + } = props; + + return ( + + + {showAddButton && ( + + )} + + ); + }, +); + +SearchAndAdd.displayName = "SearchAndAdd"; diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.types.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.types.ts new file mode 100644 index 000000000000..f1ab9960287b --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.types.ts @@ -0,0 +1,12 @@ +export interface SearchAndAddProps { + /** Placeholder text for search input. */ + placeholder?: string; + /** Value for search input in controlled mode. */ + searchTerm?: string; + /** Callback to be called when add button is clicked. */ + onAdd?: () => void; + /** Callback to be called when search input value changes. */ + onSearch?: (searchTerm: string) => void; + /** Whether to show the add button. Allows to hide the button for users with insufficient access privileges. */ + showAddButton: boolean; +} diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/index.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/index.ts new file mode 100644 index 000000000000..f1bd4354942d --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/index.ts @@ -0,0 +1,2 @@ +export { SearchAndAdd } from "./SearchAndAdd"; +export * from "./SearchAndAdd.types"; diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts index 95639a0663f6..2c77164adaba 100644 --- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts @@ -1,3 +1,4 @@ export { ListItemContainer, ListHeaderContainer } from "./styles"; export { ListWithHeader } from "./ListWithHeader"; export { EditorSegments } from "./EditorSegments"; +export * from "./SearchAndAdd"; diff --git a/app/client/src/pages/Editor/IDE/EditorPane/JS/List.tsx b/app/client/src/pages/Editor/IDE/EditorPane/JS/List.tsx index bdfd8a15c965..7812572bc7ae 100644 --- a/app/client/src/pages/Editor/IDE/EditorPane/JS/List.tsx +++ b/app/client/src/pages/Editor/IDE/EditorPane/JS/List.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useSelector } from "react-redux"; -import { Flex, Text } from "@appsmith/ads"; +import { Flex, Text, SearchAndAdd } from "@appsmith/ads"; import styled from "styled-components"; import { selectJSSegmentEditorList } from "ee/selectors/appIDESelectors"; @@ -18,7 +18,6 @@ import { FilesContextProvider } from "pages/Editor/Explorer/Files/FilesContextPr import { useJSAdd } from "ee/pages/Editor/IDE/EditorPane/JS/hooks"; import { JSListItem } from "ee/pages/Editor/IDE/EditorPane/JS/ListItem"; import { BlankState } from "./BlankState"; -import { AddAndSearchbar } from "../components/AddAndSearchbar"; import { EmptySearchResult } from "../components/EmptySearchResult"; import { EDITOR_PANE_TEXTS, createMessage } from "ee/constants/messages"; import { filterEntityGroupsBySearchTerm } from "IDE/utils"; @@ -64,10 +63,10 @@ const ListJSObjects = () => { py="spaces-3" > {itemGroups && itemGroups.length > 0 ? ( - ) : null} { py="spaces-3" > {itemGroups.length > 0 ? ( - ) : null} diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx b/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx index e0e84b2974ad..491d89ce9ac2 100644 --- a/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx +++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx @@ -63,6 +63,7 @@ const ListWidgets = (props: { /* If no widgets exist, show the blank state */