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:
parent
5637b4dfa4
commit
8e568d6056
|
|
@ -59,6 +59,8 @@ public class Application extends BaseDomain {
|
|||
|
||||
String icon;
|
||||
|
||||
private String slug;
|
||||
|
||||
@JsonIgnore
|
||||
AppLayout unpublishedAppLayout;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ public class PageDTO {
|
|||
|
||||
String name;
|
||||
|
||||
String slug;
|
||||
|
||||
@Transient
|
||||
String applicationId;
|
||||
|
||||
|
|
|
|||
|
|
@ -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("^-|-$","");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user