Skip to content

Commit

Permalink
feat: Track recently forked templates (appsmithorg#13159)
Browse files Browse the repository at this point in the history
This PR adds feature to track recently forked templates by the user.
  • Loading branch information
Nayan committed May 10, 2022
1 parent 005c391 commit c75e330
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public ApplicationTemplateControllerCE(ApplicationTemplateService applicationTem

@GetMapping
public Mono<ResponseDTO<List<ApplicationTemplate>>> getAll() {
return applicationTemplateService.getActiveTemplates().collectList()
return applicationTemplateService.getActiveTemplates(null).collectList()
.map(templates -> new ResponseDTO<>(HttpStatus.OK.value(), templates, null));
}

Expand Down Expand Up @@ -52,4 +52,10 @@ public Mono<ResponseDTO<Application>> importApplicationFromTemplate(@PathVariabl
return applicationTemplateService.importApplicationFromTemplate(templateId, organizationId)
.map(importedApp -> new ResponseDTO<>(HttpStatus.OK.value(), importedApp, null));
}

@GetMapping("recent")
public Mono<ResponseDTO<List<ApplicationTemplate>>> getRecentlyUsedTemplates() {
return applicationTemplateService.getRecentlyUsedTemplates().collectList()
.map(templates -> new ResponseDTO<>(HttpStatus.OK.value(), templates, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public class UserData extends BaseDomain {

Map<String, Object> userClaims;

// list of template ids that were recently forked by the user
private List<String> recentlyUsedTemplateIds;

public GitProfile getGitProfileByKey(String key) {
// Always use DEFAULT_GIT_PROFILE as fallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public class ApplicationTemplateServiceImpl extends ApplicationTemplateServiceCE
public ApplicationTemplateServiceImpl(CloudServicesConfig cloudServicesConfig,
ReleaseNotesService releaseNotesService,
ImportExportApplicationService importExportApplicationService,
AnalyticsService analyticsService) {
super(cloudServicesConfig, releaseNotesService, importExportApplicationService, analyticsService);
AnalyticsService analyticsService,
UserDataService userDataService) {
super(cloudServicesConfig, releaseNotesService, importExportApplicationService, analyticsService, userDataService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;

public interface ApplicationTemplateServiceCE {
Flux<ApplicationTemplate> getActiveTemplates();
Flux<ApplicationTemplate> getActiveTemplates(List<String> templateIds);
Flux<ApplicationTemplate> getSimilarTemplates(String templateId);
Flux<ApplicationTemplate> getRecentlyUsedTemplates();
Mono<ApplicationTemplate> getTemplateDetails(String templateId);
Mono<Application> importApplicationFromTemplate(String templateId, String organizationId);
Mono<ApplicationTemplate> getFilters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,26 @@
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.UserDataService;
import com.appsmith.server.solutions.ImportExportApplicationService;
import com.appsmith.server.solutions.ReleaseNotesService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Type;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -33,15 +38,18 @@ public class ApplicationTemplateServiceCEImpl implements ApplicationTemplateServ
private final ReleaseNotesService releaseNotesService;
private final ImportExportApplicationService importExportApplicationService;
private final AnalyticsService analyticsService;
private final UserDataService userDataService;

public ApplicationTemplateServiceCEImpl(CloudServicesConfig cloudServicesConfig,
ReleaseNotesService releaseNotesService,
ImportExportApplicationService importExportApplicationService,
AnalyticsService analyticsService) {
AnalyticsService analyticsService,
UserDataService userDataService) {
this.cloudServicesConfig = cloudServicesConfig;
this.releaseNotesService = releaseNotesService;
this.importExportApplicationService = importExportApplicationService;
this.analyticsService = analyticsService;
this.userDataService = userDataService;
}

@Override
Expand All @@ -64,11 +72,21 @@ public Flux<ApplicationTemplate> getSimilarTemplates(String templateId) {
}

@Override
public Flux<ApplicationTemplate> getActiveTemplates() {
public Flux<ApplicationTemplate> getActiveTemplates(List<String> templateIds) {
final String baseUrl = cloudServicesConfig.getBaseUrl();

return WebClient
.create(baseUrl + "/api/v1/app-templates?version=" + releaseNotesService.getReleasedVersion())
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance()
.queryParam("version", releaseNotesService.getReleasedVersion());

if(!CollectionUtils.isEmpty(templateIds)) {
uriComponentsBuilder.queryParam("id", templateIds);
}

// uriComponents will build url in format: version=version&id=id1&id=id2&id=id3
UriComponents uriComponents = uriComponentsBuilder.build();

Flux<ApplicationTemplate> applicationTemplateFlux = WebClient
.create(baseUrl + "/api/v1/app-templates?" + uriComponents.getQuery())
.get()
.exchangeToFlux(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.OK)) {
Expand All @@ -79,6 +97,16 @@ public Flux<ApplicationTemplate> getActiveTemplates() {
return clientResponse.createException().flatMapMany(Flux::error);
}
});

if(!CollectionUtils.isEmpty(templateIds)) {
// the items should be sorted based on the order of the templateIds when it's not empty
applicationTemplateFlux = applicationTemplateFlux.sort(
// sort based on index of id in templateIds list.
// index of first item will be less than index of next item
Comparator.comparingInt(o -> templateIds.indexOf(o.getId()))
);
}
return applicationTemplateFlux;
}

@Override
Expand Down Expand Up @@ -137,8 +165,19 @@ public Mono<Application> importApplicationFromTemplate(String templateId, String
applicationTemplate.setId(templateId);
Map<String, Object> extraProperties = new HashMap<>();
extraProperties.put("templateAppName", application.getName());
return analyticsService.sendObjectEvent(AnalyticsEvents.FORK, applicationTemplate, extraProperties)
.thenReturn(application);
return userDataService.addTemplateIdToLastUsedList(templateId).then(
analyticsService.sendObjectEvent(AnalyticsEvents.FORK, applicationTemplate, extraProperties)
).thenReturn(application);
});
}

@Override
public Flux<ApplicationTemplate> getRecentlyUsedTemplates() {
return userDataService.getForCurrentUser().flatMapMany(userData -> {
if(!CollectionUtils.isEmpty(userData.getRecentlyUsedTemplateIds())) {
return getActiveTemplates(userData.getRecentlyUsedTemplateIds());
}
return Flux.just();
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public interface UserDataServiceCE {

Mono<UserData> updateLastUsedAppAndOrgList(Application application);

Mono<UserData> addTemplateIdToLastUsedList(String templateId);

Mono<Map<String, Boolean>> getFeatureFlagsForCurrentUser();

Mono<UserData> setCommentState(CommentOnboardingState commentOnboardingState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ public Mono<UserData> updateLastUsedAppAndOrgList(Application application) {
});
}

@Override
public Mono<UserData> addTemplateIdToLastUsedList(String templateId) {
return this.getForCurrentUser().flatMap(userData -> {
// set recently used template ids
userData.setRecentlyUsedTemplateIds(
addIdToRecentList(userData.getRecentlyUsedTemplateIds(), templateId, 5)
);
return repository.save(userData);
});
}

private List<String> addIdToRecentList(List<String> srcIdList, String newId, int maxSize) {
if(srcIdList == null) {
srcIdList = new ArrayList<>();
Expand All @@ -274,7 +285,7 @@ private List<String> addIdToRecentList(List<String> srcIdList, String newId, int
if(srcIdList.size() > 1) {
CollectionUtils.removeDuplicates(srcIdList);
}
// keeping the last 10 org ids, there may be a lot of deleted organization ids which are not used anymore
// keeping the last maxSize ids, there may be a lot of ids which are not used anymore
if(srcIdList.size() > maxSize) {
srcIdList = srcIdList.subList(0, maxSize);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ public void updateLastUsedAppAndOrgList_WhenListIsEmpty_orgIdPrepended() {
return userDataRepository.save(userData);
}).then(userDataService.updateLastUsedAppAndOrgList(application));

userDataService.updateLastUsedAppAndOrgList(application);
StepVerifier.create(saveMono).assertNext(userData -> {
Assert.assertEquals(1, userData.getRecentlyUsedOrgIds().size());
Assert.assertEquals(sampleOrgId, userData.getRecentlyUsedOrgIds().get(0));
Expand Down Expand Up @@ -278,6 +277,66 @@ public void updateLastUsedAppAndOrgList_TooManyRecentIds_ListsAreTruncated() {
}).verifyComplete();
}

@Test
@WithUserDetails(value = "api_user")
public void addTemplateIdToLastUsedList_WhenListIsEmpty_templateIdPrepended() {
final Mono<UserData> saveMono = userDataService.getForCurrentUser().flatMap(userData -> {
// set recently used template ids to null
userData.setRecentlyUsedTemplateIds(null);
return userDataRepository.save(userData);
}).then(userDataService.addTemplateIdToLastUsedList("123456"));

StepVerifier.create(saveMono).assertNext(userData -> {
Assert.assertEquals(1, userData.getRecentlyUsedTemplateIds().size());
Assert.assertEquals("123456", userData.getRecentlyUsedTemplateIds().get(0));
}).verifyComplete();
}

@Test
@WithUserDetails(value = "api_user")
public void addTemplateIdToLastUsedList_WhenListIsNotEmpty_templateIdPrepended() {
final Mono<UserData> resultMono = userDataService.getForCurrentUser().flatMap(userData -> {
// Set an initial list of template ids to the current user.
userData.setRecentlyUsedTemplateIds(Arrays.asList("123", "456"));
return userDataRepository.save(userData);
}).flatMap(userData -> {
// Now check whether a new template id is put at first.
String newTemplateId = "456";
return userDataService.addTemplateIdToLastUsedList(newTemplateId);
});

StepVerifier.create(resultMono).assertNext(userData -> {
Assert.assertEquals(2, userData.getRecentlyUsedTemplateIds().size());
Assert.assertEquals("456", userData.getRecentlyUsedTemplateIds().get(0));
Assert.assertEquals("123", userData.getRecentlyUsedTemplateIds().get(1));
}).verifyComplete();
}

@Test
@WithUserDetails(value = "api_user")
public void addTemplateIdToLastUsedList_TooManyRecentIds_ListsAreTruncated() {
String newTemplateId = "new-template-id";

final Mono<UserData> resultMono = userDataService.getForCurrentUser().flatMap(userData -> {
// Set an initial list of 12 template ids to the current user
userData.setRecentlyUsedTemplateIds(new ArrayList<>());
for(int i = 1; i <= 12; i++) {
userData.getRecentlyUsedTemplateIds().add("template-" + i);
}
return userDataRepository.save(userData);
}).flatMap(userData -> {
// Now check whether a new template id is put at first.
return userDataService.addTemplateIdToLastUsedList(newTemplateId);
});

StepVerifier.create(resultMono).assertNext(userData -> {
// org id list should be truncated to 10
assertThat(userData.getRecentlyUsedTemplateIds().size()).isEqualTo(5);
assertThat(userData.getRecentlyUsedTemplateIds().get(0)).isEqualTo(newTemplateId);
assertThat(userData.getRecentlyUsedTemplateIds().get(4)).isEqualTo("template-4");
}).verifyComplete();
}

@Test
@WithUserDetails(value = "api_user")
public void deleteProfilePhotot_WhenExists_RemovedFromAssetAndUserData() {
Expand Down

0 comments on commit c75e330

Please sign in to comment.