From efab105e1995a30a1de6bd8a618738dbec3971d2 Mon Sep 17 00:00:00 2001 From: Trisha Anand Date: Wed, 19 Aug 2020 15:20:00 +0530 Subject: [PATCH 1/2] Clone Page feature inside an application (#357) * Working version of cloning page given page id. The clone is created inside the same application and is in unpublished state. * Added a test case for Clone Page feature * Incorporated review comments. --- .../server/controllers/PageController.java | 6 ++ .../services/ApplicationPageService.java | 2 + .../services/ApplicationPageServiceImpl.java | 94 ++++++++++++++++++- .../services/LayoutActionServiceImpl.java | 3 - .../server/services/PageServiceTest.java | 40 +++++++- 5 files changed, 139 insertions(+), 6 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java index 3f8e3316d4..6cfb1714d9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java @@ -84,4 +84,10 @@ public class PageController extends BaseController { return service.delete(id) .map(deletedResource -> new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null)); } + + @PostMapping("/clone/{pageId}") + public Mono> clonePage(@PathVariable String pageId) { + return applicationPageService.clonePage(pageId) + .map(page -> new ResponseDTO<>(HttpStatus.CREATED.value(), page, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java index 537a575a26..2241048c15 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java @@ -23,4 +23,6 @@ public interface ApplicationPageService { Mono cloneApplication(Application application); Mono deleteApplication(String id); + + Mono clonePage(String pageId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java index 712ecf2abd..e22424c627 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java @@ -5,19 +5,23 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.constants.AnalyticsEvents; import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ApplicationPagesDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.repositories.ApplicationRepository; import com.mongodb.client.result.UpdateResult; import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.ArrayList; @@ -26,6 +30,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS; @@ -40,26 +45,32 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { private final PageService pageService; private final SessionUserService sessionUserService; private final OrganizationService organizationService; + private final LayoutActionService layoutActionService; private final AnalyticsService analyticsService; private final PolicyGenerator policyGenerator; private final ApplicationRepository applicationRepository; + private final ActionService actionService; public ApplicationPageServiceImpl(ApplicationService applicationService, PageService pageService, SessionUserService sessionUserService, OrganizationService organizationService, + LayoutActionService layoutActionService, AnalyticsService analyticsService, PolicyGenerator policyGenerator, - ApplicationRepository applicationRepository) { + ApplicationRepository applicationRepository, + ActionService actionService) { this.applicationService = applicationService; this.pageService = pageService; this.sessionUserService = sessionUserService; this.organizationService = organizationService; + this.layoutActionService = layoutActionService; this.analyticsService = analyticsService; this.policyGenerator = policyGenerator; this.applicationRepository = applicationRepository; + this.actionService = actionService; } public Mono createPage(Page page) { @@ -324,4 +335,85 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { .flatMap(deletedObj -> analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Application) deletedObj)); } + @Override + public Mono clonePage(String pageId) { + + // Find the source page and then prune the page layout fields to only contain the required fields that should be + // copied. + Mono sourcePageMono = pageService.findById(pageId, MANAGE_PAGES) + .flatMap(page -> Flux.fromIterable(page.getLayouts()) + .map(layout -> layout.getDsl()) + .map(dsl -> { + Layout newLayout = new Layout(); + String id = new ObjectId().toString(); + newLayout.setId(id); + newLayout.setDsl(dsl); + return newLayout; + }) + .collectList() + .map(layouts -> { + page.setLayouts(layouts); + return page; + })); + + Flux sourceActionFlux = actionService.findByPageId(pageId, MANAGE_ACTIONS); + + return sourcePageMono + .flatMap(page -> { + Mono pageNamesMono = pageService + .findNamesByApplicationId(page.getApplicationId()); + return pageNamesMono + // Set a unique name for the cloned page and then create the page. + .flatMap(pageNames -> { + Set names = pageNames.getPages() + .stream() + .map(pageNameIdDTO -> pageNameIdDTO.getName()).collect(Collectors.toSet()); + + String newPageName = page.getName() + " Copy"; + int i = 0; + String name = newPageName; + while(names.contains(name)) { + i++; + name = newPageName + i; + } + newPageName = name; + // Now we have a unique name. Proceed with creating the copy of the page + page.setId(null); + page.setName(newPageName); + return pageService.createDefault(page); + }); + }) + .flatMap(page -> { + String newPageId = page.getId(); + return sourceActionFlux + .flatMap(action -> { + action.setId(null); + action.setPageId(newPageId); + return actionService.create(action); + }) + .collectList() + .thenReturn(page); + }) + // Calculate the onload actions for this page now that the page and actions have been created + .flatMap(savedPage -> { + List layouts = savedPage.getLayouts(); + + return Flux.fromIterable(layouts) + .flatMap(layout -> layoutActionService.updateLayout(savedPage.getId(), layout.getId(), layout)) + .collectList() + .thenReturn(savedPage); + }) + .flatMap(page -> { + Mono applicationMono = applicationService.findById(page.getApplicationId(), MANAGE_APPLICATIONS); + return applicationMono + .flatMap(application -> { + ApplicationPage applicationPage = new ApplicationPage(); + applicationPage.setId(page.getId()); + application.getPages().add(applicationPage); + return applicationService.save(application) + .thenReturn(page); + }); + }); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java index 029224c850..728192afc9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java @@ -46,7 +46,6 @@ public class LayoutActionServiceImpl implements LayoutActionService { private final ActionService actionService; private final PageService pageService; private final ObjectMapper objectMapper; - private final ApplicationPageService applicationPageService; private final AnalyticsService analyticsService; /* * This pattern finds all the String which have been extracted from the mustache dynamic bindings. @@ -67,12 +66,10 @@ public class LayoutActionServiceImpl implements LayoutActionService { public LayoutActionServiceImpl(ActionService actionService, PageService pageService, ObjectMapper objectMapper, - ApplicationPageService applicationPageService, AnalyticsService analyticsService) { this.actionService = actionService; this.pageService = pageService; this.objectMapper = objectMapper; - this.applicationPageService = applicationPageService; this.analyticsService = analyticsService; } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java index 46603a9bb3..076f6c6b99 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java @@ -1,7 +1,6 @@ package com.appsmith.server.services; import com.appsmith.external.models.Policy; -import com.appsmith.server.configurations.WithMockAppsmithUser; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Page; @@ -17,7 +16,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; @@ -174,6 +172,44 @@ public class PageServiceTest { .verifyComplete(); } + @Test + @WithUserDetails(value = "api_user") + public void clonePage() throws ParseException { + Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue()) + .users(Set.of("api_user")) + .build(); + Policy readPagePolicy = Policy.builder().permission(READ_PAGES.getValue()) + .users(Set.of("api_user")) + .build(); + + Page testPage = new Page(); + testPage.setName("PageServiceTest CloneTest Source"); + setupTestApplication(); + testPage.setApplicationId(application.getId()); + + Mono pageMono = applicationPageService.createPage(testPage) + .flatMap(page -> applicationPageService.clonePage(page.getId())); + + Object parsedJson = new JSONParser(JSONParser.MODE_PERMISSIVE).parse(FieldName.DEFAULT_PAGE_LAYOUT); + StepVerifier + .create(pageMono) + .assertNext(page -> { + assertThat(page).isNotNull(); + assertThat(page.getId()).isNotNull(); + assertThat("PageServiceTest CloneTest Source Copy".equals(page.getName())); + + assertThat(page.getPolicies()).isNotEmpty(); + assertThat(page.getPolicies()).containsOnly(managePagePolicy, readPagePolicy); + + assertThat(page.getLayouts()).isNotEmpty(); + assertThat(page.getLayouts().get(0).getDsl()).isEqualTo(parsedJson); + assertThat(page.getLayouts().get(0).getWidgetNames()).isNotEmpty(); + assertThat(page.getLayouts().get(0).getPublishedDsl()).isNullOrEmpty(); + }) + .verifyComplete(); + } + + @After public void purgeAllPages() { pageService.deleteAll(); From 6b411cae7a507865e6546ee7bfc309d21a696c67 Mon Sep 17 00:00:00 2001 From: Tejaaswini Narendra <67053685+tejaaswini-narendra@users.noreply.github.com> Date: Wed, 19 Aug 2020 15:32:33 +0530 Subject: [PATCH 2/2] fix: Add loading indicator while fetching user details. (#329) * fix: Add loading indicator while fetching user details. * update: Variable name --- .../pages/organization/OrgInviteUsersForm.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/client/src/pages/organization/OrgInviteUsersForm.tsx b/app/client/src/pages/organization/OrgInviteUsersForm.tsx index 115d8899de..aeaa176554 100644 --- a/app/client/src/pages/organization/OrgInviteUsersForm.tsx +++ b/app/client/src/pages/organization/OrgInviteUsersForm.tsx @@ -13,6 +13,7 @@ import { getAllUsers, getCurrentOrg, } from "selectors/organizationSelectors"; +import Spinner from "components/editorComponents/Spinner"; import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { InviteUsersToOrgFormValues, inviteUsersToOrg } from "./helpers"; import { INVITE_USERS_TO_ORG_FORM } from "constants/forms"; @@ -110,6 +111,12 @@ const StyledButton = styled(Button)` } `; +const Loading = styled(Spinner)` + padding-top: 10px; + margin: auto; + width: 100%; +`; + const validateFormValues = (values: { users: string; role: string }) => { if (values.users && values.users.length > 0) { const _users = values.users.split(",").filter(Boolean); @@ -158,6 +165,7 @@ const OrgInviteUsersForm = (props: any) => { fetchCurrentOrg, currentOrg, isApplicationInvite, + isLoading, } = props; const currentPath = useLocation().pathname; @@ -237,16 +245,20 @@ const OrgInviteUsersForm = (props: any) => { type="submit" /> - - {allUsers.map((user: { username: string; roleName: string }) => { - return ( -
-
{user.username}
-
{user.roleName}
-
- ); - })} -
+ {isLoading ? ( + + ) : ( + + {allUsers.map((user: { username: string; roleName: string }) => { + return ( +
+
{user.username}
+
{user.roleName}
+
+ ); + })} +
+ )} {!pathRegex.test(currentPath) && canManage && (