feat: [Feature] Add slug for applications and pages (#8860)

Generates and stores a slug from application name and page names when they are created or updated. Also adds a migration to set slug to existing applications and pages.
This commit is contained in:
Nayan 2021-11-01 14:18:21 +06:00 committed by GitHub
parent 5637b4dfa4
commit 8e568d6056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 8 deletions

View File

@ -59,6 +59,8 @@ public class Application extends BaseDomain {
String icon;
private String slug;
@JsonIgnore
AppLayout unpublishedAppLayout;

View File

@ -25,6 +25,8 @@ public class PageDTO {
String name;
String slug;
@Transient
String applicationId;

View File

@ -0,0 +1,45 @@
package com.appsmith.server.helpers;
import lombok.extern.slf4j.Slf4j;
import java.text.Normalizer;
import java.util.Locale;
import java.util.regex.Pattern;
@Slf4j
public class TextUtils {
/*
* NON_LATIN regex matches any letter that is not a ASCII i.e. A-Za-z0-9, `-` and `_`
* It'll match the unicode letters.
* */
private static final Pattern NON_LATIN = Pattern.compile("[^\\w_-]");
/*
* The SEPARATORS pattern matches those characters which cane be replaced with `-`
* This includes Punctuation characters, `&`, space and not `-` itself. Details on `Punct`
* http://www.gnu.org/software/grep/manual/html_node/Character-Classes-and-Bracket-Expressions.html
* */
private static final Pattern SEPARATORS = Pattern.compile("[\\s\\p{Punct}&&[^-]]");
/**
* Creates URL safe text aka slug from the input text. It supports english locale only.
* See the test cases for sample conversions
* For other languages, it'll return empty.
* @param inputText String that'll be converted
* @return String, empty if failed due to encoding exception
*/
public static String makeSlug(String inputText) {
if(inputText == null) {
return "";
}
// remove all the separators with a `-`
String noseparators = SEPARATORS.matcher(inputText).replaceAll("-");
String normalized = Normalizer.normalize(noseparators, Normalizer.Form.NFD);
// remove any non ascii letter with empty
String slug = NON_LATIN.matcher(normalized).replaceAll("");
// convert to lower case, remove multiple consecutive `-` with single `-`
// if we've only `-` left and nothing else, replace it with empty string
return slug.toLowerCase(Locale.ENGLISH).replaceAll("-{2,}","-").replaceAll("^-|-$","");
}
}

View File

@ -35,6 +35,7 @@ import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.QApplication;
import com.appsmith.server.domains.QConfig;
import com.appsmith.server.domains.QNewAction;
import com.appsmith.server.domains.QNewPage;
import com.appsmith.server.domains.QOrganization;
import com.appsmith.server.domains.QPlugin;
import com.appsmith.server.domains.Role;
@ -46,6 +47,7 @@ import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.DslActionDTO;
import com.appsmith.server.dtos.OrganizationPluginStatus;
import com.appsmith.server.dtos.PageDTO;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.services.OrganizationService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.cloudyrock.mongock.ChangeLog;
@ -3580,4 +3582,42 @@ public class DatabaseChangelog {
// Now that the actions have completed the migrations, update the plugin to use the new UI form.
mongockTemplate.save(s3Plugin);
}
@ChangeSet(order = "094", id = "set-slug-to-application-and-page", author = "")
public void setSlugToApplicationAndPage(MongockTemplate mongockTemplate) {
// update applications
List<Application> applications = mongockTemplate.findAll(Application.class);
for(Application application : applications) {
mongockTemplate.updateFirst(
query(where(fieldName(QApplication.application.id)).is(application.getId())
.and(fieldName(QApplication.application.deleted)).is(false)),
new Update().set(fieldName(QApplication.application.slug), TextUtils.makeSlug(application.getName())),
Application.class
);
}
// update pages
List<NewPage> pages = mongockTemplate.findAll(NewPage.class);
for(NewPage page : pages) {
Update update = new Update();
if(page.getUnpublishedPage() != null) {
String fieldName = String.format("%s.%s",
fieldName(QNewPage.newPage.unpublishedPage), fieldName(QNewPage.newPage.unpublishedPage.slug)
);
update = update.set(fieldName, TextUtils.makeSlug(page.getUnpublishedPage().getName()));
}
if(page.getPublishedPage() != null) {
String fieldName = String.format("%s.%s",
fieldName(QNewPage.newPage.publishedPage), fieldName(QNewPage.newPage.publishedPage.slug)
);
update = update.set(fieldName, TextUtils.makeSlug(page.getPublishedPage().getName()));
}
mongockTemplate.updateFirst(
query(where(fieldName(QNewPage.newPage.id)).is(page.getId())
.and(fieldName(QNewPage.newPage.deleted)).is(false)),
update,
NewPage.class
);
}
}
}

View File

@ -19,6 +19,7 @@ import com.appsmith.server.dtos.ApplicationAccessDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.CommentThreadRepository;
import com.jcraft.jsch.JSch;
@ -146,6 +147,9 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
@Override
public Mono<Application> save(Application application) {
if(!StringUtils.isEmpty(application.getName())) {
application.setSlug(TextUtils.makeSlug(application.getName()));
}
return repository.save(application)
.flatMap(this::setTransientFields);
}
@ -156,13 +160,17 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
}
@Override
public Mono<Application> createDefault(Application object) {
return super.create(object);
public Mono<Application> createDefault(Application application) {
application.setSlug(TextUtils.makeSlug(application.getName()));
return super.create(application);
}
@Override
public Mono<Application> update(String id, Application application) {
application.setIsPublic(null);
if(!StringUtils.isEmpty(application.getName())) {
application.setSlug(TextUtils.makeSlug(application.getName()));
}
return repository.updateById(id, application, AclPermission.MANAGE_APPLICATIONS)
.onErrorResume(error -> {
if (error instanceof DuplicateKeyException) {

View File

@ -11,6 +11,7 @@ import com.appsmith.server.dtos.PageDTO;
import com.appsmith.server.dtos.PageNameIdDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.repositories.NewPageRepository;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
@ -21,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -128,8 +130,8 @@ public class NewPageServiceImpl extends BaseService<NewPageRepository, NewPage,
public Mono<PageDTO> createDefault(PageDTO object) {
NewPage newPage = new NewPage();
newPage.setUnpublishedPage(object);
newPage.setApplicationId(object.getApplicationId());
newPage.getUnpublishedPage().setSlug(TextUtils.makeSlug(object.getName()));
newPage.setPolicies(object.getPolicies());
if (newPage.getGitSyncId() == null) {
newPage.setGitSyncId(newPage.getApplicationId() + "_" + Instant.now().toString());
@ -379,13 +381,13 @@ public class NewPageServiceImpl extends BaseService<NewPageRepository, NewPage,
@Override
public Mono<PageDTO> updatePage(String id, PageDTO page) {
NewPage newPage = new NewPage();
newPage.setUnpublishedPage(page);
return repository.findById(id, AclPermission.MANAGE_PAGES)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PAGE, id)))
.flatMap(dbPage -> {
copyNewFieldValuesIntoOldObject(page, dbPage.getUnpublishedPage());
if(!StringUtils.isEmpty(page.getName())) {
dbPage.getUnpublishedPage().setSlug(TextUtils.makeSlug(page.getName()));
}
return this.update(id, dbPage);
})
.flatMap(savedPage -> applicationService.saveLastEditInformation(savedPage.getApplicationId())

View File

@ -0,0 +1,30 @@
package com.appsmith.server.helpers;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TextUtilsTest {
@Test
public void makeSlug() {
assertThat(TextUtils.makeSlug("Hello Darkness My Old Friend")).isEqualTo("hello-darkness-my-old-friend");
assertThat(TextUtils.makeSlug("Page1")).isEqualTo("page1");
assertThat(TextUtils.makeSlug("TestPage123")).isEqualTo("testpage123");
assertThat(TextUtils.makeSlug("Page 1")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug(" Page 1 ")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug("Page 1")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug("_page 1")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug("!page 1")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug("page 1!")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug("page__1")).isEqualTo("page-1");
assertThat(TextUtils.makeSlug("Hello (new)")).isEqualTo("hello-new");
assertThat(TextUtils.makeSlug("")).isEqualTo("");
assertThat(TextUtils.makeSlug(null)).isEqualTo("");
// text is hindi
assertThat(TextUtils.makeSlug("परीक्षण पृष्ठ")).isEqualTo("");
// text is in spanish
assertThat(TextUtils.makeSlug("página de prueba")).isEqualTo("pagina-de-prueba");
// text in chinese
assertThat(TextUtils.makeSlug("测试页")).isEqualTo("");
}
}

View File

@ -30,6 +30,7 @@ import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.MockPluginExecutor;
import com.appsmith.server.helpers.PluginExecutorHelper;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository;
@ -173,6 +174,7 @@ public class ApplicationServiceTest {
.create(applicationMono)
.assertNext(application -> {
assertThat(application).isNotNull();
assertThat(application.getSlug()).isEqualTo(TextUtils.makeSlug(application.getName()));
assertThat(application.isAppIsExample()).isFalse();
assertThat(application.getId()).isNotNull();
assertThat(application.getName().equals("ApplicationServiceTest TestApp"));
@ -325,6 +327,7 @@ public class ApplicationServiceTest {
assertThat(t.getId()).isNotNull();
assertThat(t.getPolicies()).isNotEmpty();
assertThat(t.getName()).isEqualTo("NewValidUpdateApplication-Test");
assertThat(t.getSlug()).isEqualTo(TextUtils.makeSlug(t.getName()));
})
.verifyComplete();
}

View File

@ -19,6 +19,7 @@ import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.MockPluginExecutor;
import com.appsmith.server.helpers.PluginExecutorHelper;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.repositories.PluginRepository;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONArray;
@ -165,7 +166,9 @@ public class PageServiceTest {
.assertNext(page -> {
assertThat(page).isNotNull();
assertThat(page.getId()).isNotNull();
assertThat("PageServiceTest TestApp".equals(page.getName()));
assertThat(page.getName()).isEqualTo("PageServiceTest TestApp");
assertThat(page.getSlug()).isEqualTo(TextUtils.makeSlug(page.getName()));
assertThat(page.getPolicies()).isNotEmpty();
assertThat(page.getPolicies()).containsOnly(managePagePolicy, readPagePolicy);
@ -243,7 +246,8 @@ public class PageServiceTest {
.assertNext(page -> {
assertThat(page).isNotNull();
assertThat(page.getId()).isNotNull();
assertThat("New Page Name".equals(page.getName()));
assertThat(page.getName()).isEqualTo("New Page Name");
assertThat(page.getSlug()).isEqualTo(TextUtils.makeSlug(page.getName()));
// Check for the policy object not getting overwritten during update
assertThat(page.getPolicies()).isNotEmpty();