Move action also works successfully in removing the action from the previous page's onLoadActions while moving to another page.

This commit is contained in:
Trisha Anand 2020-01-13 12:07:10 +00:00
parent a542abc563
commit 28e20ed3bd
12 changed files with 271 additions and 146 deletions

View File

@ -3,11 +3,13 @@ package com.appsmith.server.controllers;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Action;
import com.appsmith.server.dtos.ActionMoveDTO;
import com.appsmith.server.dtos.ExecuteActionDTO;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.services.ActionCollectionService;
import com.appsmith.server.services.ActionService;
import com.appsmith.server.services.LayoutActionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -28,12 +30,15 @@ import javax.validation.Valid;
public class ActionController extends BaseController<ActionService, Action, String> {
private final ActionCollectionService actionCollectionService;
private final LayoutActionService layoutActionService;
@Autowired
public ActionController(ActionService service,
ActionCollectionService actionCollectionService) {
ActionCollectionService actionCollectionService,
LayoutActionService layoutActionService) {
super(service);
this.actionCollectionService = actionCollectionService;
this.layoutActionService = layoutActionService;
}
@PostMapping
@ -56,4 +61,11 @@ public class ActionController extends BaseController<ActionService, Action, Stri
return service.executeAction(executeActionDTO)
.map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null));
}
@PutMapping("/move")
public Mono<ResponseDTO<Action>> moveAction(@RequestBody @Valid ActionMoveDTO actionMoveDTO) {
log.debug("Going to move action {} from page {} to page {}", actionMoveDTO.getAction().getName(), actionMoveDTO.getAction().getPageId(), actionMoveDTO.getDestinationPageId());
return layoutActionService.moveAction(actionMoveDTO)
.map(action -> new ResponseDTO<>(HttpStatus.OK.value(), action, null));
}
}

View File

@ -3,6 +3,7 @@ package com.appsmith.server.controllers;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.services.LayoutActionService;
import com.appsmith.server.services.LayoutService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -22,10 +23,13 @@ import javax.validation.Valid;
public class LayoutController {
private final LayoutService service;
private final LayoutActionService layoutActionService;
@Autowired
public LayoutController(LayoutService layoutService) {
public LayoutController(LayoutService layoutService,
LayoutActionService layoutActionService) {
this.service = layoutService;
this.layoutActionService = layoutActionService;
}
@PostMapping("/pages/{pageId}")
@ -42,7 +46,7 @@ public class LayoutController {
@PutMapping("/{layoutId}/pages/{pageId}")
public Mono<ResponseDTO<Layout>> updateLayout(@PathVariable String pageId, @PathVariable String layoutId, @RequestBody Layout layout) {
return service.updateLayout(pageId, layoutId, layout)
return layoutActionService.updateLayout(pageId, layoutId, layout)
.map(created -> new ResponseDTO<>(HttpStatus.OK.value(), created, null));
}

View File

@ -37,7 +37,7 @@ public class RestApiImportController {
ApiImporter service;
switch (type) {
case CURL :
case CURL:
service = curlImporterService;
break;
default:

View File

@ -50,7 +50,7 @@ public class UserController extends BaseController<UserService, User, String> {
* in order to construct client facing URLs that will be sent to the user over email.
*
* @param userPasswordDTO
* @param originHeader The Origin header in the request. This is a mandatory parameter.
* @param originHeader The Origin header in the request. This is a mandatory parameter.
* @return
*/
@PostMapping("/forgotPassword")
@ -83,7 +83,7 @@ public class UserController extends BaseController<UserService, User, String> {
* This function creates an invite for a new user to join the Appsmith platform. We require the Origin header
* in order to construct client facing URLs that will be sent to the user via email.
*
* @param user The user object for the new user being invited to the Appsmith platform
* @param user The user object for the new user being invited to the Appsmith platform
* @param originHeader Origin header in the request
* @return
*/

View File

@ -0,0 +1,17 @@
package com.appsmith.server.dtos;
import com.appsmith.server.domains.Action;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
@Getter
@Setter
public class ActionMoveDTO {
@NotNull
Action action;
@NotNull
String destinationPageId;
}

View File

@ -430,8 +430,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
public Flux<Action> saveAll(List<Action> actions) {
return repository.saveAll(actions);
}
/**
* This function replaces the variables in the Object with the actual params
*/

View File

@ -27,7 +27,7 @@ import java.util.regex.Pattern;
@Service
@Slf4j
public class CurlImporterService extends BaseApiImporter{
public class CurlImporterService extends BaseApiImporter {
private static final String headerRegex = "\\-H\\s+\\'(.+?)\\'";
private static final String methodRegex = "\\-X\\s+(.+?)\\b";
@ -56,21 +56,21 @@ public class CurlImporterService extends BaseApiImporter{
// Find all the headers here
List<Property> headers = actionConfiguration.getHeaders();
while (headerMatcher.find()) {
String headerString = headerMatcher.group();
String[] splitHeader = headerString.split("'");
String headerString = headerMatcher.group();
String[] splitHeader = headerString.split("'");
String header = splitHeader[1];
String[] keyValuePairInString = header.split(":");
String header = splitHeader[1];
String[] keyValuePairInString = header.split(":");
Property property = new Property();
property.setKey(keyValuePairInString[0]);
property.setValue(keyValuePairInString[1]);
Property property = new Property();
property.setKey(keyValuePairInString[0]);
property.setValue(keyValuePairInString[1]);
if (headers == null) {
headers = new ArrayList<>();
}
headers.add(property);
if (headers == null) {
headers = new ArrayList<>();
}
headers.add(property);
}
actionConfiguration.setHeaders(headers);
@ -93,7 +93,7 @@ public class CurlImporterService extends BaseApiImporter{
Boolean urlFound = false;
// Find the URL now
//Ignoring the first word which is "curl"
for (int i = 1; i< cmdSplit.length; i++) {
for (int i = 1; i < cmdSplit.length; i++) {
try {
// If the string doesnt throw an exception when being converted to a URI, its a valid URL.
URI uri = new URL(cmdSplit[i]).toURI();

View File

@ -0,0 +1,12 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.dtos.ActionMoveDTO;
import reactor.core.publisher.Mono;
public interface LayoutActionService {
public Mono<Layout> updateLayout(String pageId, String layoutId, Layout layout);
public Mono<Action> moveAction(ActionMoveDTO actionMoveDTO);
}

View File

@ -0,0 +1,199 @@
package com.appsmith.server.services;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.Page;
import com.appsmith.server.dtos.ActionMoveDTO;
import com.appsmith.server.dtos.DslActionDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.appsmith.server.helpers.MustacheHelper.extractMustacheKeys;
import static java.util.stream.Collectors.toSet;
@Service
@Slf4j
public class LayoutActionServiceImpl implements LayoutActionService {
private final ActionService actionService;
private final PageService pageService;
private final ObjectMapper objectMapper;
private final ApplicationPageService applicationPageService;
/*
* This pattern finds all the String which have been extracted from the mustache dynamic bindings.
* e.g. for the given JS function using action with name "fetchUsers"
* {{JSON.stringify(fetchUsers)}}
* This pattern should return ["JSON.stringify", "fetchUsers"]
*/
private final Pattern pattern = Pattern.compile("[a-zA-Z0-9._]+");
public LayoutActionServiceImpl(ActionService actionService,
PageService pageService,
ObjectMapper objectMapper,
ApplicationPageService applicationPageService) {
this.actionService = actionService;
this.pageService = pageService;
this.objectMapper = objectMapper;
this.applicationPageService = applicationPageService;
}
@Override
public Mono<Layout> updateLayout(String pageId, String layoutId, Layout layout) {
String dslString = "";
// Convert the DSL into a String
JSONObject dsl = layout.getDsl();
try {
dslString = objectMapper.writeValueAsString(dsl);
} catch (JsonProcessingException e) {
log.debug("Exception caught during conversion of DSL Json object to String. ", e);
}
Mono<Set<String>> dynamicBindingNamesMono = Mono.just(dslString)
// Extract all the mustache keys in the DSL to get the dynamic bindings used in the DSL.
.map(dslString1 -> extractMustacheKeys(dslString1))
.map(dynamicBindings -> {
Set<String> dynamicBindingNames = new HashSet<>();
if (!dynamicBindings.isEmpty()) {
for (String mustacheKey : dynamicBindings) {
String key = mustacheKey.trim();
// Extract all the words in the dynamic bindings
Matcher matcher = pattern.matcher(key);
while (matcher.find()) {
String word = matcher.group();
String[] subStrings = word.split(Pattern.quote("."));
if (subStrings.length > 0) {
// We are only interested in the top level. e.g. if its Input1.text, we want just Input1
dynamicBindingNames.add(subStrings[0]);
}
}
}
}
return dynamicBindingNames;
});
Mono<Set<DslActionDTO>> onLoadActionsMono = dynamicBindingNamesMono
.flatMapMany(dynamicBindingNames -> findRestApiActionsByPageIdAndHTTPMethodGET(dynamicBindingNames, pageId))
.map(action -> {
// Since we are only interested in few fields, prepare the DslActionDTO that needs to be stored in
// the layout and return it to be collected in to a set.
DslActionDTO newAction = new DslActionDTO();
newAction.setId(action.getId());
newAction.setPluginType(action.getPluginType());
newAction.setJsonPathKeys(action.getJsonPathKeys());
newAction.setName(action.getName());
return newAction;
})
.collect(toSet());
return pageService.findByIdAndLayoutsId(pageId, layoutId)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID + " or " + FieldName.LAYOUT_ID)))
.zipWith(onLoadActionsMono)
.map(tuple -> {
Page page = tuple.getT1();
Set<DslActionDTO> onLoadActions = tuple.getT2();
List<Layout> layoutList = page.getLayouts();
//Because the findByIdAndLayoutsId call returned non-empty result, we are guaranteed to find the layoutId here.
for (Layout storedLayout : layoutList) {
if (storedLayout.getId().equals(layoutId)) {
//Copy the variables to conserve before update
JSONObject publishedDsl = storedLayout.getPublishedDsl();
Set<DslActionDTO> publishedLayoutOnLoadActions = storedLayout.getPublishedLayoutOnLoadActions();
//Update
layout.setLayoutOnLoadActions(onLoadActions);
BeanUtils.copyProperties(layout, storedLayout);
storedLayout.setId(layoutId);
//Copy back the conserved variables.
storedLayout.setPublishedDsl(publishedDsl);
storedLayout.setPublishedLayoutOnLoadActions(publishedLayoutOnLoadActions);
break;
}
}
page.setLayouts(layoutList);
return page;
})
.flatMap(pageService::save)
.flatMap(page -> {
List<Layout> layoutList = page.getLayouts();
for (Layout storedLayout : layoutList) {
if (storedLayout.getId().equals(layoutId)) {
return Mono.just(storedLayout);
}
}
return Mono.empty();
});
}
/**
* Given a list of names of actions (nodes) and pageId, it hits the database and returns all the actions matching
* this criteria of name and pageId with http method 'GET'
*
* @param nodes
* @param pageId
* @return
*/
Flux<Action> findRestApiActionsByPageIdAndHTTPMethodGET(Set<String> nodes, String pageId) {
return actionService
.findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(nodes, pageId, "GET");
}
@Override
public Mono<Action> moveAction(ActionMoveDTO actionMoveDTO) {
Action action = actionMoveDTO.getAction();
String oldPageId = action.getPageId();
action.setPageId(actionMoveDTO.getDestinationPageId());
/*
* The following steps are followed here :
* 1. Update and save the action
* 2. Run updateLayout on the old page
* 3. Run updateLayout on the new page.
* 4. Return the saved action.
*/
return actionService
.save(action)
.flatMap(savedAction -> pageService
.findById(oldPageId)
.map(page -> page.getLayouts()
.stream()
/*
* subscribe() is being used here because within a stream, the master subscriber provided
* by spring framework does not get attached here leading to the updateLayout mono not
* emitting. The same is true for the updateLayout call for the new page.
*/
.map(layout -> updateLayout(oldPageId, layout.getId(), layout).subscribe())
.collect(toSet()))
.then(pageService.findById(actionMoveDTO.getDestinationPageId()))
.map(page -> page.getLayouts()
.stream()
.map(layout -> updateLayout(actionMoveDTO.getDestinationPageId(), layout.getId(), layout).subscribe())
.collect(toSet()))
.thenReturn(savedAction));
}
}

View File

@ -7,6 +7,4 @@ public interface LayoutService {
Mono<Layout> createLayout(String pageId, Layout layout);
Mono<Layout> getLayout(String pageId, String layoutId, Boolean viewMode);
Mono<Layout> updateLayout(String pageId, String layoutId, Layout layout);
}

View File

@ -1,27 +1,20 @@
package com.appsmith.server.services;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.Page;
import com.appsmith.server.dtos.DslActionDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.bson.types.ObjectId;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultEdge;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -29,7 +22,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.appsmith.server.helpers.MustacheHelper.extractMustacheKeys;
import static java.util.stream.Collectors.toSet;
@Slf4j
@Service
@ -37,7 +29,6 @@ public class LayoutServiceImpl implements LayoutService {
private final ApplicationPageService applicationPageService;
private final PageService pageService;
private final ActionService actionService;
/*
* This pattern finds all the String which have been extracted from the mustache dynamic bindings.
* e.g. for the given JS function using action with name "fetchUsers"
@ -45,17 +36,12 @@ public class LayoutServiceImpl implements LayoutService {
* This pattern should return ["JSON.stringify", "fetchUsers"]
*/
private final Pattern pattern = Pattern.compile("[a-zA-Z0-9._]+");
private final ObjectMapper objectMapper;
@Autowired
public LayoutServiceImpl(ApplicationPageService applicationPageService,
PageService pageService,
ActionService actionService,
ObjectMapper objectMapper) {
PageService pageService) {
this.applicationPageService = applicationPageService;
this.pageService = pageService;
this.actionService = actionService;
this.objectMapper = objectMapper;
}
@Override
@ -100,111 +86,6 @@ public class LayoutServiceImpl implements LayoutService {
});
}
@Override
public Mono<Layout> updateLayout(String pageId, String layoutId, Layout layout) {
Set<String> dynamicBindingNames = new HashSet<>();
String dslString = "";
// Convert the DSL into a String
JSONObject dsl = layout.getDsl();
try {
dslString = objectMapper.writeValueAsString(dsl);
} catch (JsonProcessingException e) {
log.debug("Exception caught during conversion of DSL Json object to String. ", e);
}
// Extract all the mustache keys in the DSL to get the dynamic bindings used in the DSL.
Set<String> dynamicBindings = extractMustacheKeys(dslString);
if (!dynamicBindings.isEmpty()) {
for (String mustacheKey : dynamicBindings) {
String key = mustacheKey.trim();
// Extract all the words in the dynamic bindings
Matcher matcher = pattern.matcher(key);
while (matcher.find()) {
String word = matcher.group();
String[] subStrings = word.split(Pattern.quote("."));
if (subStrings.length > 0 ) {
// We are only interested in the top level. e.g. if its Input1.text, we want just Input1
dynamicBindingNames.add(subStrings[0]);
}
}
}
}
Mono<Set<DslActionDTO>> onLoadActionsMono = findRestApiActionsByPageIdAndHTTPMethodGET(dynamicBindingNames, pageId)
.map(action -> {
// Since we are only interested in few fields, prepare the DslActionDTO that needs to be stored in
// the layout and return it to be collected in to a set.
DslActionDTO newAction = new DslActionDTO();
newAction.setId(action.getId());
newAction.setPluginType(action.getPluginType());
newAction.setJsonPathKeys(action.getJsonPathKeys());
newAction.setName(action.getName());
return newAction;
})
.collect(toSet());
return pageService.findByIdAndLayoutsId(pageId, layoutId)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID + " or " + FieldName.LAYOUT_ID)))
.flatMap(applicationPageService::doesPageBelongToCurrentUserOrganization)
//The pageId given is correct and belongs to the current user's organization.
.zipWith(onLoadActionsMono)
.map(tuple -> {
Page page = tuple.getT1();
Set<DslActionDTO> onLoadActions = tuple.getT2();
List<Layout> layoutList = page.getLayouts();
//Because the findByIdAndLayoutsId call returned non-empty result, we are guaranteed to find the layoutId here.
for (Layout storedLayout : layoutList) {
if (storedLayout.getId().equals(layoutId)) {
//Copy the variables to conserve before update
JSONObject publishedDsl = storedLayout.getPublishedDsl();
Set<DslActionDTO> publishedLayoutOnLoadActions = storedLayout.getPublishedLayoutOnLoadActions();
//Update
layout.setLayoutOnLoadActions(onLoadActions);
BeanUtils.copyProperties(layout, storedLayout);
storedLayout.setId(layoutId);
//Copy back the conserved variables.
storedLayout.setPublishedDsl(publishedDsl);
storedLayout.setPublishedLayoutOnLoadActions(publishedLayoutOnLoadActions);
break;
}
}
page.setLayouts(layoutList);
return page;
})
.flatMap(pageService::save)
.flatMap(page -> {
List<Layout> layoutList = page.getLayouts();
for (Layout storedLayout : layoutList) {
if (storedLayout.getId().equals(layoutId)) {
return Mono.just(storedLayout);
}
}
return Mono.empty();
});
}
/**
* Given a list of names of actions (nodes) and pageId, it hits the database and returns all the actions matching
* this criteria of name and pageId with http method 'GET'
*
* @param nodes
* @param pageId
* @return
*/
Flux<Action> findRestApiActionsByPageIdAndHTTPMethodGET(Set<String> nodes, String pageId) {
return actionService
.findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(nodes, pageId, "GET");
}
/**
* Walks the DSL and extracts all the widget names from it and adds it to the graph.

View File

@ -38,6 +38,9 @@ public class LayoutServiceTest {
@Autowired
PageService pageService;
@Autowired
LayoutActionService layoutActionService;
Mono<Layout> layoutMono;
Mono<Application> applicationMono;
@ -134,7 +137,7 @@ public class LayoutServiceTest {
obj.put("key", "value-updated");
updateLayout.setDsl(obj);
Mono<Layout> updatedLayoutMono = layoutService.updateLayout("random-impossible-id-page", startLayout.getId(), updateLayout);
Mono<Layout> updatedLayoutMono = layoutActionService.updateLayout("random-impossible-id-page", startLayout.getId(), updateLayout);
StepVerifier
.create(updatedLayoutMono)
@ -164,7 +167,7 @@ public class LayoutServiceTest {
obj1.put("key1", "value-updated");
updateLayout.setDsl(obj);
Mono<Layout> updatedLayoutMono = layoutService.updateLayout(page.getId(), startLayout.getId(), updateLayout);
Mono<Layout> updatedLayoutMono = layoutActionService.updateLayout(page.getId(), startLayout.getId(), updateLayout);
StepVerifier
.create(updatedLayoutMono)